Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions charts/operator/crds/agent.rossoctl.dev_agentruntimes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,53 @@
- permissive
- strict
type: string
onError:
description: |-
OnError sets the chain-default policy applied to every selected plugin that
has no explicit per-plugin override in Plugins. enforce is the framework
default (on_error omitted); observe/off are emitted explicitly. Ignored when
PluginPreset is unset.
enum:
- enforce
- observe
- "off"
type: string
pluginPreset:
description: |-
PluginPreset selects an AuthBridge layer-3 plugin-pipeline preset for this
workload's per-agent authbridge-config-<name> ConfigMap. When set, the
admission webhook renders the full canonical pipeline (all supported plugins
in fixed order; unselected ones emitted with on_error: off) instead of the
default two-plugin (jwt-validation + token-exchange) synthesis.

Presets (membership; every preset seeds its plugins at policy "enforce"):
auth-only inbound: jwt-validation outbound: token-exchange
ibac-only inbound: a2a-parser outbound: inference-parser, mcp-parser, ibac
full inbound: a2a-parser, jwt-validation outbound: token-exchange,
inference-parser, mcp-parser, ibac

Only honored on the proxy-sidecar / lite paths (the plugin pipeline lives in
the per-agent ConfigMap those modes mount). Requires the AuthBridge sidecar to
be injected.
enum:
- auth-only
- ibac-only
- full
type: string
plugins:
description: |-
Plugins carries per-plugin policy overrides layered on top of PluginPreset, as
"NAME:POLICY" tokens (e.g. "ibac:observe"). POLICY is one of enforce|observe|off
and maps to the plugin entry's on_error field: enforce omits on_error (the
framework default), observe sets on_error: observe, off sets on_error: off.
A plugin named here that isn't part of the preset is added at the given policy.
Ignored when PluginPreset is unset.

token-exchange and token-broker are mutually exclusive on the outbound chain;
a spec that activates both is rejected by admission.
items:
type: string
type: array
targetRef:
description: TargetRef identifies the workload backing this agent
runtime (duck typing).
Expand Down Expand Up @@ -485,7 +532,7 @@
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.

Check warning on line 535 in charts/operator/crds/agent.rossoctl.dev_agentruntimes.yaml

View workflow job for this annotation

GitHub Actions / YAML Lint

535:151 [line-length] line too long (162 > 150 characters)
format: date-time
type: string
message:
Expand All @@ -497,7 +544,7 @@
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date

Check warning on line 547 in charts/operator/crds/agent.rossoctl.dev_agentruntimes.yaml

View workflow job for this annotation

GitHub Actions / YAML Lint

547:151 [line-length] line too long (162 > 150 characters)
with respect to the current state of the instance.
format: int64
minimum: 0
Expand Down
25 changes: 25 additions & 0 deletions charts/operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,28 @@ defaults:
jwt_svids = [{jwt_audience="http://keycloak.localtest.me:8080/realms/rossoctl", jwt_svid_file_name="/opt/jwt_svid.token"}]
jwt_svid_file_mode = 0644
include_federated_domains = true

# IBAC (intent-based access control) judge settings. The operator stamps
# these into the ibac plugin's config when a workload selects an AuthBridge
# plugin preset that includes ibac (spec.pluginPreset: ibac-only | full).
# The ibac plugin calls this OpenAI-compatible "judge" LLM to decide whether
# an outbound action matches the user's stated intent.
#
# judgeEndpoint/judgeModel MUST be set before an ibac-bearing preset can
# enforce — an ibac plugin rendered with an empty judge_endpoint fails the
# sidecar's config reload. When they are empty the operator still emits the
# plugin (so the pipeline shape is correct) but logs a warning at admission.
# The judge system prompt is baked into the operator binary, not sourced here.
ibac:
# OpenAI-compatible base URL of the judge LLM, e.g.
# "http://litellm.rossoctl-system.svc.cluster.local:4000".
judgeEndpoint: ""
# Model id the judge call uses, e.g. "llama3.2:3b".
judgeModel: ""
# Bounds the judge call in milliseconds. Mirrors the harness default (15000).
timeoutMs: 15000
# Optional: the agent's own LLM host, so the judge can distinguish agent
# LLM traffic from tool traffic. Omitted from the plugin config when empty.
agentLlmHost: ""
# Optional: bearer token for the judge endpoint. Omitted when empty.
judgeBearer: ""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: judgeBearer will end up in a ConfigMap (plaintext). For a bearer token, consider sourcing from a Secret reference in a follow-up — avoids credential-in-ConfigMap for anyone with namespace read.

