Skip to content

Commit eb9f7a5

Browse files
committed
feat: introduce evidence level and refactor sdk/cli
1 parent 674ac98 commit eb9f7a5

52 files changed

Lines changed: 5098 additions & 543 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default defineConfig({
5050
{ text: 'Add Runtime Health', link: '/guides/add-runtime-health' },
5151
{ text: 'Add Push Mode', link: '/guides/add-push-mode' },
5252
{ text: 'CI Integration', link: '/guides/ci-integration' },
53+
{ text: 'E2E Testing', link: '/guides/e2e-testing' },
5354
{ text: 'Migrate an Existing Agent', link: '/guides/migrate-existing-agent' },
5455
{ text: 'Migrate GymCoach', link: '/guides/migrate-gymcoach' },
5556
{ text: 'Migrate OpenAGI', link: '/guides/migrate-openagi' },

docs/RUNBOOK.md

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,30 +69,34 @@ helm upgrade my-agent ./out/ -f out/values.yaml
6969
| `CONTROL_PLANE_PORT` | No | `4001` | Control plane listen port |
7070
| `ANTHROPIC_API_KEY` | No | — | Enables LLM gap analysis (`GET /agentspec/gap`) |
7171
| `AUDIT_RING_SIZE` | No | `1000` | Max audit ring entries retained in memory |
72-
| `OPA_URL` | No | — | OPA base URL (e.g. `http://localhost:8181`). When set, `/gap` calls OPA for behavioral violations AND the proxy evaluates every request. Fails-open if OPA is unreachable. |
73-
| `OPA_PROXY_MODE` | No | `track` | Per-request OPA mode on the proxy (port 4000). `track` — record violations in the audit ring and add `X-AgentSpec-OPA-Violations` header, but forward the request. `enforce` — block with `403 PolicyViolation` before forwarding. `off` — disable proxy OPA checks entirely. |
72+
| `OPA_URL` | No | — | OPA base URL (e.g. `http://localhost:8181`). When set, `/gap` calls OPA for behavioral violations AND the proxy evaluates agent response headers. Fails-open if OPA is unreachable. |
73+
| `OPA_PROXY_MODE` | No | `track` | HeaderReporting OPA mode on the proxy (port 4000). `track` — record violations in audit ring + `X-AgentSpec-OPA-Violations` header, never blocks. `enforce` — replace agent response with `403 PolicyViolation` when OPA denies. `off` — disable proxy OPA checks entirely. OPA is only called when the agent sets `X-AgentSpec-*` response headers (sdk-langgraph `AgentSpecMiddleware`). |
7474

7575
`UPSTREAM_URL` and `MANIFEST_PATH` must be set correctly. The sidecar will fail to start if `UPSTREAM_URL` is not a valid `http://` or `https://` URL, or if port values are non-integer.
7676

7777
---
7878

7979
---
8080

81-
## OPA Request Headers
81+
## OPA Behavioral Observation (HeaderReporting + EventPush)
8282

83-
When `OPA_URL` is set, the proxy reads these headers from the incoming request to populate the OPA input document. Set them from your agent code (or `GuardrailMiddleware`) to give OPA the full runtime context it needs to enforce policies accurately.
83+
OPA now evaluates **real agent behavior** — not honor-system client headers. There are two reporting paths:
8484

85-
| Header | Example | Description |
86-
|--------|---------|-------------|
87-
| `X-AgentSpec-Guardrails-Invoked` | `pii-detector,toxicity-filter` | Comma-separated list of guardrail types actually run on this request |
88-
| `X-AgentSpec-Tools-Called` | `plan-workout,log-session` | Comma-separated list of tools invoked |
89-
| `X-AgentSpec-User-Confirmed` | `true` | Set to `true` if the user explicitly confirmed a destructive action |
85+
### HeaderReporting — Agent response headers (sdk-langgraph `AgentSpecMiddleware`)
9086

91-
When these headers are absent, the proxy uses worst-case defaults (`guardrails_invoked: []`, `tools_called: []`). In `track` mode this records a violation. In `enforce` mode, any declared guardrail will cause a 403.
87+
The `agentspec-langgraph` `AgentSpecMiddleware` sets internal headers on the agent's HTTP **response** after processing:
9288

93-
The proxy sets `X-AgentSpec-OPA-Violations` on every response where violations fired (regardless of mode), so clients and upstream tooling can observe policy gaps.
89+
| Response header (agent → sidecar) | Description |
90+
|-----------------------------------|-------------|
91+
| `X-AgentSpec-Guardrails-Invoked` | Comma-separated guardrail types that actually ran |
92+
| `X-AgentSpec-Tools-Called` | Comma-separated tool names that were called |
93+
| `X-AgentSpec-User-Confirmed` | `true` if user confirmed a destructive action |
9494

95-
In `enforce` mode, the sidecar returns a structured error **before** forwarding to the upstream agent:
95+
The sidecar proxy reads these in its `onResponse` callback and **strips them before forwarding to the client**. Clients never see these headers. OPA is only called when at least one behavioral header is present.
96+
97+
The proxy sets `X-AgentSpec-OPA-Violations` on every response where violations fired (regardless of mode), so clients can observe policy gaps.
98+
99+
In `enforce` mode, when OPA denies based on agent response headers:
96100

97101
```
98102
HTTP/1.1 403 Forbidden
@@ -102,7 +106,28 @@ Content-Type: application/json
102106
{"error":"PolicyViolation","blocked":true,"violations":["pii_detector_not_invoked"],"message":"Request blocked by OPA policy: pii_detector_not_invoked"}
103107
```
104108

105-
When OPA is unreachable the proxy **fails open** (forwards the request with a warning log) regardless of mode. Set `OPA_PROXY_MODE=off` to silence OPA calls entirely while keeping `OPA_URL` set for `/gap`.
109+
> **Note:** Unlike the old implementation, `enforce` mode evaluates the agent's response headers. The upstream agent **always** processes the request. Only the client-visible response is blocked (replaced with 403).
110+
111+
### EventPush — Out-of-band event push (sdk-langgraph `SidecarClient`)
112+
113+
The agent pushes behavioral events after each request via `POST /agentspec/events` on the control plane (port 4001). EventPush always records regardless of `OPA_PROXY_MODE`.
114+
115+
```bash
116+
curl -X POST http://localhost:4001/agentspec/events \
117+
-H "Content-Type: application/json" \
118+
-d '{
119+
"requestId": "<x-request-id from proxy>",
120+
"agentName": "gymcoach",
121+
"events": [
122+
{"type":"guardrail","guardrailType":"pii-detector","invoked":true,"blocked":false},
123+
{"type":"tool","name":"plan-workout","success":true,"latencyMs":82}
124+
]
125+
}'
126+
# 200 {"requestId":"...","found":true,"opaViolations":[]}
127+
# 202 {"requestId":"...","found":false} ← race (no retry needed)
128+
```
129+
130+
When OPA is unreachable the proxy **fails open** (forwards the request) regardless of mode. Set `OPA_PROXY_MODE=off` to silence HeaderReporting OPA calls entirely while keeping `OPA_URL` set for `/gap` and EventPush.
106131

107132
---
108133

docs/concepts/compliance.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,33 @@ Output:
3030
https://owasp.org/www-project-top-10-for-large-language-model-applications/
3131
```
3232

33+
## Evidence Tiers
34+
35+
Every audit rule and gap issue carries an **evidence tier** label that tells you what kind of evidence backs the finding:
36+
37+
| Badge | Tier | Meaning |
38+
|-------|------|---------|
39+
| `[D]` | Declarative | Manifest analysis only — we read the YAML, no I/O required |
40+
| `[P]` | Probed | Health check verified at infrastructure level (`agentspec health`) |
41+
| `[B]` | Behavioral | Runtime events confirmed actual execution (sdk-langgraph + EventPush) |
42+
43+
All current audit rules are `[D]` — declarative. The grade (A–F) reflects manifest declarations only.
44+
45+
The `agentspec audit` output shows `[D]` badges next to each violation:
46+
47+
```
48+
[critical] [D] SEC-LLM-06 — Sensitive data disclosure
49+
Long-term memory declared without piiScrubFields
50+
→ Add spec.memory.hygiene.piiScrubFields: [ssn, credit_card, bank_account]
51+
52+
Evidence Breakdown
53+
[D] Declarative 18/22 (manifest declarations)
54+
[P] Probed N/A (run `agentspec health <file>` for live checks)
55+
[B] Behavioral N/A (no runtime events — deploy with sdk-langgraph + EventPush)
56+
```
57+
58+
See [Probe Coverage](./probe-coverage.md) for a complete field-by-field matrix of what each tier verifies.
59+
3360
## Compliance Packs
3461

3562
### `owasp-llm-top10`

docs/concepts/opa.md

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,61 @@ The `agentspec-langgraph` Python package provides this for LangGraph agents. It
131131

132132
See [LangGraph Runtime Instrumentation](../adapters/langgraph.md#runtime-behavioral-instrumentation) for the full integration guide.
133133

134-
## Per-request proxy enforcement
134+
## Behavioral observation pipeline
135135

136-
The sidecar proxy (port 4000) evaluates OPA on **every request** when `OPA_URL` is set. The mode is controlled by the `OPA_PROXY_MODE` env var:
136+
OPA needs to know what the agent *actually did* — which guardrails fired, which tools were called. This data comes from the `agentspec-langgraph` sub-SDK via one of two reporting paths:
137137

138-
| Mode | Behaviour |
139-
|------|-----------|
140-
| `track` (default) | Record violations in the audit ring; add `X-AgentSpec-OPA-Violations` response header; forward the request. Safe for initial rollout — never blocks traffic. |
141-
| `enforce` | Block with `403 PolicyViolation` **before forwarding to the upstream agent**. Use after validating policies in `track` mode. |
142-
| `off` | Skip proxy OPA checks entirely. `/gap` still calls OPA if `OPA_URL` is set. |
138+
### HeaderReporting — Agent response headers
139+
140+
`AgentSpecMiddleware` (FastAPI/Starlette) sets internal headers on the agent's HTTP response after each request completes:
141+
142+
```
143+
X-AgentSpec-Guardrails-Invoked: pii-detector,toxicity-filter
144+
X-AgentSpec-Tools-Called: plan-workout
145+
X-AgentSpec-User-Confirmed: true
146+
```
147+
148+
The sidecar proxy reads these in its `onResponse` callback, then **strips them before forwarding to the client**. Clients never see these headers.
149+
150+
```python
151+
from fastapi import FastAPI
152+
from agentspec_langgraph import AgentSpecMiddleware
153+
154+
app = FastAPI()
155+
app.add_middleware(AgentSpecMiddleware, guardrail_middleware=guardrail_mw)
156+
```
157+
158+
### EventPush — Out-of-band event push
159+
160+
`SidecarClient` pushes a batch of behavioral events to `POST /agentspec/events` after each request. This is fire-and-forget and swallows all errors.
161+
162+
```python
163+
from agentspec_langgraph import GuardrailMiddleware, SidecarClient
164+
165+
sidecar = SidecarClient(url="http://localhost:4001")
166+
middleware = GuardrailMiddleware(agent_name="gymcoach")
167+
168+
async with middleware.new_request_context(
169+
request_id=request.headers.get("x-request-id"),
170+
sidecar_client=sidecar,
171+
) as ctx:
172+
content = ctx.wrap("pii-detector", pii_fn)(user_input)
173+
# → On exit: events pushed to POST /agentspec/events
174+
```
175+
176+
EventPush always records behavioral data regardless of `OPA_PROXY_MODE`. HeaderReporting (response headers) triggers OPA evaluation in the proxy.
177+
178+
## Per-request proxy enforcement (HeaderReporting)
179+
180+
The sidecar proxy (port 4000) evaluates OPA on agent response headers when `OPA_URL` is set. The mode is controlled by the `OPA_PROXY_MODE` env var:
181+
182+
| Mode | Trigger | Behaviour |
183+
|------|---------|-----------|
184+
| `track` (default) | Agent response headers present | Record violations in the audit ring; add `X-AgentSpec-OPA-Violations` response header; forward the response to client. Safe for initial rollout — never blocks. |
185+
| `enforce` | Agent response headers present | If OPA denies: sidecar replaces agent response with `403 PolicyViolation`. Agent always processes the request; only the client-visible response is blocked. |
186+
| `off` || Skip proxy OPA checks entirely. `/gap` still calls OPA if `OPA_URL` is set. |
187+
188+
> **Note:** If the agent does not set `X-AgentSpec-*` response headers (e.g. not using sdk-langgraph), OPA is not called and the request passes through regardless of mode. Use EventPush (`SidecarClient`) for agents that cannot use middleware.
143189
144190
Configure globally (docker-compose or Helm):
145191

@@ -163,7 +209,7 @@ Override per-pod with annotation: `agentspec.io/opa-proxy-mode: enforce`.
163209

164210
### 403 PolicyViolation response
165211

166-
When `enforce` mode blocks a request, the sidecar returns before the upstream agent ever sees the request:
212+
When `enforce` mode blocks a request based on agent response headers, the sidecar replaces the upstream response with a 403:
167213

168214
```
169215
HTTP/1.1 403 Forbidden
@@ -180,25 +226,21 @@ Content-Type: application/json
180226
}
181227
```
182228

183-
### The honor system — and why it matters
184-
185-
The sidecar builds the OPA input from **request headers**. It does not observe what the agent actually executed. OPA knows `pii-detector` was invoked only because the caller said so via a header:
186-
187-
| Incoming request header | OPA `input` field |
188-
|-------------------------|-------------------|
189-
| `X-AgentSpec-Guardrails-Invoked: pii-detector` | `guardrails_invoked: ["pii-detector"]` |
190-
| `X-AgentSpec-Tools-Called: plan-workout` | `tools_called: ["plan-workout"]` |
191-
| `X-AgentSpec-User-Confirmed: true` | `user_confirmed: true` |
192-
193-
If a header is **absent**, the field defaults to empty. With `pii-detector` declared in `agent.yaml` and `guardrails_invoked: []`, OPA fires `pii_detector_not_invoked` immediately — because the caller did not declare that the guardrail ran.
229+
### Enforcement model
194230

195-
This is **declaration-based enforcement**, not execution-verified enforcement. A caller that sets the header without actually running the guardrail passes OPA. To close that gap, use a framework sub-SDK (`agentspec-langgraph` etc.) that sets these headers automatically from real guardrail invocations inside the agent's execution path.
231+
| Path | Mechanism | Real-time blocking |
232+
|------|-----------|-------------------|
233+
| `off` | No OPA calls | — |
234+
| `track` (HeaderReporting) | Record violations in audit ring + `X-AgentSpec-OPA-Violations` header | Never blocks |
235+
| `enforce` (HeaderReporting) | OPA evaluates agent response headers; if deny → 403 to client | ✅ Yes (client-side) |
236+
| EventPush | OPA evaluates pushed events retroactively; updates audit ring | ❌ No (observation) |
237+
| Agent-side | `GuardrailMiddleware.enforce_opa()` raises `PolicyViolationError` | ✅ Yes (in-process) |
196238

197239
## Framework sub-SDKs: the other half
198240

199-
OPA evaluates an input document on every request. That document needs live runtime data — which guardrails were invoked, how many tokens were used, which tools were called. The sidecar builds a partial input from the manifest and probe data; for full behavioral coverage you also need a **framework sub-SDK** that intercepts the agent's execution path and sets the headers automatically.
241+
OPA evaluates an input document on every request. That document needs live runtime data — which guardrails were invoked, how many tokens were used, which tools were called. The sidecar builds a partial input from the manifest and probe data; for full behavioral coverage you also need a **framework sub-SDK** that intercepts the agent's execution path.
200242

201-
The `agentspec-langgraph` Python package provides this for LangGraph agents. It intercepts tool calls, LLM calls, and guardrail invocations and sets `X-AgentSpec-*` headers on outgoing requests so that OPA receives ground truth rather than self-reported data.
243+
The `agentspec-langgraph` Python package provides this for LangGraph agents. It intercepts tool calls, LLM calls, and guardrail invocations and reports them via HeaderReporting (response headers) or EventPush (out-of-band event push).
202244

203245
See [LangGraph Runtime Instrumentation](../adapters/langgraph.md#runtime-behavioral-instrumentation) for the full integration guide.
204246

0 commit comments

Comments
 (0)