From 179526d9c0b3cd10eaef48ab6f511ed9027a56a6 Mon Sep 17 00:00:00 2001 From: Osher-Elhadad Date: Thu, 4 Jun 2026 14:10:48 +0300 Subject: [PATCH] feat(sparc-service): one-command install; deploy SPARC via a shared installer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SPARC service deploy artifacts don't belong in the kagenti Helm chart, so move them here next to the rest of SPARC. Add authbridge/sparc-service/deploy: a single `make install` that configures the provider (watsonx by default, or ollama/openai/azure/litellm) and deploys the service into a cluster — run once, before enabling the `sparc` plugin on any agent. Publish the sparc-service image from CI so the default install needs no local build (a local `make image` path remains for kind). Point the finance-sparc demo at the same installer instead of carrying its own copy of the manifest, so there is one source of truth, and make the demo's verdict summary scan every session so it reports reliably regardless of which session the agent's outbound calls land in. Signed-off-by: Osher-Elhadad --- .github/workflows/build.yaml | 6 ++ authbridge/demos/finance-sparc/Makefile | 39 +++----- authbridge/demos/finance-sparc/README.md | 14 ++- .../demos/finance-sparc/scripts/drive-demo.sh | 3 +- .../finance-sparc/scripts/show-verdicts.py | 49 +++++++--- authbridge/docs/sparc-plugin.md | 16 ++++ authbridge/sparc-service/README.md | 15 ++++ authbridge/sparc-service/deploy/Makefile | 90 +++++++++++++++++++ authbridge/sparc-service/deploy/README.md | 73 +++++++++++++++ .../deploy}/sparc-service.yaml | 31 ++++--- 10 files changed, 281 insertions(+), 55 deletions(-) create mode 100644 authbridge/sparc-service/deploy/Makefile create mode 100644 authbridge/sparc-service/deploy/README.md rename authbridge/{demos/finance-sparc/k8s => sparc-service/deploy}/sparc-service.yaml (62%) 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