63 changes: 63 additions & 0 deletions operator/api/v1alpha1/agentruntime_plugins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
Copyright 2026.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import "testing"

func TestValidatePlugins(t *testing.T) {
tests := []struct {
name string
spec AgentRuntimeSpec
wantErr bool
}{
{
name: "no preset is a no-op even with malformed plugins",
spec: AgentRuntimeSpec{Plugins: []string{"bogus:loud"}},
},
{
name: "valid tokens with preset",
spec: AgentRuntimeSpec{PluginPreset: "full", Plugins: []string{"ibac:observe", "mcp-parser:off"}},
},
{
name: "bare name defaults to enforce",
spec: AgentRuntimeSpec{PluginPreset: "full", Plugins: []string{"ibac"}},
},
{
name: "unknown plugin name",
spec: AgentRuntimeSpec{PluginPreset: "full", Plugins: []string{"nope:enforce"}},
wantErr: true,
},
{
name: "invalid policy",
spec: AgentRuntimeSpec{PluginPreset: "full", Plugins: []string{"ibac:loud"}},
wantErr: true,
},
{
name: "empty token",
spec: AgentRuntimeSpec{PluginPreset: "full", Plugins: []string{" "}},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.spec.ValidatePlugins()
if (err != nil) != tt.wantErr {
t.Errorf("ValidatePlugins() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
92 changes: 92 additions & 0 deletions operator/api/v1alpha1/agentruntime_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ limitations under the License.
package v1alpha1

import (
"fmt"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -163,6 +166,48 @@ type AgentRuntimeSpec struct {
//
// +optional
Auth *AuthConfig `json:"auth,omitempty"`

// PluginPreset selects an AuthBridge layer-3 plugin-pipeline preset for this
// workload's per-agent authbridge-config-<name> ConfigMap. When set, the
// admission webhook renders the full canonical pipeline (all supported plugins
// in fixed order; unselected ones emitted with on_error: off) instead of the
// default two-plugin (jwt-validation + token-exchange) synthesis.
//
// Presets (membership; every preset seeds its plugins at policy "enforce"):
// auth-only inbound: jwt-validation outbound: token-exchange
// ibac-only inbound: a2a-parser outbound: inference-parser, mcp-parser, ibac
// full inbound: a2a-parser, jwt-validation outbound: token-exchange,
// inference-parser, mcp-parser, ibac
//
// Only honored on the proxy-sidecar / lite paths (the plugin pipeline lives in
// the per-agent ConfigMap those modes mount). Requires the AuthBridge sidecar to
// be injected.
//
// +optional
// +kubebuilder:validation:Enum=auth-only;ibac-only;full
PluginPreset string `json:"pluginPreset,omitempty"`

// Plugins carries per-plugin policy overrides layered on top of PluginPreset, as
// "NAME:POLICY" tokens (e.g. "ibac:observe"). POLICY is one of enforce|observe|off
// and maps to the plugin entry's on_error field: enforce omits on_error (the
// framework default), observe sets on_error: observe, off sets on_error: off.
// A plugin named here that isn't part of the preset is added at the given policy.
// Ignored when PluginPreset is unset.
//
// token-exchange and token-broker are mutually exclusive on the outbound chain;
// a spec that activates both is rejected by admission.
//
// +optional
Plugins []string `json:"plugins,omitempty"`

// OnError sets the chain-default policy applied to every selected plugin that
// has no explicit per-plugin override in Plugins. enforce is the framework
// default (on_error omitted); observe/off are emitted explicitly. Ignored when
// PluginPreset is unset.
//
// +optional
// +kubebuilder:validation:Enum=enforce;observe;off
OnError string `json:"onError,omitempty"`
}

// AuthConfig defines authentication configuration for an agent or tool.
Expand Down Expand Up @@ -209,6 +254,53 @@ type RouteMatch struct {
HostRegex string `json:"hostRegex,omitempty"`
}

// SupportedAuthBridgePlugins is the set of plugin names accepted in
// AgentRuntimeSpec.Plugins override tokens. Kept in this leaf API package so
// both the validating webhook and the injector's pipeline renderer reference
// one list without an import cycle. Keep in sync with the injector's canonical
// inbound/outbound order lists (internal/webhook/injector/preset_pipeline.go).
var SupportedAuthBridgePlugins = map[string]bool{
"a2a-parser": true,
"jwt-validation": true,
"token-exchange": true,
"token-broker": true,
"inference-parser": true,
"mcp-parser": true,
"ibac": true,
}

// supportedAuthBridgePolicies is the legal per-plugin / chain-default policy set.
var supportedAuthBridgePolicies = map[string]bool{"enforce": true, "observe": true, "off": true}

// ValidatePlugins checks the spec.plugins override tokens ("NAME:POLICY") for a
// known plugin name and a valid policy. No-op when pluginPreset is unset (the
// preset gates whether Plugins is honored). The token-exchange/token-broker
// mutex is enforced at render time, where preset membership is resolved.
func (s *AgentRuntimeSpec) ValidatePlugins() error {
if s.PluginPreset == "" {
return nil
}
for _, tok := range s.Plugins {
t := strings.TrimSpace(tok)
if t == "" {
return fmt.Errorf("empty plugin override token in spec.plugins")
}
name := t
policy := "enforce"
if idx := strings.Index(t, ":"); idx >= 0 {
name = strings.TrimSpace(t[:idx])
policy = strings.TrimSpace(t[idx+1:])
}
if !SupportedAuthBridgePlugins[name] {
return fmt.Errorf("unknown plugin %q in spec.plugins token %q", name, tok)
}
if !supportedAuthBridgePolicies[policy] {
return fmt.Errorf("invalid policy %q in spec.plugins token %q (want enforce, observe, or off)", policy, tok)
}
}
return nil
}

// CardStatus holds the fetched A2A agent card data along with fetch metadata
// and optional verification results. Populated by the card discovery phase when
// --enable-card-discovery is set.
Expand Down
5 changes: 5 additions & 0 deletions operator/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions operator/config/crd/bases/agent.rossoctl.dev_agentruntimes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,53 @@ spec:
- permissive
- strict
type: string
onError:
description: |-
OnError sets the chain-default policy applied to every selected plugin that
has no explicit per-plugin override in Plugins. enforce is the framework
default (on_error omitted); observe/off are emitted explicitly. Ignored when
PluginPreset is unset.
enum:
- enforce
- observe
- "off"
type: string
pluginPreset:
description: |-
PluginPreset selects an AuthBridge layer-3 plugin-pipeline preset for this
workload's per-agent authbridge-config-<name> ConfigMap. When set, the
admission webhook renders the full canonical pipeline (all supported plugins
in fixed order; unselected ones emitted with on_error: off) instead of the
default two-plugin (jwt-validation + token-exchange) synthesis.

Presets (membership; every preset seeds its plugins at policy "enforce"):
auth-only inbound: jwt-validation outbound: token-exchange
ibac-only inbound: a2a-parser outbound: inference-parser, mcp-parser, ibac
full inbound: a2a-parser, jwt-validation outbound: token-exchange,
inference-parser, mcp-parser, ibac

Only honored on the proxy-sidecar / lite paths (the plugin pipeline lives in
the per-agent ConfigMap those modes mount). Requires the AuthBridge sidecar to
be injected.
enum:
- auth-only
- ibac-only
- full
type: string
plugins:
description: |-
Plugins carries per-plugin policy overrides layered on top of PluginPreset, as
"NAME:POLICY" tokens (e.g. "ibac:observe"). POLICY is one of enforce|observe|off
and maps to the plugin entry's on_error field: enforce omits on_error (the
framework default), observe sets on_error: observe, off sets on_error: off.
A plugin named here that isn't part of the preset is added at the given policy.
Ignored when PluginPreset is unset.

token-exchange and token-broker are mutually exclusive on the outbound chain;
a spec that activates both is rejected by admission.
items:
type: string
type: array
targetRef:
description: TargetRef identifies the workload backing this agent
runtime (duck typing).
Expand Down
7 changes: 7 additions & 0 deletions operator/internal/webhook/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,12 @@ func CompiledDefaults() *PlatformConfig {
LogLevel: "info",
EnableMetrics: true,
},
// IBAC judge defaults. JudgeEndpoint/JudgeModel are intentionally empty —
// they must be configured (Helm defaults.ibac.*) before an ibac-bearing
// preset (ibac-only / full) can enforce. TimeoutMS mirrors the harness
// default (authbridge/apply-pipeline.sh IBAC_TIMEOUT_MS default 15000).
IBAC: IBACConfig{
TimeoutMS: 15000,
},
}
}
29 changes: 29 additions & 0 deletions operator/internal/webhook/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type PlatformConfig struct {
TokenExchange TokenExchangeDefaults `json:"tokenExchange" yaml:"tokenExchange"`
Spiffe SpiffeConfig `json:"spiffe" yaml:"spiffe"`
Observability ObservabilityConfig `json:"observability" yaml:"observability"`
IBAC IBACConfig `json:"ibac" yaml:"ibac"`
}

type ImageConfig struct {
Expand Down Expand Up @@ -110,6 +111,34 @@ type ObservabilityConfig struct {
EnableMetrics bool `json:"enableMetrics" yaml:"enableMetrics"`
}

// IBACConfig holds the platform-level settings the operator stamps into the
// ibac plugin's config when a workload selects an AuthBridge preset that
// includes ibac (ibac-only / full). These are the LLM "judge" the ibac plugin
// calls to decide whether an outbound action matches the user's intent.
//
// JudgeEndpoint/JudgeModel must be set for ibac to enforce — an ibac plugin
// rendered without a judge_endpoint fails the sidecar's config reload. When
// they are empty the operator still emits the plugin (so the pipeline shape is
// correct) but logs a warning; the preset should be configured before use.
//
// The ibac judge system prompt is baked into the operator binary
// (ibacSystemPrompt in pod_mutator.go), not sourced from here.
type IBACConfig struct {
// JudgeEndpoint is the OpenAI-compatible base URL of the judge LLM,
// e.g. "http://litellm.rossoctl-system.svc.cluster.local:4000".
JudgeEndpoint string `json:"judgeEndpoint" yaml:"judgeEndpoint"`
// JudgeModel is the model id the judge call uses, e.g. "llama3.2:3b".
JudgeModel string `json:"judgeModel" yaml:"judgeModel"`
// TimeoutMS bounds the judge call. Defaults to 15000 when unset.
TimeoutMS int `json:"timeoutMs" yaml:"timeoutMs"`
// AgentLLMHost optionally names the agent's own LLM host so the judge can
// distinguish agent LLM traffic from tool traffic. Omitted when empty.
AgentLLMHost string `json:"agentLlmHost" yaml:"agentLlmHost"`
// JudgeBearer optionally supplies the bearer token for the judge endpoint.
// Omitted when empty.
JudgeBearer string `json:"judgeBearer" yaml:"judgeBearer"`
}

// DeepCopy creates a copy of the config
func (c *PlatformConfig) DeepCopy() *PlatformConfig {
if c == nil {
Expand Down
Loading
Loading