diff --git a/exgentic_a2a_runner/authbridge/apply-pipeline.sh b/exgentic_a2a_runner/authbridge/apply-pipeline.sh index e3fa5b9..b132918 100755 --- a/exgentic_a2a_runner/authbridge/apply-pipeline.sh +++ b/exgentic_a2a_runner/authbridge/apply-pipeline.sh @@ -79,6 +79,11 @@ JWT_VALIDATION_KEYCLOAK_URL="${JWT_VALIDATION_KEYCLOAK_URL:-$(_ab_base_key KEYCL JWT_VALIDATION_KEYCLOAK_REALM="${JWT_VALIDATION_KEYCLOAK_REALM:-$(_ab_base_key KEYCLOAK_REALM)}" export JWT_VALIDATION_ISSUER JWT_VALIDATION_KEYCLOAK_URL JWT_VALIDATION_KEYCLOAK_REALM +# --- SPARC defaults (consumed only when sparc is in the resolved set). +SPARC_REFLECTOR_ENDPOINT="${SPARC_REFLECTOR_ENDPOINT:-http://sparc-service.rossoctl-system.svc.cluster.local:8090}" +SPARC_TIMEOUT_MS="${SPARC_TIMEOUT_MS:-30000}" +export SPARC_REFLECTOR_ENDPOINT SPARC_TIMEOUT_MS + echo "Applying AuthBridge pipeline to $NAMESPACE/$AGENT_NAME" echo " Plugins: $PIPELINE_PLUGINS" if [ -n "${PIPELINE_OVERLAY_FILE:-}" ]; then @@ -113,7 +118,7 @@ fi # --- Pre-flight: validate plugin names in PIPELINE_PLUGINS up front so # we fail before any kubectl call. -KNOWN_PLUGINS=(jwt-validation token-exchange token-broker a2a-parser mcp-parser inference-parser ibac) +KNOWN_PLUGINS=(jwt-validation token-exchange token-broker a2a-parser mcp-parser inference-parser sparc ibac) VALID_POLICIES=(enforce observe off) is_known() { local needle=$1; shift; for x in "$@"; do [[ "$x" == "$needle" ]] && return 0; done; return 1; } for tok in $PIPELINE_PLUGINS; do diff --git a/exgentic_a2a_runner/authbridge/pipeline-merge.py b/exgentic_a2a_runner/authbridge/pipeline-merge.py index 3cfe749..52a68b0 100755 --- a/exgentic_a2a_runner/authbridge/pipeline-merge.py +++ b/exgentic_a2a_runner/authbridge/pipeline-merge.py @@ -49,7 +49,8 @@ "token-broker", # gate (mutex with token-exchange) "inference-parser", # observe-only, after token gate "mcp-parser", # observe-only, after token gate - "ibac", # last: reads parser extension slots + "sparc", # semantic pre-execution guardrail; reads parser slots + "ibac", # intent-based access control; reads parser extension slots ] CHAIN_FOR: dict[str, str] = {p: "inbound" for p in INBOUND_ORDER} diff --git a/exgentic_a2a_runner/authbridge/plugins/sparc.yaml b/exgentic_a2a_runner/authbridge/plugins/sparc.yaml new file mode 100644 index 0000000..8d37bf8 --- /dev/null +++ b/exgentic_a2a_runner/authbridge/plugins/sparc.yaml @@ -0,0 +1,14 @@ +# SPARC plugin fragment. +# +# ${VAR} placeholders are expanded by apply-pipeline.sh (envsubst) before +# the merge runs. SPARC_REFLECTOR_ENDPOINT defaults to the in-cluster +# sparc-service deployed by kagenti-extensions. +name: sparc +config: + reflector_endpoint: "${SPARC_REFLECTOR_ENDPOINT}" + enforcement: "mcp" + track: "fast_track" + on_reject_action: "observe" + deny_score_threshold: 0 + fail_policy: "open" + timeout_ms: ${SPARC_TIMEOUT_MS} diff --git a/exgentic_a2a_runner/authbridge/presets/full.yaml b/exgentic_a2a_runner/authbridge/presets/full.yaml index 55d7dab..246dbe3 100644 --- a/exgentic_a2a_runner/authbridge/presets/full.yaml +++ b/exgentic_a2a_runner/authbridge/presets/full.yaml @@ -1,4 +1,4 @@ # full preset. -# Everything on: auth + token exchange + parsers + IBAC. +# Everything on: auth + token exchange + parsers + SPARC + IBAC. inbound: [a2a-parser, jwt-validation] -outbound: [token-exchange, inference-parser, mcp-parser, ibac] +outbound: [token-exchange, inference-parser, mcp-parser, sparc, ibac] diff --git a/exgentic_a2a_runner/authbridge/presets/sparc-only.yaml b/exgentic_a2a_runner/authbridge/presets/sparc-only.yaml new file mode 100644 index 0000000..9cbb415 --- /dev/null +++ b/exgentic_a2a_runner/authbridge/presets/sparc-only.yaml @@ -0,0 +1,6 @@ +# sparc-only preset. +# SPARC guardrails without inbound auth (e.g. an upstream gateway has already +# terminated auth, or the runner calls the agent directly without a JWT). +# Outbound SPARC needs the parsers to populate extension slots. +inbound: [a2a-parser] +outbound: [inference-parser, mcp-parser, sparc] diff --git a/exgentic_a2a_runner/deploy-agent.sh b/exgentic_a2a_runner/deploy-agent.sh index 2107dd1..414a32a 100755 --- a/exgentic_a2a_runner/deploy-agent.sh +++ b/exgentic_a2a_runner/deploy-agent.sh @@ -135,14 +135,14 @@ while [[ $# -gt 0 ]]; do echo " --in-cluster Running as a Kubernetes Job inside the cluster" echo "" echo "AuthBridge plugin pipeline (see AUTHBRIDGE_PIPELINE_SPEC.md):" - echo " --plugin-preset PRESET Named bundle: auth-only | ibac-only | full" + echo " --plugin-preset PRESET Named bundle: auth-only | ibac-only | sparc-only | full" echo " --plugin NAME[:POLICY] Enable plugin; POLICY ∈ {enforce(default), observe, off}; repeatable" echo " --no-plugin NAME Shorthand for --plugin NAME:off; repeatable" echo " --plugin-config-file PATH Flat-map YAML overlay merged after selectors" echo " -h, --help Show this help message" echo "" echo "Plugin names: jwt-validation, token-exchange, token-broker, a2a-parser," - echo " mcp-parser, inference-parser, ibac" + echo " mcp-parser, inference-parser, sparc, ibac" echo "" echo "Plugin tunables (consumed when the named plugin is in the active set):" echo " IBAC_JUDGE_ENDPOINT Judge LLM base URL (default: http://host.docker.internal:11434)" @@ -556,6 +556,12 @@ ENV_VARS_WITH_CONFIG=$(echo "$ENV_VARS_WITH_CONFIG" | jq ". + [{\"name\": \"EXGE echo "Adding LITELLM_LOCAL_MODEL_COST_MAP=True" ENV_VARS_WITH_CONFIG=$(echo "$ENV_VARS_WITH_CONFIG" | jq ". + [{\"name\": \"LITELLM_LOCAL_MODEL_COST_MAP\", \"value\": \"True\"}]") +# Disable LiteLLM response caching. The exgentic default is litellm_caching=True. +# Cached runs complete in ~3.6s with no real LLM/tool calls, so inference-parser +# never captures the conversation context and SPARC/IBAC never fire. +echo "Adding EXGENTIC_LITELLM_CACHING=false" +ENV_VARS_WITH_CONFIG=$(echo "$ENV_VARS_WITH_CONFIG" | jq ". + [{\"name\": \"EXGENTIC_LITELLM_CACHING\", \"value\": \"false\"}]") + echo "✓ Environment variables prepared for deployment" echo "" @@ -590,7 +596,7 @@ import os, sys import yaml KNOWN = ["jwt-validation", "token-exchange", "token-broker", - "a2a-parser", "mcp-parser", "inference-parser", "ibac"] + "a2a-parser", "mcp-parser", "inference-parser", "sparc", "ibac"] VALID = ("enforce", "observe", "off") CHAIN = {p: ("inbound" if p in ("a2a-parser", "jwt-validation") else "outbound") for p in KNOWN} @@ -605,7 +611,7 @@ if preset: pdir = os.environ.get("PRESETS_DIR", "") pfile = os.path.join(pdir, f"{preset}.yaml") if not os.path.isfile(pfile): - die(f"unknown preset '{preset}' (looked for {pfile}); available: auth-only, ibac-only, full") + die(f"unknown preset '{preset}' (looked for {pfile}); available: auth-only, ibac-only, sparc-only, full") with open(pfile) as f: d = yaml.safe_load(f) or {} for chain in ("inbound", "outbound"): @@ -905,12 +911,27 @@ fi echo "" -# Step 11.4: Apply the AuthBridge plugin pipeline overlay (if any -# plugin selector was supplied). The operator base config enables every -# supported plugin; this overlay sets on_error: off on the ones we -# didn't select and tunes config on the ones we did. +# Step 11.4: Enable TLS bridge on AgentRuntime. We set this on the CR so the +# operator provisions the cert-manager CA secret. The actual tls_bridge config +# is injected directly into the ConfigMap in Step 11.5 to avoid race conditions +# with the operator's reconciliation loop. +if [ "$AUTHBRIDGE_ENABLED" = "true" ]; then + echo "Step 11.4: Enabling TLS bridge on AgentRuntime..." + if kubectl patch agentruntime -n "$NAMESPACE" "$AGENT_NAME" --type=merge -p '{"spec":{"tlsBridgeMode":"enabled"}}' 2>/dev/null; then + echo " ✓ tlsBridgeMode=enabled (CA will be provisioned by cert-manager)" + else + echo " Warning: Could not patch AgentRuntime tlsBridgeMode (CR may not exist yet)" + fi + echo "" +fi + +# Step 11.5: Apply the AuthBridge plugin pipeline overlay AFTER the TLS bridge +# rollout has settled. The operator has now written its base ConfigMap; this +# overlay patches the plugin selection on top without triggering another rollout. +# Also injects tls_bridge config directly into the ConfigMap — the operator does +# not reliably write this even when tlsBridgeMode=enabled is set on AgentRuntime. if [ "$AUTHBRIDGE_ENABLED" = "true" ]; then - echo "Step 11.4: Applying AuthBridge pipeline overlay..." + echo "Step 11.5: Applying AuthBridge pipeline overlay..." SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" if [ ! -x "$SCRIPT_DIR/authbridge/apply-pipeline.sh" ]; then echo "Error: $SCRIPT_DIR/authbridge/apply-pipeline.sh not found or not executable" @@ -929,6 +950,18 @@ if [ "$AUTHBRIDGE_ENABLED" = "true" ]; then TOKEN_BROKER_URL="${TOKEN_BROKER_URL:-}" \ TOKEN_BROKER_AUDIENCE="${TOKEN_BROKER_AUDIENCE:-}" \ "$SCRIPT_DIR/authbridge/apply-pipeline.sh" + + # Inject tls_bridge section directly into the ConfigMap. The authbridge + # filesystem-watch reloader picks this up without a pod restart. + echo " Injecting tls_bridge into authbridge ConfigMap..." + CM_NAME="authbridge-config-$AGENT_NAME" + CURRENT_CONFIG=$(kubectl -n "$NAMESPACE" get configmap "$CM_NAME" -o jsonpath='{.data.config\.yaml}') + NEW_CONFIG=$(printf '%s\ntls_bridge:\n ca_dir: /etc/authbridge/tls-bridge-ca\n mode: enabled\n' "$CURRENT_CONFIG") + TMP_TLS=$(mktemp) + printf '%s' "$NEW_CONFIG" >"$TMP_TLS" + kubectl -n "$NAMESPACE" create configmap "$CM_NAME" --from-file=config.yaml="$TMP_TLS" --dry-run=client -o yaml | kubectl apply -f - >/dev/null + rm -f "$TMP_TLS" + echo " ✓ tls_bridge injected into $CM_NAME" echo "" fi diff --git a/exgentic_a2a_runner/pyproject.toml b/exgentic_a2a_runner/pyproject.toml index 003520a..34ea545 100644 --- a/exgentic_a2a_runner/pyproject.toml +++ b/exgentic_a2a_runner/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "opentelemetry-exporter-otlp>=1.20.0", "opentelemetry-instrumentation-requests>=0.41b0", "opentelemetry-instrumentation-httpx>=0.41b0", - "pyyaml>=6.0", + "pyyaml>=6.0.3", ] [project.optional-dependencies] diff --git a/exgentic_a2a_runner/sparc-deny.yaml b/exgentic_a2a_runner/sparc-deny.yaml new file mode 100644 index 0000000..1364023 --- /dev/null +++ b/exgentic_a2a_runner/sparc-deny.yaml @@ -0,0 +1,3 @@ +sparc: + on_reject_action: "deny" + skip_tools: ["message"] diff --git a/exgentic_a2a_runner/sparc-observe.yaml b/exgentic_a2a_runner/sparc-observe.yaml new file mode 100644 index 0000000..25a6fb9 --- /dev/null +++ b/exgentic_a2a_runner/sparc-observe.yaml @@ -0,0 +1,2 @@ +sparc: + on_reject_action: "observe" diff --git a/exgentic_a2a_runner/uv.lock b/exgentic_a2a_runner/uv.lock index 8c1afe0..14f60fd 100644 --- a/exgentic_a2a_runner/uv.lock +++ b/exgentic_a2a_runner/uv.lock @@ -378,7 +378,7 @@ requires-dist = [ { name = "opentelemetry-sdk", specifier = ">=1.20.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.10.0" }, - { name = "pyyaml", specifier = ">=6.0" }, + { name = "pyyaml", specifier = ">=6.0.3" }, { name = "requests", specifier = ">=2.28.0" }, ] provides-extras = ["dev"]