Skip to content

Align expectations for Guardian reference implementations (#252) - #256

Open
artmaro wants to merge 5 commits into
GenAI-Security-Project:integrationfrom
artmaro:docs/refimpl-architecture-concept
Open

artmaro wants to merge 5 commits into
GenAI-Security-Project:integrationfrom
artmaro:docs/refimpl-architecture-concept

Conversation

@artmaro

@artmaro artmaro commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

What changed

The README says what we expect from each maintained Guardian reference implementation. The expectations cover architecture, message signing, logging, concurrency, use of the AGT runtime, and verification.

Which issue does this implement

Closes #252

Base branch

  • integration, because this touches the specification, schemas, a reference
    implementation, an adapter, tests, or CI
  • main, because every changed path is on the documentation lane allowlist in
    CONTRIBUTING.md

Type of change

  • Specification change (schema, hooks, events, AgBOM)
  • Reference implementation or adapter
  • Documentation
  • Tooling or CI
  • Governance (licensing, security policy, contributor docs)

What I would like reviewers to weigh in on

  1. Maintained languages. The document names TypeScript, Python, Rust, and Go. Is the project ready to maintain four? May be we really want to start with three and others can be community based from ACS project support.
  2. Every maintained implementation evaluates policy with the upstream AGT runtime. A reimplementation does not count.
  3. Until the adaptors ready, we'll use shims and client code. I propose we move them out of the Guardian trees into separate folder. I also suggest we create contract tests to validate all implementations in order to approve PRs.
  4. key_id. The specification does not say what key_id holds for HMAC. The document proposes a key name, with default as the default.

I tested this

  • I synced my branch with the base branch before opening this
  • uv run pytest -v passes on my machine
  • uv run mkdocs build --strict passes on my machine

Checklist

  • Commits are signed off with git commit -s (required by the DCO)
  • Prose follows STYLE.md
  • No secrets, tokens, or internal URLs in the diff

Security

  • This change has no security impact

…ations

The directory had no top-level document. The new README says what the
directory holds, then states what we expect from each maintained Guardian
reference implementation.

Four open pull requests add or rework a Guardian, and each read the
one-sentence porting issues differently. The expectations let every
implementation be reviewed against the same list.

The expectations are a working aid for review. They are not part of the
specification and they are not normative.

Part of GenAI-Security-Project#252

Signed-off-by: Evgeny Kokuykin <artmaro@gmail.com>
@github-actions

This comment has been minimized.

…n bytes

The introduction now says only what the directory is. The table of
contents and the status line are gone, because both would go stale as
implementations land.

Key material is at least 32 bytes, not 32 characters. The specification
and every implementation count bytes.

Part of GenAI-Security-Project#252

Signed-off-by: Evgeny Kokuykin <artmaro@gmail.com>
@acs-board-reconciler acs-board-reconciler Bot moved this from Needs triage to In progress in ACS Project Tracker Oct 12, 2026
@rocklambros rocklambros removed the status:needs-triage Not yet triaged. Applied by the issue forms label Oct 12, 2026

@rocklambros rocklambros left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@artmaro, a few fixes before we merge:

  1. Lines 49 and 51 don't match the TypeScript Guardian (PR #229). It sets key_id to the session id, not default, and it base64-decodes the secret instead of using the raw string. A Go or Rust Guardian built to this README won't verify against the TypeScript one. Please change both lines to match what PR #229 does.
  2. Line 23 says we maintain a contract test suite, but we don't have one yet. Please change it to "will maintain". Line 79 depends on the same suite, so please reword it too.
  3. Line 35 should name the runtime: agent_control_specification, at the version in agt.lock. AGT's Go module is a different policy engine, so "the AGT runtime" is ambiguous for Go.
  4. Lines 69-75 read as rules the TypeScript Guardian already meets, but its logs aren't bounded yet. Please mark them as targets.

On your questions: I'd list only TypeScript as maintained for now, and add the others as they merge. Yes to the AGT rule and to moving the shims.

- Name the policy runtime, agent_control_specification, at the version in
  agt.lock, and say that AGT's Go module does not qualify.
- Set key_id to the session_id and base64-decode the key material, as
  the TypeScript Guardian in GenAI-Security-Project#229 does.
- List only TypeScript as maintained. Other languages are added as they
  merge.
- Say that the contract test suite will exist, and make verification
  depend on it.
- Mark bounded processing and bounded logging as targets the TypeScript
  Guardian does not meet yet.
- Add message signing to the architectural boundaries.

Part of GenAI-Security-Project#252

Signed-off-by: Evgeny Kokuykin <artmaro@gmail.com>
@artmaro

artmaro commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for reviewing, @rocklambros. All points are addressed in the latest commit:

  1. key_id is now the session id and the key material is base64-decoded, matching fix(agt): implement ACS-Core wire safeguards #229. I'll ask the other pull requests that sign to follow.
  2. The contract test suite is now "will maintain". Will make separate ticket for that one.
  3. Added reference to the version in agt.lock.
  4. Bounded processing and bounded logging are marked as targets.
  5. The maintained-implementations line now lists only TypeScript and we will update readme as we go.

@rocklambros

Copy link
Copy Markdown
Collaborator

@artmaro, thanks for turning these around. One correction before this goes further, and it's on me for not flagging it sooner.

key_id shouldn't be the session id. Ariel settled what key_id means on issue #118 (comment 6149587377): it's an opaque label the deployment chooses, naming one configured secret. A verifier MUST reject an unknown key_id and MUST NOT fall back to another secret, and key_id MUST NOT be derived from the secret. PR #272 puts that into §10. PR #229 still ties it to the session id today (server.ts:700, :915, :1358), so "matching PR #229" would carry that forward, and I've asked for the same change there.

So could you make that line say key_id comes from configuration, with no default, and please hold off on asking the other signing PRs to switch to the session id? PR #183 has already moved to the configured-label form. The base64 decoding of the key material is fine as is.

President added a commit to disciplinedware/agent-control-standard that referenced this pull request Oct 15, 2026
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>
@rocklambros

Copy link
Copy Markdown
Collaborator

@artmaro, one more thing on the signing section, and it folds into the key_id change I just asked for.

Here's the exact wording from §10 so you can match it in one pass (docs/spec/instrument/specification.md:354): "key_id is an opaque label, chosen by the deployment, that names one configured secret... A verifier MUST reject an unknown key_id (SIGNATURE_INVALID) and MUST NOT fall back to another secret." So line 51 should set key_id from configuration with no default, and the verifier rejects an unknown label instead of comparing it against the session. Keep the sentence about the response carrying the request's key_id, that one's right.

The bit I don't think anyone's caught: line 49 describes the HKDF derivation but never says what key_id actually points at. The spec ends that sentence with "...a 32-byte output, applied to the secret that key_id names." Without that clause the paragraph still reads like the session is the only input, which is how line 51 drifted in the first place, so add it.

Honestly, I'd just link §10 for this whole block and list only what a reviewer checks on the PR, rather than restate the crypto here. We watched the restatement drift once already. If it says "sign and verify per §10" it can't fall out of sync the next time §10 moves.

The HKDF params and the base64 decode are both fine, so nothing to change there.

Review on this pull request asked to link section 10 rather than restate
the HMAC key derivation and the meaning of key_id, so the README cannot
drift when section 10 changes. GenAI-Security-Project#272 adds those rules to section 10.

The section keeps only what section 10 does not define: how key material
is encoded and read, that it stays out of code and logs, and that each
implementation documents how to configure and replace it.

Part of GenAI-Security-Project#252

Signed-off-by: Evgeny Kokuykin <artmaro@gmail.com>
…rchitecture-concept

Signed-off-by: Evgeny Kokuykin <artmaro@gmail.com>
@artmaro

artmaro commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thank you, @rocklambros, agreed that's a better shape for this section.

The signing section now links §10 instead of restating it, so the HKDF clause and the key_id rule come from §10 directly. Only after we finalize the readme I'll start asking reference implementations to follow the principles.

We can merge this PR after #272. Please take a final look. Thanks

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[RefImpl] Expectations for Guardian implementations

2 participants