diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 90ac0c98e..6503fc7bf 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -53,6 +53,12 @@ jobs: context: ./authbridge dockerfile: cmd/authbridge-lite/Dockerfile + # SPARC reflection service — the backend the `sparc` plugin calls. + # Deployed once per cluster via authbridge/sparc-service/deploy. + - name: sparc-service + context: ./authbridge/sparc-service + dockerfile: Dockerfile + steps: # 1. Checkout code - name: Checkout repository diff --git a/authbridge/demos/finance-sparc/Makefile b/authbridge/demos/finance-sparc/Makefile index fe645499f..2b1c0fdff 100644 --- a/authbridge/demos/finance-sparc/Makefile +++ b/authbridge/demos/finance-sparc/Makefile @@ -15,7 +15,7 @@ # export WX_API_KEY and WX_PROJECT_ID (e.g. `set -a; . /path/.env; set +a`). .PHONY: help preflight host-setup build-images build-authbridge load-images \ - ollama-up sparc-config deploy wait-pods patch-config setup-keycloak drive demo \ + ollama-up deploy wait-pods patch-config setup-keycloak drive demo \ build-abctl show-result undeploy logs-agent logs-sparc logs-ollama .DEFAULT_GOAL := help @@ -36,6 +36,8 @@ AUTHBRIDGE_IMAGE ?= ghcr.io/kagenti/kagenti-extensions/authbridge:v0.6.0-alpha. AGENT_IMAGE := finance-agent:latest MCP_IMAGE := finance-mcp:latest SPARC_IMAGE := sparc-service:latest +# Shared SPARC-service installer (the demo deploys the service through it). +SPARC_DEPLOY := ../../sparc-service/deploy # Pinned (must match k8s/ollama.yaml). Override only if you stage a different tag. OLLAMA_IMAGE := ollama/ollama:0.30.2 # In-cluster Ollama (the agent's reasoning LLM for the watsonx path). @@ -128,28 +130,15 @@ load-images: build-images build-authbridge ## Load all images into kind $(CONTAINER_RUNTIME) exec $(KIND_NODE) ctr -n k8s.io images tag localhost/$$img docker.io/library/$$img >/dev/null 2>&1 || true; \ done -sparc-config: ## Create the SPARC service ConfigMap (+ watsonx secret) for $(PROVIDER) - @if [ "$(PROVIDER)" = "watsonx" ]; then \ - kubectl -n $(PLATFORM_NS) create configmap sparc-service-config \ - --from-literal=SPARC_LLM_PROVIDER=watsonx --from-literal=SPARC_MODEL=$(SPARC_MODEL) \ - --from-literal=SPARC_TRACK=fast_track --from-literal=PORT=8090 \ - --dry-run=client -o yaml | kubectl apply -f - ; \ - kubectl -n $(PLATFORM_NS) create secret generic sparc-watsonx \ - --from-literal=WX_API_KEY="$${WX_API_KEY}" --from-literal=WX_PROJECT_ID="$${WX_PROJECT_ID}" \ - --from-literal=WX_URL="$${WX_URL:-https://us-south.ml.cloud.ibm.com}" \ - --dry-run=client -o yaml | kubectl apply -f - ; \ - else \ - kubectl -n $(PLATFORM_NS) create configmap sparc-service-config \ - --from-literal=SPARC_LLM_PROVIDER=ollama --from-literal=SPARC_MODEL=$(SPARC_MODEL) \ - --from-literal=SPARC_TRACK=fast_track --from-literal=PORT=8090 \ - --from-literal=OLLAMA_BASE_URL=$(SPARC_OLLAMA_URL) \ - --dry-run=client -o yaml | kubectl apply -f - ; \ - kubectl -n $(PLATFORM_NS) create secret generic sparc-watsonx --dry-run=client -o yaml | kubectl apply -f - ; \ - fi - @kubectl -n $(PLATFORM_NS) rollout restart deploy/sparc-service >/dev/null 2>&1 || true - -deploy: sparc-config ## Deploy the SPARC service, finance MCP server, and agent - kubectl apply -f k8s/sparc-service.yaml +deploy: ## Deploy the SPARC service (shared installer), finance MCP server, and agent + @# The SPARC service is deployed through the shared installer — the same + @# one-command path a user runs for any cluster. The demo's images are + @# already built + kind-loaded by load-images, so we pass IMAGE and skip the + @# installer's own build. WX_* creds (watsonx) are read from the environment. + $(MAKE) -C $(SPARC_DEPLOY) install \ + NAMESPACE=$(PLATFORM_NS) IMAGE=$(SPARC_IMAGE) PROVIDER=$(PROVIDER) \ + MODEL=$(SPARC_MODEL) TRACK=fast_track OLLAMA_BASE_URL=$(SPARC_OLLAMA_URL) \ + KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) kubectl apply -f k8s/finance-mcp.yaml kubectl apply -f k8s/agent.yaml @# Point the agent's reasoning LLM at the provider's Ollama (in-cluster for @@ -195,9 +184,7 @@ undeploy: ## Remove demo resources (kagenti install untouched) -kubectl -n $(NS) delete -f k8s/agent.yaml --ignore-not-found -kubectl -n $(NS) delete -f k8s/finance-mcp.yaml --ignore-not-found -kubectl -n $(NS) delete -f k8s/ollama.yaml --ignore-not-found - -kubectl delete -f k8s/sparc-service.yaml --ignore-not-found - -kubectl -n $(PLATFORM_NS) delete configmap sparc-service-config --ignore-not-found - -kubectl -n $(PLATFORM_NS) delete secret sparc-watsonx --ignore-not-found + -$(MAKE) -C $(SPARC_DEPLOY) uninstall NAMESPACE=$(PLATFORM_NS) -kubectl -n $(NS) delete configmap authbridge-config-$(AGENT) --ignore-not-found -kubectl -n $(NS) delete secret -l kagenti.io/client-name=$(AGENT) --ignore-not-found @echo "[*] demo resources removed." diff --git a/authbridge/demos/finance-sparc/README.md b/authbridge/demos/finance-sparc/README.md index c2e20a558..5c6fe1f7c 100644 --- a/authbridge/demos/finance-sparc/README.md +++ b/authbridge/demos/finance-sparc/README.md @@ -72,9 +72,15 @@ make demo PROVIDER=ollama `make demo` is one-shot and idempotent. It: checks cluster egress (a no-op on healthy networks), builds + loads images (including an authbridge image carrying the `sparc` plugin), -stages + warms the in-cluster Ollama model(s), deploys the SPARC service + finance MCP server + -agent, enables the pipeline, configures Keycloak, and drives the two-turn scenario **through -real inbound auth** (no jwt bypass), printing SPARC's verdicts. +stages + warms the in-cluster Ollama model(s), deploys the SPARC service via the shared +[installer](../../sparc-service/deploy/README.md), deploys the finance MCP server + agent, +enables the pipeline, configures Keycloak, and drives the two-turn scenario **through real +inbound auth** (no jwt bypass), printing SPARC's verdicts. + +> The SPARC service is the one piece you also need outside this demo. The demo deploys it for +> you through the same one-command installer a user would run for any agent — +> [`sparc-service/deploy`](../../sparc-service/deploy/README.md) — so there's a single, reusable +> path. It must be deployed before the `sparc` plugin is enabled. ## How to read the output @@ -115,7 +121,7 @@ real inbound auth** (no jwt bypass), printing SPARC's verdicts. | `finance-agent/` | A regular A2A agent: Ollama reasoning, MCP `tools/list` discovery, MCP tool execution. | | `finance-mcp/` | MCP server: `get_transaction`, `lookup_customer`, `issue_refund`, `get_invoice`, `list_currencies` over a small multi-record dataset (real id: `TX4827`). | | `k8s/sparc-patch.yaml` | Pipeline additions: `a2a-parser` inbound; `inference-parser` + `mcp-parser` + `sparc` outbound (mcp mode, fast_track, reflect, `skip_tools: [list_currencies]`). | -| `k8s/{agent,finance-mcp,sparc-service}.yaml` | Workload manifests. | +| `k8s/{agent,finance-mcp}.yaml` | Demo workload manifests. (The SPARC service itself is deployed via the shared installer at [`sparc-service/deploy/`](../../sparc-service/deploy/README.md).) | | `k8s/ollama.yaml` | In-cluster Ollama for the agent's reasoning model on the **watsonx** path; model staged onto the node — no host networking. (The `PROVIDER=ollama` path uses your host Ollama instead.) | | `scripts/host-setup.sh` | Generic egress check (no-op on healthy networks; portable MSS clamp only if the link blackholes PMTUD). | | `scripts/stage-ollama-model.sh` | Copies a model from the host Ollama store onto the kind node (local, no pull). | diff --git a/authbridge/demos/finance-sparc/scripts/drive-demo.sh b/authbridge/demos/finance-sparc/scripts/drive-demo.sh index 7ae243d8b..10427a3ae 100644 --- a/authbridge/demos/finance-sparc/scripts/drive-demo.sh +++ b/authbridge/demos/finance-sparc/scripts/drive-demo.sh @@ -60,11 +60,10 @@ send 2 'The transaction id is TX4827. Please proceed with the refund, reason: du echo " agent> $(reply /tmp/finance-t2.json)" say "SPARC verdicts recorded for this session:" -SID="$CTX" kubectl -n "$NS" port-forward "deploy/$AGENT" 19094:9094 >/tmp/finance-pf94.log 2>&1 & PF94=$!; trap 'kill $PF $PF94 $KCPF 2>/dev/null || true' EXIT sleep 3 SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) -curl -s -m 10 "http://localhost:19094/v1/sessions/$SID" 2>/dev/null | python3 "$SCRIPT_DIR/show-verdicts.py" +python3 "$SCRIPT_DIR/show-verdicts.py" "http://localhost:19094" echo echo "Done. Inspect the full pipeline with: make show-result" diff --git a/authbridge/demos/finance-sparc/scripts/show-verdicts.py b/authbridge/demos/finance-sparc/scripts/show-verdicts.py index d089551db..315b1b082 100644 --- a/authbridge/demos/finance-sparc/scripts/show-verdicts.py +++ b/authbridge/demos/finance-sparc/scripts/show-verdicts.py @@ -1,26 +1,53 @@ #!/usr/bin/env python3 -"""Print the SPARC plugin verdicts from a session snapshot (read on stdin).""" +"""Print the SPARC plugin verdicts recorded by an agent's authbridge session API. + +Scans every session (an agent's outbound calls can land in the conversation +session or the `default` bucket depending on timing), de-duplicates, and prints +each SPARC verdict. Pass the session-API base URL as the first argument +(default http://localhost:19094). +""" import json import sys +import urllib.request + +BASE = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:19094" + + +def get(path): + with urllib.request.urlopen(BASE + path, timeout=10) as resp: + return json.load(resp) + try: - data = json.load(sys.stdin) + sessions = get("/v1/sessions").get("sessions", []) except Exception: - print(" (no session events yet)") + print(" (could not reach the session API)") sys.exit(0) -seen = False -for event in data.get("events", []): - for inv in (event.get("invocations") or {}).get("outbound", []): - if inv.get("plugin") == "sparc" and inv.get("action") in ("allow", "modify", "deny", "observe"): +seen = set() +rows = [] +for s in sessions: + try: + snapshot = get("/v1/sessions/" + s["id"]) + except Exception: + continue + for event in snapshot.get("events", []): + for inv in (event.get("invocations") or {}).get("outbound", []): + if inv.get("plugin") != "sparc": + continue + if inv.get("action") not in ("allow", "modify", "deny", "observe"): + continue det = inv.get("details", {}) - seen = True score = det.get("score") + key = (inv.get("action"), inv.get("reason"), det.get("tool"), score) + if key in seen: + continue + seen.add(key) score_s = f" score={score}" if score not in (None, "") else "" - print( + rows.append( " SPARC {}/{} tool={}{}".format( inv.get("action"), inv.get("reason"), det.get("tool"), score_s ) ) -if not seen: - print(" (no sparc verdicts found in session; check `make logs-sparc`)") + +print("\n".join(rows) if rows else " (no sparc verdicts found in any session; check `make logs-sparc`)") diff --git a/authbridge/docs/sparc-plugin.md b/authbridge/docs/sparc-plugin.md index 8c4909089..af2badb1d 100644 --- a/authbridge/docs/sparc-plugin.md +++ b/authbridge/docs/sparc-plugin.md @@ -67,6 +67,22 @@ grounded) is `<=` it, any reject is escalated to `deny`. > governs the case where the conversation/tool context is missing (no `inference-parser` event): > `open` skips and records `no_inference_context`; `closed` blocks. +## Prerequisite: deploy the SPARC service + +The plugin is a thin client — it calls the [SPARC reflection service](../sparc-service/README.md) +over HTTP. **Deploy that service once per cluster before enabling this plugin**, or every tool call +hits an unreachable reflector (and falls back to `fail_policy`). It's one command: + +```bash +cd authbridge/sparc-service/deploy +export WX_API_KEY=... WX_PROJECT_ID=... # or PROVIDER=ollama, openai, ... +make install # deploys into kagenti-system by default +``` + +See [`sparc-service/deploy/README.md`](../sparc-service/deploy/README.md) for providers, the +local-image path (`make image install` on kind), and configuration. Point `reflector_endpoint` +below at the resulting service (`http://sparc-service..svc:8090`). + ## Configuration ```yaml diff --git a/authbridge/sparc-service/README.md b/authbridge/sparc-service/README.md index 93b98c2e3..547df8178 100644 --- a/authbridge/sparc-service/README.md +++ b/authbridge/sparc-service/README.md @@ -18,6 +18,21 @@ This service is deliberately *just* a reflection wrapper: - It returns SPARC's verdict **faithfully**. All enforcement policy (observe / inject / deny, score thresholds) lives in the AuthBridge plugin, not here. +## Deploy to a cluster (one command) + +Deploy this service **once per cluster, before enabling the `sparc` plugin** on any agent: + +```bash +cd deploy +export WX_API_KEY=... WX_PROJECT_ID=... # or PROVIDER=ollama, openai, ... +make install # → kagenti-system; published image +# kind dev cluster (build + load a local image first): +make image install +``` + +See [`deploy/README.md`](deploy/README.md) for all providers, namespaces, and the local-build path. +The rest of this document covers the service's HTTP API and environment configuration. + ## API ### `POST /reflect` diff --git a/authbridge/sparc-service/deploy/Makefile b/authbridge/sparc-service/deploy/Makefile new file mode 100644 index 000000000..486a75b8a --- /dev/null +++ b/authbridge/sparc-service/deploy/Makefile @@ -0,0 +1,90 @@ +# Install the SPARC reflection service into a cluster — one command. +# +# Deploy this ONCE per cluster BEFORE enabling the `sparc` plugin on any agent. +# +# make install # watsonx (needs WX_API_KEY / WX_PROJECT_ID) +# make install PROVIDER=ollama OLLAMA_BASE_URL=http://host.docker.internal:11434 +# make install PROVIDER=openai # needs OPENAI_API_KEY +# make image install # build the image locally + load into kind first +# make uninstall +# +# By default it deploys the published image. On a kind dev cluster (or to run a +# local build) prepend the `image` target to build + load sparc-service:latest. + +.PHONY: help install uninstall image status + +.DEFAULT_GOAL := help + +PROVIDER ?= watsonx +NAMESPACE ?= kagenti-system +IMAGE ?= ghcr.io/kagenti/kagenti-extensions/sparc-service:latest +TRACK ?= fast_track +LLM_TIMEOUT ?= 120 +PORT ?= 8090 +# Optional (provider-dependent). MODEL defaults per-provider in the service. +MODEL ?= +OLLAMA_BASE_URL ?= http://host.docker.internal:11434 +LLM_KWARGS_JSON ?= +LLM_REGISTRY_ID ?= +# Local build / kind (only used by `make image`). +KIND_CLUSTER_NAME ?= kagenti +KIND_NODE := $(KIND_CLUSTER_NAME)-control-plane +CONTAINER_RUNTIME ?= $(shell docker info >/dev/null 2>&1 && echo docker || (podman info >/dev/null 2>&1 && echo podman) || echo docker) + +# Provider credential env vars copied into the sparc-creds Secret when present. +# LiteLLM (openai/azure/litellm) reads its provider key from the environment, so +# injecting these as env via the Secret is enough. +CRED_VARS := WX_API_KEY WX_PROJECT_ID WX_URL OPENAI_API_KEY OPENAI_BASE_URL \ + AZURE_API_KEY ANTHROPIC_API_KEY GEMINI_API_KEY OLLAMA_API_KEY + +help: ## Show this menu + @printf "\nInstall the SPARC reflection service (deploy before enabling the sparc plugin).\n\n" + @printf "Run:\n \033[1mmake install\033[0m (watsonx; needs WX_* env)\n" + @printf " \033[1mmake install PROVIDER=ollama\033[0m (local ollama)\n" + @printf " \033[1mmake image install\033[0m (build + kind-load, then install)\n\n" + @printf "Targets:\n" + @awk 'BEGIN{FS=":.*## "} /^[a-zA-Z_-]+:.*## /{printf " \033[36m%-12s\033[0m %s\n",$$1,$$2}' $(MAKEFILE_LIST) + @printf "\nVars: PROVIDER(=%s) NAMESPACE(=%s) IMAGE MODEL TRACK OLLAMA_BASE_URL\n" "$(PROVIDER)" "$(NAMESPACE)" + @printf " LLM_KWARGS_JSON LLM_REGISTRY_ID KIND_CLUSTER_NAME\n\n" + +image: ## Build the sparc-service image locally and load it into kind + @echo "[*] building $(IMAGE)" + $(CONTAINER_RUNTIME) build -t $(IMAGE) .. + @echo "[*] kind load $(IMAGE) into $(KIND_CLUSTER_NAME)" + kind load docker-image $(IMAGE) --name $(KIND_CLUSTER_NAME) + @# Let containerd resolve the bare docker.io/library/ ref kubelet uses. + -$(CONTAINER_RUNTIME) exec $(KIND_NODE) ctr -n k8s.io images tag localhost/$(IMAGE) docker.io/library/$(IMAGE) >/dev/null 2>&1 || true + +install: ## Create config + creds and deploy the SPARC service into $(NAMESPACE) + @kubectl get ns $(NAMESPACE) >/dev/null 2>&1 || kubectl create ns $(NAMESPACE) + @echo "[*] config: provider=$(PROVIDER) namespace=$(NAMESPACE) track=$(TRACK)" + @# --- ConfigMap (non-secret settings) --- + @cfg="--from-literal=SPARC_LLM_PROVIDER=$(PROVIDER) --from-literal=SPARC_TRACK=$(TRACK)"; \ + cfg="$$cfg --from-literal=SPARC_LLM_TIMEOUT=$(LLM_TIMEOUT) --from-literal=PORT=$(PORT)"; \ + [ -n "$(MODEL)" ] && cfg="$$cfg --from-literal=SPARC_MODEL=$(MODEL)"; \ + [ "$(PROVIDER)" = "ollama" ] && cfg="$$cfg --from-literal=OLLAMA_BASE_URL=$(OLLAMA_BASE_URL)"; \ + [ -n "$(LLM_KWARGS_JSON)" ] && cfg="$$cfg --from-literal=SPARC_LLM_KWARGS_JSON=$(LLM_KWARGS_JSON)"; \ + [ -n "$(LLM_REGISTRY_ID)" ] && cfg="$$cfg --from-literal=SPARC_LLM_REGISTRY_ID=$(LLM_REGISTRY_ID)"; \ + kubectl -n $(NAMESPACE) create configmap sparc-service-config $$cfg --dry-run=client -o yaml | kubectl apply -f - + @# --- Secret (provider credentials present in the environment) --- + @sec=""; for v in $(CRED_VARS); do \ + val=$$(printenv $$v || true); [ -n "$$val" ] && sec="$$sec --from-literal=$$v=$$val"; \ + done; \ + kubectl -n $(NAMESPACE) create secret generic sparc-creds $$sec --dry-run=client -o yaml | kubectl apply -f - + @# --- Workload --- + kubectl -n $(NAMESPACE) apply -f sparc-service.yaml + kubectl -n $(NAMESPACE) set image deploy/sparc-service sparc-service=$(IMAGE) >/dev/null + @kubectl -n $(NAMESPACE) rollout restart deploy/sparc-service >/dev/null 2>&1 || true + kubectl -n $(NAMESPACE) rollout status deploy/sparc-service --timeout=180s + @echo "[*] SPARC service ready at http://sparc-service.$(NAMESPACE).svc:$(PORT)/reflect" + @echo " Now enable the 'sparc' plugin in an agent's authbridge config (see docs/sparc-plugin.md)." + +status: ## Show the SPARC service deployment + config + kubectl -n $(NAMESPACE) get deploy,svc,cm/sparc-service-config -l app.kubernetes.io/name=sparc-service 2>/dev/null; \ + kubectl -n $(NAMESPACE) get cm sparc-service-config -o yaml 2>/dev/null | sed -n '/^data:/,/^[a-z]/p' + +uninstall: ## Remove the SPARC service, its config, and creds from $(NAMESPACE) + -kubectl -n $(NAMESPACE) delete -f sparc-service.yaml --ignore-not-found + -kubectl -n $(NAMESPACE) delete configmap sparc-service-config --ignore-not-found + -kubectl -n $(NAMESPACE) delete secret sparc-creds --ignore-not-found + @echo "[*] SPARC service removed from $(NAMESPACE)." diff --git a/authbridge/sparc-service/deploy/README.md b/authbridge/sparc-service/deploy/README.md new file mode 100644 index 000000000..b0bfc0af0 --- /dev/null +++ b/authbridge/sparc-service/deploy/README.md @@ -0,0 +1,73 @@ +# Install the SPARC reflection service + +The AuthBridge [`sparc` plugin](../../docs/sparc-plugin.md) calls this service to decide whether an +agent's proposed tool call is grounded in the conversation. **Deploy it once per cluster — before +you enable the `sparc` plugin on any agent.** Without it, the plugin has nothing to call. + +It's one command: + +```bash +cd authbridge/sparc-service/deploy + +# watsonx (default) — provide credentials in the environment: +export WX_API_KEY=... WX_PROJECT_ID=... +make install + +# or local Ollama (no credentials): +make install PROVIDER=ollama OLLAMA_BASE_URL=http://host.docker.internal:11434 + +# or OpenAI / Azure / anything LiteLLM supports: +make install PROVIDER=openai # needs OPENAI_API_KEY +make install PROVIDER=litellm MODEL=anthropic/claude-3-5-sonnet ANTHROPIC_API_KEY=... +``` + +`make install` creates the `sparc-service-config` ConfigMap and a `sparc-creds` Secret (from +whatever provider credentials are in your environment), deploys the service into `NAMESPACE` +(default `kagenti-system`), and waits for it to become ready. Re-running it is idempotent. + +When it's up, enable the plugin on an agent — see +[`docs/sparc-plugin.md`](../../docs/sparc-plugin.md#prerequisite-deploy-the-sparc-service). + +## Image + +The default image is `ghcr.io/kagenti/kagenti-extensions/sparc-service:latest` (published by CI). +On a **kind** dev cluster, build and load a local image first: + +```bash +make image install # builds sparc-service:latest, loads it into kind, then installs +``` + +Override with `IMAGE=/sparc-service:`. + +## Configuration + +All knobs are `make` variables (mirrors of the service's [environment settings](../README.md)): + +| Variable | Default | Notes | +|---|---|---| +| `PROVIDER` | `watsonx` | `watsonx` \| `ollama` \| `openai` \| `azure` \| `litellm` | +| `NAMESPACE` | `kagenti-system` | where the service is deployed | +| `IMAGE` | ghcr `sparc-service:latest` | override for a local build or private registry | +| `MODEL` | per-provider | required for `azure`/`litellm` (e.g. `azure/`) | +| `TRACK` | `fast_track` | SPARC reflection track | +| `OLLAMA_BASE_URL` | `http://host.docker.internal:11434` | used when `PROVIDER=ollama` | +| `LLM_KWARGS_JSON` | — | extra LiteLLM client kwargs for `openai`/`azure`/`litellm` | +| `LLM_REGISTRY_ID` | — | advanced: override the ALTK client registry id | +| `LLM_TIMEOUT` | `120` | per-call timeout (seconds) | + +Credentials are read from the environment at install time (`WX_API_KEY`, `WX_PROJECT_ID`, `WX_URL`, +`OPENAI_API_KEY`, `AZURE_API_KEY`, `ANTHROPIC_API_KEY`, …) and stored in the `sparc-creds` Secret. + +## Security / network posture + +The service is an **in-cluster backend** — no ingress, never public; same trust model as the +authbridge session API. `/reflect` is unauthenticated by default, so restrict callers with a +`NetworkPolicy` (allow only the authbridge sidecars). It runs non-root and makes its own egress to +the LLM provider. See [`docs/sparc-plugin.md`](../../docs/sparc-plugin.md#security--network-posture). + +## Other targets + +```bash +make status # show the deployment + active config +make uninstall # remove the service, ConfigMap, and Secret +``` diff --git a/authbridge/demos/finance-sparc/k8s/sparc-service.yaml b/authbridge/sparc-service/deploy/sparc-service.yaml similarity index 62% rename from authbridge/demos/finance-sparc/k8s/sparc-service.yaml rename to authbridge/sparc-service/deploy/sparc-service.yaml index a99bb6091..3f0d53858 100644 --- a/authbridge/demos/finance-sparc/k8s/sparc-service.yaml +++ b/authbridge/sparc-service/deploy/sparc-service.yaml @@ -1,16 +1,22 @@ -# SPARC reflection service for the demo, deployed into kagenti-system. -# Its config (provider/model/track) is supplied by the `sparc-service-config` -# ConfigMap and the optional `sparc-watsonx` Secret, both created by the -# Makefile (provider-aware: watsonx or ollama). In production this ships via the -# kagenti Helm chart (components.sparcService.enabled=true). +# SPARC reflection service — the backend the AuthBridge `sparc` plugin calls. +# +# Deploy this ONCE per cluster (any namespace; kagenti-system by convention) +# BEFORE enabling the `sparc` plugin on an agent. Agents reach it at +# http://sparc-service..svc:8090/reflect. +# +# Config comes from two objects the installer (Makefile) creates: +# - sparc-service-config ConfigMap — provider / model / track / etc. +# - sparc-creds Secret — provider credentials (optional) +# +# No namespace is baked in — apply with `kubectl -n apply -f`. apiVersion: apps/v1 kind: Deployment metadata: name: sparc-service - namespace: kagenti-system labels: app.kubernetes.io/name: sparc-service - app.kubernetes.io/part-of: finance-sparc-demo + app.kubernetes.io/component: reflection-service + app.kubernetes.io/part-of: authbridge spec: replicas: 1 selector: @@ -20,7 +26,8 @@ spec: metadata: labels: app.kubernetes.io/name: sparc-service - app.kubernetes.io/part-of: finance-sparc-demo + # The service makes its own egress to the LLM provider; keep it out of + # agent-side sidecar injection / ambient capture. kagenti.io/inject: disabled spec: securityContext: @@ -30,7 +37,7 @@ spec: type: RuntimeDefault containers: - name: sparc-service - image: sparc-service:latest + image: ghcr.io/kagenti/kagenti-extensions/sparc-service:latest imagePullPolicy: IfNotPresent ports: - containerPort: 8090 @@ -39,7 +46,7 @@ spec: - configMapRef: name: sparc-service-config - secretRef: - name: sparc-watsonx + name: sparc-creds optional: true securityContext: allowPrivilegeEscalation: false @@ -69,10 +76,10 @@ apiVersion: v1 kind: Service metadata: name: sparc-service - namespace: kagenti-system labels: app.kubernetes.io/name: sparc-service - app.kubernetes.io/part-of: finance-sparc-demo + app.kubernetes.io/component: reflection-service + app.kubernetes.io/part-of: authbridge spec: selector: app.kubernetes.io/name: sparc-service