Repository navigation
Conversation
Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
…179) CodeQL default setup was turned on for this repository on 2026-09-21 and covers the Actions language. This adds zizmor, which the [OWASP GitHub Actions Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/GitHub_Actions_Security_Cheat_Sheet.html) recommends beside CodeQL for defense in depth. zizmor reads each workflow's logic and checks what CodeQL does not: hash pins no tag points to, version comments that disagree with their pin, App tokens broader than the job needs, and credentials a checkout leaves in `.git/config`. Run against `integration` before this change with online audits on, zizmor 1.30.1 reported 15 findings. This clears 13. The other two are `github-app` findings on `board-reconcile.yml`, whose App token takes the installation's full grant instead of naming its repository and permissions. Token scopes are not changing right now, so those two stay open in code scanning, where they record the deferred decision instead of disappearing behind a suppression. - **`pr-intake.yml`, 1 high.** zizmor flags every `pull_request_target`. This one checks out no code and reads the pull request body as data through an environment variable, which the workflow's header already explains. It carries an inline `zizmor: ignore` that points at that explanation. - **Eleven checkouts across nine workflows, 11 low.** Each left the job's token in `.git/config`. None of those jobs pushes with git. They call `gh` with `GH_TOKEN` or touch no network at all, and `sync_version.yml` hands its push to `create-pull-request`, which configures its own token. All eleven now set `persist-credentials: false`. The checkout in `validate-owasp-metadata.yaml` also gains the `# v7.0.1` comment every other pin carries. - **`reference-implementation.yml`, 1 medium.** The setup-bun pin `735343b` is v2.0.2, and its comment said v2.0.1. Only the comment changes. Dependabot's #83 already treats it as 2.0.2. The new workflow runs zizmor from a `zizmor` dependency group in `uv.lock`, pinned with hashes. No new action enters the Actions allowlist, and Dependabot's uv ecosystem moves zizmor under the existing 7-day cooldown. The group is not a default group, so `uv sync --locked` in the deploy and test jobs never installs it. A default sync against this lockfile would uninstall it. It runs on pull requests and pushes that touch `.github/`, an action definition, `pyproject.toml`, or `uv.lock`, and weekly, because the online audits compare pins against advisories published after the pin. Findings upload as SARIF under the category `zizmor`, and the job stays green on findings so code scanning tracks them and a ruleset can gate on them later. A tool or upload failure still fails the job. A pull request from a fork cannot upload with its read-only token, so it gets annotations instead. `upload-sarif` is pinned at v4.38.0 because v4.38.1 was three days old, inside the cooldown this repository applies to its dependencies. Open pull requests #162 and #169 add workflows that zizmor will flag once they land, for unset `persist-credentials` and the same `# v2.0.1` setup-bun comment. Verified locally: zizmor 1.30.1 with online audits reports only the two deferred `github-app` findings, with 1 ignored and 26 low-confidence findings suppressed by the default persona. The workflow's own command produces valid SARIF 2.1.0. `actionlint` is clean. `uv lock --check` passes under uv 0.9.9, the version CI pins, and the lockfile diff adds only zizmor. `uv run pytest` gives 278 passed, 1 skipped. Signed-off-by: Rock Lambros <rock@rockcyber.com>
…comment in the Go workflow zizmor, added in GenAI-Security-Project#179, reports both once this workflow lands. Nothing in the job pushes with git, so the checkout sets persist-credentials to false. The setup-bun pin 735343b is v2.0.2, and only its comment changes. Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
…efore the code On the 24 Sep call the pull request was called too big to review. This commit changes documentation only. - docs/architecture.md, new: what the Guardian is made of, a diagram for each path a request takes, and the order to read the code in. - docs/embedding.md is now docs/extending.md: what your own implementation of each interface must do. - README: "Start here" links every section and document. - Each fact lives in one document; a Mermaid diagram GitHub could not render and several statements that did not match the code are fixed. No code changed. make check, uv run pytest (279 passed) and uv run mkdocs build --strict pass. Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
|
@afogel @rocklambros Following today's call I reorganised the docs so the review can go in two steps.
No code changed. Happy to answer questions while you go through it. |
|
I completed an independent substantive review of the Go Guardian against the ACS v0.1.0 requirements and existing TypeScript Guardian behavior. I checked the handshake/envelopes, duplicate request_id handling, chain hashing, all five dispositions, MODIFY/ASK/DEFER behavior, replay/skew handling, HMAC, session and chain handling, intent immutability, decision honoring, system/ping, wrapped MCP, PolicyEngine/delegate_to, hook-specific behavior, failure/state consistency, concurrency, TypeScript↔Go parity, external Guardian wire behavior, AGT bridge parity, ACS-Core vectors, and the existing #160–#167 regression areas. |
|
Not a review of the Go implementation, I have not read it at that depth. But I did check it against the six reference-Guardian bugs in the #160 to #167 range, and each one has a named mechanism here. Capability enforcement returns Worth adding that the wire here is authenticated. Anything other than All of those are filed against the TypeScript reference, so they stay open on it. But a second implementation that does not reproduce them is useful evidence for what the reference should be doing, and every claim above is cheap to re-check from the name. |
Brings in PR GenAI-Security-Project#21, which changed specification/v0.1.0/hooks/subagent-stop.json. Claude-Session: https://claude.ai/code/session_01NFMr7gTJSEMihfcUTXMhR4 Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
…ecurity-Project#21) - Refresh the embedded hooks/subagent-stop.json: final_chain_hash is now optional, and the minimal subagentStop in the tests omits it. - The Codex and OpenCode host adapters deny a MODIFY their host cannot apply with modify_unsupported and record it in their audit log (§6.5). They record a signed MODIFY of invalid shape as modify_invalid (§6.3). - The docs cite GenAI-Security-Project#21 as merged ACS-Core, not an open proposal. Claude-Session: https://claude.ai/code/session_01NFMr7gTJSEMihfcUTXMhR4 Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
|
Merged integration (with #21) into this branch and updated the code for it:
The Guardian works the same as before. |
|
Re-checked the eight against 2b8ccf5 after the merge, since my earlier note named code at 158243d. All still hold: On the section 6.5 change, the One thing the merge changed in this PR's favour. The acs-core row #21 landed now names replay protection and a baseline HMAC-SHA256 signature in the mandatory column. Those are the two things this tree has and the TypeScript reference discloses as absent, so the floor moving today made the gap between them wider rather than narrower. |
The host adapters answered DENY at every hook when they could not honor a Guardian decision. §6.5 forbids DENY at postCompact, and hooks.md permits no DENY at five hooks, whose actions have already happened. - Where the hook permits no DENY, the host's refusal is ALLOW plus an audit record. This covers a MODIFY the host cannot apply (modify_unsupported), a MODIFY of invalid shape (modify_invalid), a refused handshake (now recorded as session_refused) and the decision failure posture. - The handshake diagram shows the AuditLog envelopes. Claude-Session: https://claude.ai/code/session_01NFMr7gTJSEMihfcUTXMhR4 Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
The default build uses the pinned AGT Rust library through its C ABI. Set EVALUATOR=go to build or run the Go evaluator explicitly. Keep one projection and one evaluator interface. Verify the loaded library against its build record. Refuse changed sources and missing native support. Compare both evaluators against recorded decisions. Preserve empty result labels and retain decision slots until uninterruptible engines return. Use GOOS and GOARCH for Linux amd64 and arm64, both Mac architectures, and Windows amd64. Package the library and build record with the binary. Use MinGW for Windows and the Apple SDK for native macOS builds. Add portable timeout and shutdown tests and a five-platform CI matrix. Build Linux amd64 and arm64 images with Docker Buildx. Keep secrets outside the image. Run signed conformance probes through a published host port. Update build, configuration, architecture, and deployment documentation. Validation: Linux make check passes. Windows AGT, CLI shutdown, and recorded decisions pass under Wine. Both Linux images build and pass all 10 external probes; arm64 executes through Docker emulation. Real Windows and macOS runner results remain unverified. Claude-Session: https://claude.ai/code/session_01NFMr7gTJSEMihfcUTXMhR4 Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
Go 1.27.2 fixes the net/http CONNECT connection desynchronization (GO-2026-6605). Go 1.27.2 and golang.org/x/net v0.60.0 fix the HTTP/2 HPACK encoder race (GO-2026-6617). Use the same Go version in the image. Run Staticcheck through go tool with this module's pinned dependencies. The versioned go run command cannot read Go 1.27.2 export data. Document the supported lint command. Check the custom-engine module with the same pinned Staticcheck and govulncheck binaries. Check its module tidiness and run vet once. Run govulncheck with and without the native AGT build tag. Prepare Go build commands for all pinned native targets to check cgo flags. This check detects flags Go rejects without requiring cross-compilers. Show each make check stage as it starts and completes. Use one line in terminals and separate lines in CI. Keep the existing gate order and save full output in a private log. Print diagnostics on failure and retain the gate's nonzero exit code. Update and simplify the documentation. Validation: full make check passes with Go 1.27.2, including both evaluators, race checks, conformance, and govulncheck. Both Linux image architectures build and pass the external conformance probes. The full gate also passes with compact output. Negative checks prove that failed checks remain failures and that the full log is retained. Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
The repository's Actions policy does not allow msys2/setup-msys2, so the workflow failed at startup and no Go job ran. Remove the Windows row and its MSYS2 step. Linux amd64, Linux arm64 and both Mac architectures keep the complete gate. State in building.md that a Windows build is checked by hand. Claude-Session: https://claude.ai/code/session_01NFMr7gTJSEMihfcUTXMhR4 Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
… is tested check-coverage.sh discarded the output of go test, so a test that failed during the coverage run left only an exit code. The macos-15-intel job of run 38003172529 failed this way. Keep the output and print it on failure. No run on Windows backs the Windows claims. The README and building.md now say that the Windows amd64 build is not tested on Windows. Claude-Session: https://claude.ai/code/session_01NFMr7gTJSEMihfcUTXMhR4 Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
|
@Mayur021 @artmaro - pushed an update:
|
|
@President, a heads-up about your embedded spec copy. This PR is based on Three more spec PRs touch the same files: #258 is open, #261 is open, and the #228 change is waiting on those two. Each one changes the copy again. Please rebase on integration, run |
rocklambros
left a comment
There was a problem hiding this comment.
@President, I went through the commits from 9 October, since every review on this thread so far was done on 5 October against 2b8ccf52. That means fee41d41 (proceed where DENY is not allowed) and 470dd21a (AGT through the C ABI) haven't had a review yet. I merged this branch onto current integration (185ef4e) and ran the Go tests there with Go 1.27.2. Here's what needs to change before it can merge.
-
The embedded spec copy fails on the merged tree.
TestSchemasMatchSpecificationreports thattrace/ocsf-mapping.jsonandtrace/otel-mapping.jsondiffer, because PR #63 changed both. This is the same ask as my earlier comment: rebase on integration, runcd reference-implementations/agt-go && go generate ./internal/schema, and push. Aftergo generate, every package andgo run ./cmd/docs-checkpassed for me. -
TestRecordingIsCurrentwill go red on integration after every TypeScript Guardian merge.agtbridge/recorded_cases_test.go:139-145compares the recording withgit log -1 --format=%Hfor the TS paths. We squash-merge into integration, so even if a TS author re-records inside their own PR, the squash creates a new SHA and the test fails right after merge. I checked this by adding a no-op commit underagt/packages/guardian/srcon the merged tree, and the test failed. Four open PRs touch that path today (PR #244, PR #246, PR #229, PR #112). Please record tree IDs instead of a commit, for examplegit rev-parse HEAD:reference-implementations/agt/policyfor each path, both inscripts/record-ts-cases.shand in the test. Tree IDs are content-addressed, so they survive a squash. -
opa_timeoutcan equaldecision_timeout, andguardian.yamlships both at5s. AGT runs oneopaprocess per decision and cannot be interrupted (agteval.go:279-281), so when OPA is slow the agent's 5s deadline expires before AGT's own fail-closed deny arrives. A deployment that negotiatedproceedthen runs the tool ungoverned.validate()incmd/guardian/main.goalready enforceswrite_timeout > decision_timeoutat line 290. Please add the same kind of check for theagtevaluator (opa_timeoutmust be less thandecision_timeout), setopa_timeout: 4singuardian.yaml, and add a row toevaluator_test.gofor it. -
At hooks where DENY is not allowed, a failed decision plus a failed audit write now proceeds with no record anywhere. That's in
internal/hostadapter/client.go:155-157, andclient_test.goasserts it aspost_compact_audit_unwritten. ALLOW is the right answer atpostCompact, but §6.4 says the bypass "MUST be recorded". Please print one JSON line to stderr whenrecordAuditEventfails, and extend that test to check for it. In the same commit, the §6.3 row indocs/conformance.mdstill says "Met", even though an invalid MODIFY at those five hooks now gives ALLOW and not DENY. I think your reading is right, but it's an interpretation, so mark the row that way and add a line todocs/differences.md. I'll open a spec issue to put thepostCompactexception into §6.3. -
The PR body describes the code from before 9 October. It says the Go Guardian uses the OPA Go library, but the default is now
evaluator: agtthrough cgo, andEVALUATOR=gois the fallback. It also says five files sit outside the directory, but there are six (agt/README.mdchanged). The 279 tests and the 87.5% figures are old too. Reviewers judge the design from the body, so please update it.
These aren't blockers, but they'd be nice in this PR or a follow-up:
- Add a test that
agt/agt.lockandagt-native.lockname the same AGT ref. Right now each evaluator only checks its own lock file, so an AGT bump can leave the default evaluator behind. scripts/check-coverage.shleaves outagtbridge/agtevalandinternal/hostadapter, so the 80% floor measures the non-default path.max_engine_calls: 256made sense in-process. Underagteach slot is an OS process, so a lower default foragtwould be safer.refusalAtdepends on the Decision lines inhooks.md, butmethod_test.go:50only checks the count. A test that reads the Decision lines fromdocs/spec/instrument/hooks.mdwould catch a disposition change.
I also checked a few things that came out clean: AGT reads the OPA env vars once at build time, an unknown evaluator value fails validate(), and AGT's FFI wraps every call in catch_unwind.
|
@President, one more thing I forgot in the review, and it isn't a blocker. Once this merges, nothing will propose updates for the Go module. |
An independent implementation of the ACS v0.1.0 wire contract under reference-implementations/agt-python/: JSON-RPC envelopes, handshake negotiation, the hook taxonomy, the five dispositions, the SessionContext chain with a published chain_hash, replay and skew protection, and the HMAC-SHA256 baseline signature. Wrapped MCP is not implemented yet, so the ServerHello answers profiles_accepted: [] rather than claiming acs-core; the README's status table says exactly which parts are implemented. The suite validates envelopes against this repository's schemas by relative path, pins the cross-implementation signature vectors from the open Go port (GenAI-Security-Project#169), ports the conformance harness's ten external probes, and pins every hook's schema to its method name. A path-scoped workflow runs it in CI. Signed-off-by: Duke - Duc Dinh <dtduc.contact@gmail.com>
Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
The embedded schemas follow integration after GenAI-Security-Project#63, GenAI-Security-Project#258 and GenAI-Security-Project#261. The parity recording stores the git object IDs of the deciding TypeScript paths instead of a commit, so a squash merge no longer makes TestRecordingIsCurrent fail. The paths include agt.lock and the agt-bridge package.json, which pin the AGT commit and SDK. The recorder and the test read them from one list; the test fails when the recording names other paths or was made from another corpus, and the recorder refuses uncommitted changes to them. A test checks that the Go workflow runs on every deciding path. agt/README.md tells a TypeScript contributor when to re-record. A test requires agt-native.lock and agt.lock to pin the same AGT repo and commit. The native build fails when AGT at that commit changes ffi.rs, which agteval/acs.h is written from, or fetches another opa than agt-native.lock pins. Under the agt evaluator, policy.opa_timeout must be shorter than protocol.decision_timeout; guardian.yaml ships 4s. Otherwise a slow opa lets the agent stop waiting before AGT's deny arrives (GenAI-Security-Project#269). make load measures either evaluator under concurrent clients, selected by EVALUATOR like the other targets. Under agt each decision runs one opa process; on a 20-core host decisions stop rising near 16 concurrent ones, and at 256 they reach the 5s timeout. guardian.yaml now ships limits.max_engine_calls 32, and docs/load.md records the results. The load run found that an engine call gave its slot back only after its answer was sent, so a client's next step could be refused engine_saturated with no call running; callEngine now releases the slot first, also after a panic. When the host adapter's audit log cannot be written, the record goes to stderr as one JSON line with the final disposition, and the refusal's reasoning says whether the record went to stderr or nowhere. The audit file's close error now counts. The Codex and OpenCode hooks pass their own stderr. The §6.3 row says that ALLOW at a hook that permits no DENY is an interpretation (GenAI-Security-Project#270), and the docs say only the adapter library reaches those hooks. Dependabot proposes Go updates, one pull request per dependency across both modules. The Go workflow runs weekly, so govulncheck sees advisories for the toolchain that Dependabot does not move, triggers on the agt-bridge package.json, and no longer fetches full history. A test checks that the method table permits DENY exactly where hooks.md does, and fails on a Decision line it cannot classify. The coverage floor counts every package except commands, examples and test helpers. The Guardian meets the expectations GenAI-Security-Project#256 sets for reference implementations, mapped to tests in docs/conformance.md. audit.max_file_bytes and audit.max_files rotate each audit file; guardian.yaml keeps at most 1 GiB per log; max_file_bytes needs max_files of at least 1, so a rotation never deletes the live log. make load can set the rotation and fails if the log drops, splits or reorders a record or exceeds its bound; docs/load.md records a run and the cost of long sessions. storage.provider names the session store. The HMAC secret file holds base64 of at least 32 bytes. key_id is a configured label with no default, an unknown key_id is refused (GenAI-Security-Project#118, GenAI-Security-Project#272), and the hooks and the conformance harness take it as a flag. A toolCallResult whose request_id_ref matches no request is recorded (GenAI-Security-Project#272). Decisions from other pull requests that the Go code contradicted: - A shell command's URL ends only at ASCII whitespace; U+FEFF or U+00A0 in the userinfo no longer hides the host curl reaches (GenAI-Security-Project#268). - Versions are compatible when they share X.Y under 0.x; the handshake negotiates only such a version, and a step must match it (GenAI-Security-Project#196, GenAI-Security-Project#255). - A policy target that is not a string is denied evaluation_failed; the TypeScript fix is in GenAI-Security-Project#229, so two recorded cases are listed as ahead of the recording until it is re-recorded. - The host adapter records every decision failure under either posture, with its class, handshake failures included (GenAI-Security-Project#37). - The host adapter denies under any posture a signed error other than -32603, an unverified answer and a malformed answer; the docs mark this as an interpretation of §6.4 ahead of GenAI-Security-Project#32, which is open. The version rule is marked the same way against §3. - The audit directory is 0700; a symlinked audit path or an audit file open to group or others is refused (GenAI-Security-Project#71). Claude-Session: https://claude.ai/code/session_01DFdYnoBitHhPcUSq7wMWs1 Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
|
This pull request's description says it closes #87, which is on the project roadmap. When it merges, GitHub closes that issue and the roadmap counts it as delivered. Please confirm the change delivers the whole issue before merging. |
|
@rocklambros thanks for the review! Everything is in commit Schema copy is out of date
Review
Non-blockers:
Dependabot
Also in this commit
|
Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
- Regenerate the embedded schema copy and re-record the TypeScript answers after merging integration; GenAI-Security-Project#275 changed four schemas and the Guardian sources. No recorded answer changed. - An empty string in an AGT verdict field is read as absent, as the TypeScript mapVerdict now does: no policy_references, no reason_codes, and a warn without a rule is an evaluation failure (GenAI-Security-Project#263). - One test signer for the agtbridge tests instead of five copies. Claude-Session: https://claude.ai/code/session_01DFdYnoBitHhPcUSq7wMWs1 Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
|
@rocklambros one more commit after my reply: c6e9db2 - merged integration after #275 (schema copy regenerated, TS cases re-recorded, no answer changed), and an empty AGT reason now names no rule, same as #275 does in TS. |
- The writer's queue is bounded by audit.max_queue_records and audit.max_queue_bytes (4096 records and 64 MiB in guardian.yaml), the record being written included. Before, only the record count was bounded: a stalled disk could hold 4096 request bodies of limits.max_body_bytes, about 4 GiB, and requests are recorded before their signature is checked. The library takes the limits as guardian.JSONLQueue, zero meaning the defaults. The command refuses a max_queue_bytes that cannot hold one request of max_body_bytes. - Every record that does not reach the files is counted: dropped from a full queue, failed, discarded after a write failure, or offered after Close. A queue-full report no longer hides a later write failure, and the command returns the count on every exit path. - A queued envelope no longer keeps the caller's body alive beside its own copy: it held a pointer into the record. - A record dropped from a full queue keeps its seq, so a reader of the file sees the gap. - shutdown takes separate decision and delivery grace periods; its test keeps a short decision grace and delivers within shutdown_grace. - Remove agtbridge.test, a test binary committed by mistake, and ignore *.test. make check refuses a tracked binary file, a file over 1 MiB and a home-directory path. Tests: queue bounds by bytes and records, retained heap against counted bytes, losses add up under 8 concurrent producers, seq gaps, Close timeout, configuration bounds. Each was seen failing on an injected regression. make check passes, also merged with integration; pytest 919 passed; mkdocs --strict builds; the TypeScript suite 1237 passed. Claude-Session: https://claude.ai/code/session_01NqVwNgkbimQeV7PRGx9Zpa Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
Re-record the TypeScript answers: GenAI-Security-Project#289 changed the Guardian sources. No recorded answer changed. Claude-Session: https://claude.ai/code/session_01NqVwNgkbimQeV7PRGx9Zpa Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
|
@rocklambros - one more commit, 6e02186:
|
Closes #87. A Guardian written in Go. It is in
reference-implementations/agt-go/, next to the TypeScript Guardian.I compared the two Guardians on the same sessions
The policy files are the same. Both Guardians read the AGT Rego files in this repository. By default the Go Guardian runs them with the AGT runtime itself: AGT's Rust library, linked through its C ABI (cgo), built from source at the commit
agt-native.lockpins. A test requires that commit to be the oneagt/agt.lockpins for the policy bundle.EVALUATOR=gobuilds without cgo and runs the same files with the OPA Go library instead.I wrote 49 test sessions and recorded the TypeScript Guardian's answers to them. Then I sent the same steps to the Go Guardian, signed, the way an agent sends them, once with each evaluator.
Two declared exceptions, both in
docs/differences.md. When the engine fails, both Guardians deny withevaluation_failed, and each writes its own reasoning. And in two sessions the policy target is not a string: the Go Guardian denies it, as the TypeScript fix in #229 does, while the recording still holds the old ALLOW. The test fails once a new recording carries that fix, so the exception cannot outlive it.The recorded file also stores the Git object IDs of the TypeScript paths that decide an answer: the Guardian and bridge sources,
mapping.yaml, the policies,agt.lock, and the bridge'spackage.json, which pins the AGT SDK. The testTestRecordingIsCurrentcompares them with the current ones. Object IDs depend only on content, so a squash merge does not change them. If someone changes that code, this test fails, and the sessions have to be recorded again withscripts/record-ts-cases.sh.What it does
cmd/guardianis a program. You run it next to an agent.make runstarts it withguardian.yaml. It uses AGT as its policy engine.make agt-nativebuilds AGT's library and fetchesopa;docs/building.mdcovers Linux, macOS and the Windows cross-build. Each decision under AGT runs oneopaprocess, somake loadmeasures latency and memory under concurrent clients;docs/load.mdhas the results behind the shippedlimits.max_engine_calls. The HMAC secret file holds base64 of at least 32 bytes, under a configuredkey_id; the audit logs rotate by size;docs/configuration.mdlists every setting, anddocs/conformance.mdmaps each expectation of #256 to its tests.guardianis also a Go library. You give it four things: the policy engine, the keys, the session storage, and the place to write the audit log.CONTRIBUTING.mdleaves the policy engine and the signature algorithm to deployments. The library treats storage and the audit log the same way, so it takes each of the four as an interface.examples/custom-engineis a second Go module. It gives its own four and answers a step.What checks each claim
docs/conformance.mdhas one row per item. Each row names its tests.make docs-checkfails if a named test does not exist.specification/v0.1.0/. The test reads those files by relative path, so if the specification changes, the test fails.denythat was changed to anallow. It also refuses a response signed with the key of another session.docs/conformance.mdsays what it claims and what it does not claim. ACS v0.1.0 has no certification, so the claim is a self-declaration, and each row names the test that covers it.docs/differences.mdlists every difference from the TypeScript Guardian.What I ran
uv run pytest -vpasses, 913 tests.uv run mkdocs build --strictpasses.make checkin the module runs gofmt,go vet, Staticcheck,govulncheck, the tests with the race detector, theexamples/custom-enginemodule, the Codex and OpenCode adapter tests, the shared conformance check against a Guardian that is running, the coverage check, andmake docs-check. It passes. Coverage of the protocol packages, the evaluators, the host adapter and the test Observed Agent, with the AGT evaluator built in, is 85.9%. The floor is 80%.Two defaults you may ask about
on_decision_failure. The library keeps the default from §6.4, which isproceed. Theguardian.yamlin this repository setsdeny. So the Guardian you can run here blocks the step when it cannot decide.Two Codex settings in
make codex-example.-s workspace-writestops Codex from asking the person about each command, so the Guardian is the one that decides. Codex also asks once whether it may run the hook file of this project, and--dangerously-bypass-hook-trustskips that question. Both settings apply only to the Codex program that this command starts.examples/codex/README.mdsays what each one opens.Examples
examples/codex/andexamples/opencode/show that a host uses the decision the Guardian sends.make codex-exampleandmake opencode-examplestart the real Codex and OpenCode programs.make checkdoes not start them. It sends recorded host events to the same adapter code. When reference adapters for these hosts are added, these examples can point to them.Files outside the new directory
There are seven. Two are under
.github/: the Go job,.github/workflows/reference-implementation-go.yml, which also runs weekly sogovulnchecksees new advisories, and thegomodentries in.github/dependabot.yml. Theagt/README.mdchange updates its count of test files, rewords the two sentences around it, and tells a TypeScript contributor when to re-record the answers the Go Guardian replays.The other four add one check to the TypeScript conformance runner. The check sends signed requests over HTTP to any Guardian and reads the answers. It works for any Guardian, not only this one. That is why it sits next to the runner and not inside the Go module.
packages/conformance/src/external-guardian.tsis the check.packages/conformance/test/external-guardian.test.tsis its test.packages/conformance/src/main.tscalls the check and prints whether it ran.package.jsonadds theconformance:guardianscript.