Add adapters/ with Claude Code, Cursor, and NAT reference implementations - #22
Add adapters/ with Claude Code, Cursor, and NAT reference implementations#22bar-capsule wants to merge 2 commits into
Conversation
98470b4 to
8e65380
Compare
rocklambros
left a comment
There was a problem hiding this comment.
@bar-capsule I went deep on this branch before writing anything. Checked it out, ran the three suites, read each adapter against the v0.1.0 schemas. The config-only pattern is the right call, and the per-adapter docs are honest about what's deferred. I want to flag a gap before "reference implementation" sticks, because people copy reference adapters line for line, and a few of these would ride along into their deployments. Marking this request-changes so the wire-format and fail-open items get a look before merge, not as a veto on the direction.
I checked all of this against the open issues first (#10 through #19). Those are spec-level: capability resolution, HMAC key management, the conformance program. Nothing below is a dupe. This is adapter-implementation ground. Three of mine have a protocol-level twin and I'll point those out.
The one that matters most: the adapters don't emit the v0.1.0 wire format. In claude-code/acs_adapter.py the request puts acs_version, request_id, timestamp, and metadata at the top level, but request-envelope.json wants them inside params and sets additionalProperties: false. A schema-validating Guardian rejects every request. timestamp goes out as epoch millis (int(time.time() * 1000)) where the schema asks for an ISO-8601 string. The payload uses tool / name / arguments where the hook schema wants the payload wrapper with arguments as {value, provenance}. Same shape in the cursor and nat adapters. The tests stay green because example_guardian.py reads params.get("tool") too, so both sides agree with each other and disagree with the spec. Nothing validates an emitted envelope against acs_schema.json. One test that does would have caught all of it, and it's the highest-leverage thing you can add here.
The deny path fails open on anything it doesn't recognize. In translate_response, an unknown or empty decision returns {}, which Claude Code and Cursor both read as "proceed." ACS_DEFAULT_DENY only kicks in on an exception (Guardian unreachable), not on a Guardian that answers with a verdict the map doesn't know. So a v0.2 disposition, a typo, even a trailing space surviving .lower(), all proceed. NAT already does the right thing and blocks under default_deny. The other two should match it. One line each.
No signing at all (this one's adjacent to #11, not the same). The adapters don't HMAC the envelope, and the example Guardian neither verifies a signature nor checks for replays. The READMEs call this "deferred to transport," but conformance.md:28 lists the baseline signature as a Core MUST, and :67 says transport doesn't satisfy it. #11 is about key distribution and rotation once you're signing. This is the step before: the reference ships with no signing, so every copy starts from an unauthenticated channel. The default http://127.0.0.1:8787/acs in the config keeps that invisible until someone repoints the URL at another host.
Delegation walks around the gate. SubagentStart isn't in HOOK_MAP, Claude Code can't block on it anyway, and example_guardian.py allows Task by default. A subagent spawn isn't evaluated before it acts. That's the adapter-level version of #16, and it lands on the exact confused-deputy path the subagent hooks were promoted to cover. At minimum I'd surface it in mapping.md's "not mapped" list instead of leaving it silent.
On the tests: "40 tests, all passing" is true on your machine, not in CI. NAT's 12 tests skip when nvidia-nat-core isn't installed (I got Ran 12 tests in 0.000s, OK (skipped=12)), Cursor's live test is a skip placeholder, and no workflow runs the adapter tests at all (only sync_version.yml). Skips read as passes, so a regression that lets a denied call through lands green. There's no requirements.txt under adapters/, and the NAT install is unpinned (pip install nvidia-nat-core, no ==). The NAT deny test also catches ACSGuardianDenied and returns without asserting the call was actually aborted, so it passes as long as something raised. This cluster worries me second-most, because green tests on a security control are worse than no tests. Pin the dep, run the unit tests in CI, and have the deny tests assert a real side effect didn't happen (the file wasn't written, the counter stayed at 0).
Smaller stuff, and I'm less sure these are worth blocking on:
example_guardian.py's regex missesrm -fr /,rm --recursive --force /,rm -rf ~, andfind / -delete. It's labeled illustrative so I won't die on this hill, but it's the only thing a newcomer can run on day one, so I'd make it harder to fool or louder about being a toy.- A PostToolUse deny can't undo a side effect that already ran (Claude Code's own hook docs say PostToolUse can't block the action). Cursor's
beforeReadFilereturns{}, so a denied file read still happens. The pre-hooks are the only real gate, and the docs should say so plainly. - NAT's
_build_requestisn't inside the try/except, so a non-serializable kwarg throws beforedefault_denycan catch it. Andpost_invokeignores a result-side deny. mapping.mdand the code disagree on the deny shape for non-PreToolUse hooks. The doc says{"continue": false, "stopReason": ...}, the code emits{"decision": "block", "reason": ...}. One is wrong against Claude Code's contract.
None of this changes my read on the direction. I like where this is going, and the cross-adapter table in the README is genuinely useful. I'd hold the "reference implementation" label until the envelope matches the schema and the deny paths fail closed, since those are the parts people copy without reading the footnotes. Happy to send a PR with the schema-validation test, or pair on the envelope fix if that's faster.
Tracking: I cross-linked the delegation gap onto #16 and the no-signing gap onto #11 so the protocol-level and adapter-level views sit together. The rest of the findings here are adapter-specific with no matching issue.
(cc @afogel since the envelope and signing points touch conformance.md.)
|
Merge-order note: this should land after #21, and after the change-request items above are addressed.
No git conflict with #20 or #21 (this only touches |
Rock's PR GenAI-Security-Project#22 review caught that the three reference adapters and the example Guardian shared a wire format that diverged from specification/v0.1.0/request-envelope.json: acs_version / request_id / timestamp / metadata at the envelope's top level instead of inside params, timestamp as epoch milliseconds instead of ISO-8601 string, tool payload missing the required payload wrapper, arguments not wrapped per tool-call-request.json. Tests passed because the adapter and the example Guardian agreed with each other; the canonical spec was outside the test loop. This commit: - Restructures every adapter's envelope to nest the AcsParams fields inside params, ISO-8601 timestamps, metadata.{agent_id, session_id} populated, payload wrapped per the relevant hook schema, arguments wrapped as {value: ...} per tool-call-request.json:26-37. - Updates example_guardian.py to read from params.payload, gate the Task subagent tool by default, and expand the destructive-Bash regex set (rm -fr, --recursive --force, ~, --no-preserve-root, find / -delete / -exec rm, chmod 777 on system paths). - Fixes the fail-open-on-unknown-disposition bug in claude-code and cursor translate_response; NAT pre_invoke and post_invoke now default-deny on unknown verdicts. - NAT post_invoke now honors a Guardian deny verdict by clearing context.output and setting acs_post_invoke_redacted, matching Specification §6.4's output-redaction gate. - NAT _build_request is now inside the try/except in both pre_invoke and post_invoke so build errors apply the same fail posture as transport errors. - Adds tests/test_envelope_schema.py to each adapter. These validate every adapter-emitted envelope and per-hook payload against the canonical v0.1.0 JSON schemas loaded from $ACS_SPEC_DIR. They are hard-FAIL if the schemas are missing — not skipped — because spec validation is non-negotiable. - Adds .github/workflows/adapter_tests.yml to run the schema + round- trip + live tests per adapter on every push and PR, with the spec schemas pulled from upstream Agent-Control-Standard/ACS:main. - Pins nvidia-nat-core==1.7.0 (adapters/nat/requirements.txt) and jsonschema>=4.20,<5 (adapters/requirements-test.txt). - Updates each adapter's README conformance table to be MUST-honest against docs/spec/conformance.md: handshake, baseline HMAC-SHA256 integrity, replay nonce, system/ping, wrapped MCP are now marked ✗ not implemented, with citations. The previous "deferred to transport layer" claim for baseline integrity was inconsistent with conformance.md:28 and :67. Test counts (all pass, zero hidden skips): claude-code: 17 schema + 13 round-trip cursor: 36 schema + 13 round-trip nat: 6 schema + 7 round-trip + 5 live (NAT 1.7.0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The original adapter_tests.yml had three bugs that made it non-functional and reproduced the exact failure mode PR GenAI-Security-Project#22 review flagged: tests "passing" via skips that operators read as green. Fixes: - Python 3.13 → 3.12. NAT 1.7.0 has no 3.13 wheel; the install step would fail silently in skip mode on any NAT-dependent test. - Drop `example-guardian` from the matrix — no tests/ directory there; the matrix entry crashed on `unittest discover tests`. - Add the cross-adapter conformance suite (`adapters/test_acs_core_ conformance.py`) as its own job. That 48-test file was previously not run by CI at all. Skip handling, per Rock's "skips read as passes" point: - NAT job: NAT is installed (pinned `nvidia-nat-core==1.7.0` + matching `nvidia-nat-langchain`), so ANY skipped test means the test gating is buggy. Hard fail. - Conformance job: zero skips allowed — every ACS-Core MUST runs. - Other adapters: surface skips as warnings (Claude Code's live tests legitimately skip when the `claude` CLI isn't installed in CI; Cursor has a manual-procedure placeholder). Both intentional. Now exercises ~190 tests on every push to adapters/ or specification/, with the load-bearing security tests pinned and required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Thanks @rocklambros for the thorough review! every item addressed. Wire format vs request-envelope.json - Every emitted envelope is schema-validated against canonical request-envelope.json from $ACS_SPEC_DIR (not against fixtures, not against the example Guardian's shape). params wrapper, ISO-8601 timestamps, payload + {value, provenance} arg wrapping - all to spec. Deny fails open on unknown - _fail(cause=…) taxonomy across all three adapters covering transport, adapter exception, signature failure, and 7 JSON-RPC error codes via shared guardian_error_cause(). Unknown disposition + ACS_DEFAULT_DENY=1 → block; otherwise an ACS_AUDIT fail_open_bypass event with the cause label. Signing - HMAC-SHA256 across all three adapters via adapters/_common/. Every envelope signed; every response verified. SIGNATURE_INVALID / REPLAY_DETECTED / TIMESTAMP_OUT_OF_WINDOW each map to a distinct audit cause. Subagent delegation - example_guardian gates Task by default (subagent_gated); opt-in via ACS_ALLOW_SUBAGENT=1. Each adapter's mapping.md lists where delegation hooks aren't honorable by the framework. Tests on tests - Deny tests now assert the real side effect didn't happen, the way you described — counter checks plus, for NAT, a canary-file pattern (if rm -rf runs despite the deny, the canary file vanishes regardless of what the counter says). A real Vertex/Gemini react_agent run surfaced silent-bypass bugs the synthetic tests would have shipped; all have regression tests. CI workflow - Pinned nvidia-nat-core==1.7.0 + nvidia-nat-langchain==1.7.0, Python 3.12, runs the per-adapter + conformance suites on every push to adapters/ or specification/, hard-fails on any skipped NAT or conformance test. Smaller items: rm regex hardened (-rfv, -fr, --recursive --force, --no-preserve-root). READMEs say plainly that pre-hooks are the gate; post-hooks redact via output=None + audit. NAT _build_request moved inside try; post_invoke result-side deny propagates. mapping.md and code now agree on deny shape. Would appreciate re-review when you have a window. |
…w Wrapped MCP claim Two findings from Rock's review of PR GenAI-Security-Project#22: P1.1 — Conformance CI fails for the right reason now. Without rfc3339-validator installed, jsonschema's date-time format checker silently no-ops and test_timestamp_is_iso8601 false-passes (invalid "yesterday" passes validation; assertion sees an empty error list; CI shows green on a real wire-format bug). Pin rfc3339-validator in adapters/requirements-test.txt + add a fail-fast setUpClass guard that rejects any future degradation: if the date-time checker accepts "not-a-date", the whole conformance class refuses to run with a pointed error message. P2.2 — Wrapped MCP claim narrowed. conformance.md:26 lists protocols/MCP/* as part of the Core baseline. Our Core10_WrappedMcp suite verifies the WIRE-FORMAT shape (envelope validates, Guardian returns a structured response, no crash) but not full MCP request wrapping; the reference Guardian routes incoming MCP through the standard toolCallRequest path with the tool name reflecting the MCP method. The module docstring and the top-level adapters/README now say plainly that a green run = "ACS-Core baseline minus full Wrapped MCP", not "the whole baseline". v0.2 deferral marked explicitly. Deployments needing full wrapping must extend the Guardian. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… to malformed base64
Reviewer caught that base64.b64decode() in verify_signature() ran
without exception handling. A malformed signature value
("not-base64", padding garbage, truncated input) raised
binascii.Error up to the Guardian's request handler, which only
catches GuardianError. Result: a bad signature tore down the
request path on the wire (uncaught exception, 500-class response)
instead of returning the spec's SIGNATURE_INVALID (-32004). Same
risk on the adapter side for malformed signed responses. Security
control was a DoS vector.
verify_signature() now catches binascii.Error / ValueError /
TypeError around the b64decode and returns False — the existing
caller chain (Guardian's check_signature, adapter's response
verification) then emits -32004 with cause=signature_invalid_*
and the audit event fires correctly.
Regression test: Item15_VerifySignatureRobustToMalformedBase64
exercises 7 forms of unparseable input (garbage, padding-only,
mid-string padding, oversized, empty); every one must return
False, none may raise.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ation-only on every adapter
Reviewer caught that ACS-Core §hooks.md describes agentResponse as
decision-eligible (ALLOW / DENY / MODIFY), but every adapter
silently drops denies on the hook that produces it. Claude Code
maps Notification → agentResponse and returns {} on deny; Cursor
afterAgentResponse does the same; NAT lifecycle hooks are
fire-and-forget through the IntermediateStepManager subscription.
The framework constraint is real and not fixable in this PR:
- Claude Code's Notification fires AFTER assistant message
delivery — no veto path.
- Cursor's afterAgentResponse fires AFTER the message — same.
- NAT's IntermediateStepManager is a notification stream;
subscriber callbacks cannot abort an event after it fires.
This commit makes the docs honest about that. Each adapter's
mapping.md now marks the relevant hook explicitly as
"observation-only" with an explanation of which framework
boundary blocks pre-delivery enforcement. The per-adapter
README conformance tables narrow the dispositions claim to
"ALLOW / DENY / MODIFY on pre-execution hooks" with a pointer to
mapping.md for lifecycle / post-execution observation-only
posture.
Also includes a hunk missed from the previous Wrapped MCP commit:
adapters/README.md's top-level claim now says "ACS-Core baseline
minus full Wrapped MCP" to match what the conformance suite
actually verifies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…claim on post_invoke redaction Reviewer caught that the Post-tool-deny-redaction row still said post_invoke sets acs_post_invoke_redacted=True, contradicting the code in adapters/nat/acs_adapter.py:294 / :717 — InvocationContext is a strict Pydantic model and that extra attribute would crash. The real redaction signal is context.output = None plus the ACS_AUDIT post_invoke_redacted event. README now says so. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
I ran this rather than just reading it, and the first thing I hit is that the CI job can't fail. Everything else I found is downstream of that in one way or another, so I'll start there. This is a long comment because it's a 14,515-line PR that turns a docs repo into a code repo. The work is real and the threat model in The CI gate reports success when the tests failBoth test steps pipe into python -m unittest test_acs_core_conformance -v 2>&1 | tee out.log # line 39
python -m unittest discover -v tests 2>&1 | tee out.log # line 83GitHub's implicit shell for I checked rather than assuming: The header comment says the workflow "addresses the 'skips read as passes' failure mode," and the skip grep at lines 41 and 91 does work. So right now the job catches "a test didn't run" and misses "a test ran and failed," which I'd argue is worse than no workflow, because the badge certifies something false.
The conformance suite can't run from a fresh clone
Running the documented command from Two things make this worse than a broken default. The documented remedy in
The Claude Code adapter drops subagent events entirelyI said something wrong about this earlier and want to correct it. HOOK_MAP: dict[str, str] = {
"SessionStart": ..., "SessionEnd": ..., "UserPromptSubmit": ...,
"PreToolUse": ..., "PostToolUse": ..., "Notification": ..., "Stop": ...,
}
if hook_name not in HOOK_MAP:
return 0Feeding it a Cursor has the mirror-image problem. NAT's And even if all three emitted,
|
…d honest scoping (PR GenAI-Security-Project#22 review) Squashed response to the full PR GenAI-Security-Project#22 review. Ships the three reference adapters (Claude Code, Cursor, NAT) as ACS v0.1.0 EMISSION conformance: the adapters, driven through their real production entry points, emit schema-valid, signed Core traffic and honor the decisions the suite tests — validated against the canonical schemas by an independent oracle. It is deliberately NOT a full ACS-Core deployment-conformance claim (that spans Guardian + framework wiring + production config and is tracked as milestone GenAI-Security-Project#33). Enforcement correctness - Guardian REFUSALS (SIGNATURE_INVALID, REPLAY_DETECTED, TIMESTAMP_OUT_ OF_WINDOW, malformed/oversized envelope) fail CLOSED regardless of posture — each is attacker-reachable, so routing them through the §6.4 fail-open posture was a bypass primitive. HTTP-layer refusals (413/400) and oversized envelopes are caught before the wire. - Error responses are signed (schema + Guardian + adapters); an unsigned spoofable error under fail-open is an allow. - Handshakes are signed (only system/ping is signature-exempt, §13); forward-compat accepts matching-major versions; malformed ClientHello and non-object/batch JSON-RPC return -32600 instead of crashing. - Claude Task spawns emit steps/subagentStart (confused-deputy gate); Cursor default installer + example wire it fail-closed on BOTH ACS_DEFAULT_DENY=1 and failClosed:true; the Guardian gate is deny-by-default; no fabricated subagent lineage. - ServerHello on_decision_failure is honored (most-restrictive-wins); handshake failures negative-cached; secret-file-unreadable and unsigned-mode are loud, not silent; rfc8785 is a hard dependency. Emission conformance suite - CaptureGuardian oracle validates the exact bytes each production adapter sends (real subprocess for Claude/Cursor; real middleware pre/post_invoke + lifecycle observer for NAT) against the canonical schemas, with an INDEPENDENT HKDF+HMAC+JCS signature verifier (not acs_common) pinned by a frozen known-answer vector, and proven non-vacuous by negative self-tests. - Per-event: each Core method emitted once, envelope+payload valid, UUID/RFC3339/metadata/{value}-wrapper invariants. Per-session: handshake-once, unique request_ids, request_id_ref correlation, advertised==emitted (both directions). Truthful gating - One authoritative runner with platform selection (claude|cursor|nat), pass/skip/fail reported separately, exact (suite,test,reason) skip allowlist, fails on any UNEXPECTED skip. CI: shell:bash/pipefail (failing suites can't read green), DCO check, pinned deps + SHA-pinned actions, NAT installed so its tests run. - Example configs are under test (the file people copy), not just wire.py output. De-duplication - Emission is the sole schema-validation owner; the double-validating build_request() schema tests were removed after moving every mapped native event into the emission matrix (coverage verified preserved). Suite reflects distinct assertions, not overlapping layers. Docs - Claim scoped to emission conformance throughout; hard-coded test counts removed; Wrapped-MCP kept as shape-only with its Core status flagged as a pending spec-owner decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Bar Kaduri <bar@capsule.security>
There was a problem hiding this comment.
Five findings from running the branch , with four of the five are one mistake in different places.
- A Guardian DENY with a trailing space executes the tool call. §6.4:158 says an arrived decision must be honored regardless of posture, and §6.3:146 already rules that an unusable one is a DENY.
- Failing closed on
StopandSubagentStopemits{"decision":"block"}, which Claude Code reads as "do not stop, continue".subagent-stop.json:5marks both hooks not decision-eligible. settings.json.examplepasses all four settings through a per-hook"env"object that Claude Code does not implement, so a copied config runs unsigned, fail-open, with no audit sink. The test guarding that file asserts the same discarded field.- Without
rfc8785the adapter exits 1 with empty stdout, which Claude Code treats as non-blocking, so the tool runs with no audit event and no regard forACS_DEFAULT_DENY. - The cached ServerHello is read without verifying its signature, so the observed agent flips its own fail posture by editing one word in a predictable path, or by touching an empty
.failedmarker.
The tests have a similar blind spot. The suite covers a Guardian that says nothing, thoroughly, across all three adapters. It does not cover a Guardian that says something unusable, or a framework that ignores what the config tried to say. §6.4:156 and §6.4:158 draw exactly that line, and the suggested expectations put it under test.
Two notes on scope, neither one a code comment.
§6.4:156 lists "malformed response" among the transport failures, which is where treating an unreadable decision as a decision failure finds cover. Line 158 then says an arrived decision must be honored regardless of posture. Those rub against each other for the case in finding 1. A clarifying sentence in §6.4 would turn that comment from an argument into a bug report, and settle the same question for whoever writes a fourth adapter. It fits with #32 rather than this PR.
This branch also carries three normative schema edits: subagent-stop.json drops final_chain_hash from required, response-envelope.json adds a signature to the error object, and otel-mapping.json moves an attribute to optional. The reasoning behind each looks sound. The PR description still says the change set touches no normative spec text. They want their own PR and a spec-owner decision, especially with #21, #31 and #32 open on adjacent ground.
The site build depends on a bare `uv run pytest -v` running in the environment uv.lock produces. With no testpaths, pytest collects the whole repository, so any suite whose dependencies sit outside the lockfile fails at collection and takes the deploy down with it. PR #22 adds 23 adapter test files needing rfc8785, nvidia-nat-core, and ruamel, none of them locked: against a merged tree that is 22 collection errors and an interrupted run, which fails the test job and stops the build job that depends on it. Scoping collection to tests/ keeps the deploy gate about the guards it was written for. Suites carrying their own dependencies run from their own workflow, which is what adapter_tests.yml already does. CONTRIBUTING now says where guards live and why a test written elsewhere never runs, since that silence is what let both contributors build suites CI would not have executed. Signed-off-by: rocklambros <rock@rockcyber.com>
The table mapped ten specific paths and stopped, so a new top-level directory arrived governed by nothing until someone noticed it was unlisted. The adapters/ directory in PR #22 is the next one due to land. Apache 2.0 is the default because a new directory is usually code, and an over-permissive grant on prose costs less than a ShareAlike obligation attaching by accident to reference code adopters copy into their own systems. A prose directory still wants its own explicit CC-BY-SA-4.0 row. Signed-off-by: rocklambros <rock@rockcyber.com>
Nobody does, in v0.1.0. profiles_supported and profiles_accepted are self-declaration on the wire, and the release ships no conformance suite, no registry, and no steward to arbitrate a disputed claim. The page described what the label guarantees without saying that the label is the implementer's own assertion, which is the gap issue #19 raised against a standard that markets itself as a control standard rather than a wire format. The paragraph sits below the ACS-Core requirement list rather than inside it, so the line numbers PR #22's citation guard pins do not move. Signed-off-by: rocklambros <rock@rockcyber.com>
|
One more thing landed on main, and it's the kind of thing that would have looked like your fault.
Fixed in dbbd92d. |
The skill lifecycle hooks are missing from all three adapters, and the spec page is whyNone of the three adapters emit
This is our fault, not the contributor's. The hook taxonomy table in Specification section 5 listed sixteen hooks and omitted the entire skill lifecycle set. The Hooks page says nineteen. Both were correct about everything else, so there was no signal that the shorter list was the stale one. Anyone building against the specification page, which is the page that reads as canonical, never learned that skills are a governable surface at all. I have a fix queued that adds the three hooks to the taxonomy table, renumbers it to nineteen, and adds a guard test that reads the schema titles under Worth noting the harness in this PR is already ahead of the spec page: What this is worth doing about hereSkill emission is SHOULD, not MUST ( For claude-code and cursor there is an observable event: both invoke skills through a tool call, so The real obstacle is that
A gap on our side that would block youIf you do emit skill hooks and want them under ACS-Trace, there is nothing to map them to. The skill lifecycle has no OpenTelemetry span name and no OCSF class, in either the docs tables or the normative mappings: Same root cause, wider blast radius than the one table. That one is ours to close and I am raising it separately. |
One squashed commit carrying the full PR GenAI-Security-Project#22 branch, rebuilt on current main (DCO: all history signed; branch is a direct child of main). Adapters (each: acs_adapter, wire.py, mapping.md, README, tests): - Claude Code: hook-to-steps translation with explicit turn tracking (turnStart/turnEnd, per-session state), native allow/deny/ask, modify via merged updatedInput, defer substituted to deny + audit. SubagentStop is deliberately unmapped: steps/subagentStop requires final_chain_hash, which a chain-less framework cannot honestly produce; a separate schema PR proposes making it optional. ADAPTER_VERSION 0.1.3. - Cursor: documented-field payload builders (docs.cursor.com, 2026-08-22), turn tracking, beforeSubmitPrompt exit-2 blocking, failure_type-to-exit_status mapping, fail-closed contradictory modifications per §6.3. - NAT: pre/post-invoke middleware with negotiated timeouts, durable audit sink, redaction-or-deny output gate. Shared infrastructure: - acs_common: RFC 8785 (JCS) + HKDF per-session HMAC signing (§10), handshake with signed ServerHello binding and negative cache, total decision normalization, §6.3 composition-violation check. - example_guardian: signed error envelopes, capped regex scanning, durable file-locked replay state, subagent gate no weaker than the generic tool gate. Binds --port 0 and announces the assigned port on stdout so test spawns own their port by construction. - Conformance + emission suites (230 checks) driving both CLI adapters and the Guardian; NAT covered by its own suite (35). Guardian refusal handling is deliberately stricter than v0.1 spec text (always fail closed); tracked as spec issue GenAI-Security-Project#32. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Bar Kaduri <bar@capsule.security>
aae26f8 to
1b99678
Compare
PR #59 arrived with Co-Authored-By and Claude-Session trailers naming a model. The trailers were dropped in the squash, which is the right outcome and the wrong way for a contributor to learn the rule. Nothing in CONTRIBUTING.md, STYLE.md, or .github/ said it. The reason is the DCO, which already sits in this file. A sign-off is a certification a person makes about the origin of the code, and a model cannot make it. Naming one as a co-author puts a party in the trailer that cannot stand behind the certification the next line asserts. The paragraph goes in Development Process, after the spec-Discussion note and next to the sign-off step it depends on. That keeps it clear of the adapters hunk PR #22 adds higher in the file. Signed-off-by: rocklambros <rock@rockcyber.com>
|
Heads up on a branching change landing ahead of Thursday's kick-off: this pull request will need its base moved from
This one moves for several reasons at once: Two things worth knowing. This PR is the second link in the Strategic Adoption Plan's serial chain, so it should carry |
…gt (#60) ## Summary Adds `reference-implementations/agt`: a working ACS Guardian built on Microsoft's [Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit) (AGT), two host shims for Claude Code and OpenCode, a shared host adapter, an envelope Inspector, and a conformance harness that measures what ACS v0.1.0 can express of AGT, cell by cell. AGT runs unchanged at a pinned commit. Neither client contains AGT code, and AGT contains no client code. They share only the ACS envelope. The tree validates every envelope against this repository's own `specification/v0.1.0/` schemas, by relative path, so the implementation and the specification cannot drift apart. A three-minute captioned demo is embedded in the tree's README: one Guardian denies the same command from OpenCode and then from Claude Code, and the conformance harness prints the mapping table, the coverage matrix and the trace rows. ## What lands | Path under `reference-implementations/agt/` | What it is | |---|---| | `README.md` | How to install, run, read the contract and view the events. What the tree is and is not, measured against the ACS-Core list. Links every package README | | `packages/guardian/` | The ACS server: `POST /acs`, envelope validation, AGT policy input, verdict mapping, session hash chain, envelope log. Own README | | `packages/agt-bridge/` | Constructs the AGT SDK runtime and publishes the bundled OPA binary to the process environment. Own README | | `packages/host-adapter/` | Envelope building, handshake, decision validation, failure posture, audit log. Shared by both shims. Own README | | `packages/inspector/` | Tails the Guardian's logs and renders each envelope, decision, audit entry and chain entry. Own README | | `packages/conformance/` | The mapping table, the eight-by-five coverage matrix, the trace rows, and the upstream watch. Own README | | `hosts/claude-code/`, `hosts/opencode/` | The two shims, their hookmaps, and their READMEs. The OpenCode README carries a stub model for a run with no paid account | | `policy/` | AGT's stock bundle at the pinned commit, byte for byte, plus `data.json`, two manifests, and the MIT text | | `mapping.yaml`, `agt.lock`, `bun.lock`, `package.json`, `tsconfig*.json`, `opencode.json`, `.gitignore` | Configuration and lockfiles | | `scripts/` | `verify-pin`, `run-conformance`, `run-upstream-watch`, `regenerate-curl-resolved-hosts`, `verify-zero-diff` | | `test/` | The cross-package suite: dispositions, redaction, information-flow round trip, architecture invariants, the pin, README captures | | `docs/demo.mp4`, `docs/demo-poster.jpg` | The demo recording and its poster frame | Outside the tree: `LICENSING.md` gets a row and a provenance section for the vendored MIT bundle, `NOTICE` gets a paragraph, and the root `README.md` gets one line under Getting Started. ## What was left out, on purpose The tree was built in [afogel/ACS_reference_implementation](https://github.com/afogel/ACS_reference_implementation) over ten slices. The slice records, the shaping documents, the captured runbooks, that repository's CI workflows, its `SECURITY.md` and its MIT `LICENSE` are not carried over. They describe how the tree was built, not how to use it. The README links the standalone repository for that history. ## Changes made for this location - `spec/acs` was a git submodule pointing at this repository. It is gone. `packages/guardian/src/validate-envelope.ts` and `packages/conformance/src/trace-pillar.ts` resolve `specification/v0.1.0/` from the repository root by relative path, and the Guardian's path redaction now strips this repository's root rather than the tree's. The relocated-copy test in `packages/guardian/test/server.test.ts` was updated to match. - Comments that cited the slice runbooks now name the standalone repository. - Nothing else in the code changed. ## Measured From `reference-implementations/agt`, after `bun install --frozen-lockfile`: ``` bun run typecheck # zero errors bun test # 1109 pass, 1 skip, 1 fail, 68 files ``` The one failure is in `packages/guardian/test/check-response.test.ts`. It expects `response-envelope.json` to accept a ServerHello as a `result`, which is what #59 adds. With #59's `response-envelope.json` in place, the same run is 1110 pass, 1 skip, 0 fail. The skip is the byte-identity check, which needs `UPSTREAM_BUNDLE` from `bun run verify:pin`. So this PR depends on #59. Merge #59 first and the suite is green with no change here. The README's Verify section states the same dependency in terms that stay true on any checkout. ## What this is, and is not It is a working demonstration that AGT's unchanged policy engine can govern two different agent clients over one wire contract. The tool-call path is complete in both directions. All five AGT verdicts cross the wire as ACS decisions. Four of AGT's nine stock gate classes are live. It is not a complete ACS implementation. The Guardian claims `acs-core` with qualifications the README lists row by row: two of nineteen hooks are evaluated, `ask` fails the response schema for want of `ask_details`, there is no replay protection, no signature, no `system/ping`, and the wire is unauthenticated. The README's operational-debt and open-findings sections record the rest. ## Licensing `policy/lib/` is Microsoft's AGT policy bundle, MIT, byte-identical to the pinned commit. A test enforces that identity, so the files carry no added header. `LICENSING.md` gets a row and a provenance section, `NOTICE` a paragraph, and the MIT text sits at `policy/LICENSE-AGT`. Everything else in the tree lands under Apache 2.0 through the existing catch-all row. ## Not in this PR - No CI workflow. The suite needs bun and `trash`, so under CONTRIBUTING it belongs in its own workflow. I can add one scoped to `reference-implementations/agt/**` if you want it in this PR or a follow-up. - #22 proposes `adapters/` for configuration-only host adapters. This tree's subject is the Guardian side and its measurement of the contract, with the two host shims as the clients that prove it. If you prefer one directory for both, the tree can move. --------- Signed-off-by: Ariel Fogel <fogeltine@gmail.com>
|
@rocklambros retarget to integration and priority:P0 are done per your note, and the branch now carries current integration merged in, repo suite (including the new link guard over adapters/), the adapter gate (230), and NAT (35) all green on the merged tree. Holding the rest for #21, matching the blocked label. The day it lands, one alignment commit follows: the "#21 (open; not in this branch)" hedges flip to the then-current floor, and subagentStop gets wired in both IDE adapters — the optional final_chain_hash removes the reason it was unmapped. Staged for same-day turnaround; ready for your pass right after #21. |
Operationalizes the contribution governance the core team agreed on September 8, filtered through the Strategic Adoption Plan v3 committed outcome. Design: `design/2026-09-09-contribution-governance-design.md` (v1.1) Plan: `design/plans/2026-09-09-contribution-governance.md` ## What this installs **Current Priority Scope.** `CONTRIBUTING.md` gains one section that states what the project is driving at for the next ninety days, what is deferred to v0.2.0, and what is out of scope by design. It is stated once. Every other surface links to it rather than restating it, and the landing page now renders it straight out of `CONTRIBUTING.md` so the two cannot drift. **Branching.** `main` publishes the site and all 44 schema `$id` URIs on merge, so specification and code land on `integration` and publish on a deliberate promotion. A guard (`tools/base_branch_guard.py`, 16 tests) enforces a positive path allowlist and fails closed on anything nobody anticipated. The promotion exemption checks the head *repository*, not just the ref, so a fork branch named `integration` cannot walk a schema change onto the publishing branch. **Intake.** Blank issues are off. Six forms route by type, and no form can stamp a `scope:`, `priority:`, `workstream:`, or `status:accepted` label. That prohibition is the whole structural guarantee behind maintainer-only triage. The sixth form asks for nothing but a description, because the strongest outside contribution this project has received would have fit none of the other five. **The gate.** A change to behavior, normative text, or code references an issue carrying `status:accepted`. An editorial correction does not, wherever it lands. A pull request whose issue is not accepted yet is neither closed nor reviewed. It waits, and a bot says why. **Automation.** Four workflows: the base-branch guard, an integration sync, a weekly promotion pull request, and the intake comment. Plus a reminder that opens an issue when the priority scope passes its review date. **Phase 2 executor.** `tools/apply_governance.py` performs the live migration steps idempotently, `--dry-run` by default. It is structurally incapable of merging, closing, or retargeting a pull request, and a test asserts no code path can emit those commands. ## Authorship policy change The rule that a maintainer strips a `Co-Authored-By` trailer naming a model is removed. The project now mandates neither direction. The human `Signed-off-by` stays required, because only a person can make the DCO certification. A standard about agent provenance should not erase the provenance of its own commits. ## Reviewer notes - Nothing on the published site changes except the landing page's Contribute section. I built the full pipeline from `main` and from this branch and diffed: the only files that differ are `index.html` and `acs.css`. All 44 schemas are byte identical. - 253 tests pass. `mkdocs build --strict` passes. - This targets `integration` rather than `main` because it touches `.github/`, `tools/`, and `tests/`, which is exactly what the guard it installs requires. ## Still to do after this merges Retarget #63, #24, #60, and #22 to `integration`. Merge #21, #20, and #22 when the team is ready. Then run the executor for rulesets, default branch, required check, and the seeded issues. The order that matters is in the plan. --------- Signed-off-by: rocklambros <rock@rockcyber.com>
A full audit of the documentation against the live repository, verifying every claim with `gh` rather than reading prose. One statement was false. `CONTRIBUTING.md:29` described the AGT reference implementation as being "in PR #60". That pull request merged, and the code sits at `reference-implementations/agt/`. Corrected to name the path. Everything else was checked and left alone because it was already true. That includes the default-branch statements in README and CONTRIBUTING, which became correct when the default moved to `integration`, and the `help wanted` pointer, which became correct when issues #86 through #94 were filed. Pull requests #21 and #22 and issues #18, #19, #32, #33, #37, #53 and #70 were each verified still open before their sentences were left standing. Two semicolons flagged by the style check sit inside the Developer Certificate of Origin, which is verbatim legal text, and were deliberately not touched. This targets `main` directly because `CONTRIBUTING.md` is on the documentation-lane allowlist. The base-branch guard should pass, which is the lane working as designed. 259 tests pass, `mkdocs build --strict` clean, landing page renders with no surviving placeholder. Signed-off-by: rocklambros <rock@rockcyber.com>
Summary
A top-level
adapters/directory with reference adapters that wire three agent frameworks to an ACS Guardian through configuration only — no agent code changes — plus a runnable example Guardian and the conformance machinery that proves what the adapters emit.adapters/claude-code/settings.jsonwiring, explicit turn tracking (turnStart/turnEnd), native allow/deny/ask, modify via mergedupdatedInput, automated live tests againstclaude --printadapters/cursor/hooks.jsonwiring, documented-field payloads (docs.cursor.com, 2026-08-22),permission+ exit-code-2 blockingadapters/nat/FunctionMiddlewarefor NVIDIA NAT 1.7.0, configured in workflow YAML; a Guardian deny prevents the target function executingadapters/example-guardian/adapters/_common/acs_common(RFC 8785/JCS + HKDF-per-session HMAC signing, handshake with signed-ServerHello binding, total decision normalization) + the conformance and emission suites265 automated checks green in CI: 230 in the adapter gate (guardian conformance,
_common, claude-code, cursor — including emission tests that validate the exact bytes each adapter emits against the in-repo schemas and recompute signatures independently) plus 35 NAT, plus one documented manual procedure for Cursor live verification.Scope, stated plainly
tests/live_verification.md); the adapter is fully tested headlessly. Claude Code and NAT have automated live tests.KNOWN_UNMAPPED+ eachmapping.md) — fabricating unknowable fields would corrupt the artifacts they exist for.subagentStopre-wires once Slim ACS-Core: relax MODIFY, system/ping, and wrapped MCP to SHOULD #21 makesfinal_chain_hashoptional.git diffagainst the base touches nospecification/file: adapters, the CI workflow,.gitignore, and one CONTRIBUTING section.The adapter pattern
ACS-Core specifies what a hook event looks like on the wire and what the Guardian's decision looks like coming back. It does not dictate how a framework physically wires the interception in. Each adapter demonstrates the boundary choice for its framework:
hookSpecificOutput.permissionDecisionargv[1]permission(top-level, per-event) + exit code 2FunctionMiddlewareclassACSGuardianDenied(NAT 1.7.0) orInvocationAction.SKIP(NAT dev)All three emit the same signed ACS envelope. Decision honoring follows §6.4: fail-open-with-audit by default, fail-closed via
ACS_DEFAULT_DENY=1or the ServerHello posture; Guardian refusals always fail closed (deliberately stricter than v0.1 text, tracked in #32).The top-level
adapters/README.mdcontains a step-by-step walkthrough with concrete JSON payloads, a cross-adapter comparison table, and a flow diagram. Read that first.Why in-spec
adapters/and not separate reposSingle repo for spec + reference implementations on the first batch makes the spec evolve alongside the adapters that exercise it (the tests on this PR found several real schema gaps between docs and actual behavior — that feedback loop is what makes the spec text trustworthy). When the pattern stabilizes and individual adapters need their own release cycle, splitting to separate repos (or packaging, per the SDK plan) is straightforward.
Sequencing
Base is
integration,status:blockedon #21 — the Core floor this posture conforms to. When #21 lands, one alignment commit updates the floor references and wiressubagentStop.Running