-
Notifications
You must be signed in to change notification settings - Fork 38
feat(sparc-service): one-command install; deploy SPARC via a shared i… #479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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`)") | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 | ||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||||||||||||||||||||||||||
| @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/<img> 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 - | ||||||||||||||||||||||||||
|
Comment on lines
+70
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid creating an empty Secret in When none of 🔧 Suggested fix `@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 -
+ if [ -n "$$sec" ]; then \
+ kubectl -n $(NAMESPACE) create secret generic sparc-creds $$sec --dry-run=client -o yaml | kubectl apply -f -; \
+ else \
+ kubectl -n $(NAMESPACE) delete secret sparc-creds --ignore-not-found >/dev/null; \
+ fi📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| @# --- 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 | ||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||||||||||||||||||||||||||
| 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)." | ||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add Python 3.12 type hints for script interfaces and state
authbridge/demos/finance-sparc/scripts/show-verdicts.py(underauthbridge/**/*.py) has untyped top-level state and function/interface code (BASE,get, and likelyseen/rowsat 27-28), violating the repo’s Python 3.12 typing rule. Add explicit type hints using modern union syntax (e.g.,str | None).🧰 Tools
🪛 Ruff (0.15.15)
[error] 17-17: Audit URL open for permitted schemes. Allowing use of
file:or custom schemes is often unexpected.(S310)
🤖 Prompt for AI Agents