The sparc plugin verifies that an agent's proposed tool call is grounded in the
conversation and the available tool specifications before it executes — catching
hallucinated / ungrounded arguments (e.g. an invented transaction id) and inappropriate
tool selection. On a reflected reject it returns SPARC's clarification to the agent so the
agent re-asks the user; the bad call never runs.
SPARC is the SPARCReflectionComponent from the
agent-lifecycle-toolkit (ALTK) Python
package, served over HTTP by the companion SPARC reflection service.
Because AuthBridge plugins are Go, this plugin calls the service over HTTP (the same shape
ibac uses for its judge). All enforcement policy lives in the plugin; the service only
returns SPARC's verdict.
It is complementary to ibac: SPARC verifies argument grounding;
IBAC verifies intent alignment (prompt-injection / exfiltration).
SPARC's three inputs are collected from exactly what the agent produces, with no per-agent wiring or out-of-band data:
- conversation history, including the system prompt — from the agent's LLM call captured
by
inference-parser(pctx.Extensions.Inference.Messages, every role preserved); - tool specifications in OpenAI function-calling format — from the same call
(
pctx.Extensions.Inference.Tools); - the proposed tool call — from the outbound MCP
tools/call(mcpmode) or from the LLM response (inferencemode).
If the conversation/tool context isn't available (no inference-parser, or session tracking
off), the plugin skips (no_inference_context) rather than reflect on partial data.
The verdict is returned to the agent in the shape it expects, selected by enforcement:
mcp(default — the rossoctl norm). Gate the outbound MCPtools/call. The tool call comes from the MCP request; conversation + tool specs are correlated from the session's most recent inference request (robust to LLM streaming). On a reflected reject, return SPARC's clarification as a JSON-RPC MCP tool result (Reject+Violation{Status:200, Body}); the agent's MCP client consumes it like any tool output.inference(for non-MCP agents). Gate the agent's LLM response, where all three inputs are co-located. On a reflected reject, rewrite the completion (viaSetResponseBody) so the assistant turn carries the clarification and the tool_call is dropped — norms-correct for any OpenAI-style agent.
When SPARC returns reject, on_reject_action decides, with optional score escalation:
on_reject_action |
Behavior | Session record |
|---|---|---|
observe |
Log only, let the call through (shadow). | observe / reject_observed |
reflect (default) |
Return SPARC's clarification to the agent (MCP result or rewritten completion). | modify / reflected |
deny |
Hard block (403 in mcp mode; refusal completion in inference mode). |
deny / blocked |
When deny_score_threshold > 0 and SPARC's overall_avg_score (normalized 0..1, higher = better
grounded) is <= it, any reject is escalated to deny.
fail_policydefaults toopen— when SPARC is unreachable, times out, or returnsdecision=error, the proposed tool call is allowed through (and recorded). This is deliberate and diverges fromibac, which fails closed: SPARC is a grounding quality gate, not an auth control, so reflector downtime shouldn't take the agent's tools offline. Setfail_policy: closedwhen you want tool execution gated on reflector availability (e.g. a high-assurance deployment where an unverified call is worse than a failed one). The same policy governs the case where the conversation/tool context is missing (noinference-parserevent):openskips and recordsno_inference_context;closedblocks.
The plugin is a thin client — it calls the SPARC reflection service
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:
cd authbridge/sparc-service/deploy
export WX_API_KEY=... WX_PROJECT_ID=... # or PROVIDER=ollama, openai, ...
make install # deploys into rossoctl-system by defaultSee 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.<namespace>.svc:8090).
pipeline:
outbound:
plugins:
- name: sparc
config:
reflector_endpoint: "http://sparc-service.rossoctl-system.svc:8090"
enforcement: "mcp" # mcp | inference
track: "fast_track" # fast_track|slow_track|syntax|spec_free|transformations_only
on_reject_action: "reflect" # observe | reflect | deny
deny_score_threshold: 0 # 0 disables; e.g. 2.0 → deny rejects scoring <= 2
fail_policy: "open" # open=allow+log on SPARC error; closed=block
timeout_ms: 30000
skip_tools: ["list_*"] # tool-name globs NOT reflected on (e.g. read-only tools)
reflect_tools: [] # if non-empty, ONLY these globs are reflected| Field | Required | Default | Description |
|---|---|---|---|
reflector_endpoint |
Yes | — | Base URL of the SPARC service; the plugin POSTs to {endpoint}/reflect. |
reflector_bearer |
No | "" |
Bearer for the service. Empty for in-cluster unauthenticated calls. |
enforcement |
No | mcp |
mcp (gate MCP tools/call, return MCP result) or inference (gate LLM response, rewrite completion). |
track |
No | fast_track |
SPARC track. fast_track = hallucination + function-selection checks. |
on_reject_action |
No | reflect |
observe | reflect | deny (see table above). |
deny_score_threshold |
No | 0 |
Escalate a reject to deny when score <= this ([0,1]; 0 disables). |
fail_policy |
No | open |
On SPARC unreachable / decision=error: open allows+logs; closed blocks. |
timeout_ms |
No | 30000 |
Per-call reflection timeout. Rejected below 100. Reflection is out-of-band (the plugin calls the SPARC service directly), so this is independent of the forward proxy's upstream timeout — raise it for slow/CPU-bound providers (e.g. an in-cluster Ollama). |
skip_tools |
No | — | Tool-name globs (path.Match) to NOT reflect on (e.g. trivial read tools). |
reflect_tools |
No | — | If non-empty, ONLY reflect tools matching these globs; all others skipped. |
bypass_hosts / bypass_paths |
No | built-in | Host / path globs skipped without reflecting. |
sparc declares RequiresAny: ["inference-parser", "mcp-parser"]. For the richest, generic
reflection wire all three parsers:
pipeline:
inbound:
plugins:
- name: a2a-parser # seeds the session with the user turn
- name: jwt-validation
outbound:
plugins:
- name: token-exchange
- name: inference-parser # captures messages (incl. system) + tool specs
- name: mcp-parser # parses the tool call (mcp mode)
- name: sparcsparc records a flat Invocation per call (action allow/modify/deny/observe,
reason, tool, score) and publishes the full structured verdict via the plugin-event
escape-hatch (Extensions.Custom["sparc/event"] → SessionEvent.Plugins["sparc"]), so abctl
and other consumers can render the decision, score, track, enforcement mode, and per-issue
explanations/corrections.
| Reason | HTTP / effect | Meaning |
|---|---|---|
sparc.reflected |
200 MCP result / rewritten completion | A reject was reflected back to the agent as a clarification. |
sparc.blocked |
403 (mcp) / refusal completion (inference) | A reject handled by deny (or escalated via score). |
sparc.reflector_unavailable |
503 / refusal (closed); pass-through (open) | SPARC unreachable or decision=error, per fail_policy. |
sparc.no_inference_context |
503 (closed); pass-through (open) | No conversation/tool context to ground against, per fail_policy. |
The reflection service is an in-cluster backend — same trust model as the authbridge session
API (:9094): no ingress, never exposed publicly. Its /reflect endpoint is unauthenticated by
default and runs outside the agent-side ambient mesh (rossoctl.io/inject: disabled), so it makes
its own egress to the LLM provider. Treat the cluster network boundary as the control: restrict
who can reach it with a NetworkPolicy (allow only the authbridge sidecars), since any pod that
can POST to /reflect can trigger LLM calls billed to the configured credentials. For an extra
hop, set reflector_bearer on the plugin and terminate it at the service (or a sidecar). The
service never echoes provider exception text back to callers; details stay in its logs.
- Not an intent/auth control — use
ibac+token-exchangeroutes / NetworkPolicy for that. - No principal/identity input — SPARC reflects on
(messages, tool_specs, tool_calls)only. inferencemode can't see streamed tool_calls (usemcpmode for streaming MCP agents).- Cost: one reflection round-trip per (non-skipped) tool call.
sparc-service/README.md— the reflection service.demos/finance-sparc/— the end-to-end demo.ibac-plugin.md— the complementary intent control.
authlib/plugins/sparc/plugin.go— config, gates, policy, structured event.authlib/plugins/sparc/collect.go— generic input collection.authlib/plugins/sparc/respond.go— MCP result + completion-rewrite responders.authlib/plugins/sparc/reflector.go— HTTP client to the service.