Skip to content

Tighten forward compat for 0.x versions - #255

Open
aaronormerod wants to merge 2 commits into
GenAI-Security-Project:integrationfrom
aaronormerod:spec/forward-compat-0x
Open

aaronormerod wants to merge 2 commits into
GenAI-Security-Project:integrationfrom
aaronormerod:spec/forward-compat-0x

Conversation

@aaronormerod

@aaronormerod aaronormerod commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

Narrow the §3 forward compat row: while the major version is 0, require a matching X.Y and accept any patch. negotiated_version in handshake.json now defers to that row. The §17.1 -32001 row also covers a step whose params.acs_version is not the session's negotiated_version.

Which issue does this implement

Closes #196

Base branch

  • integration, because this touches docs/spec/

Type of change

  • Specification change (schema, hooks, events, AgBOM)

Specification changes

  • I opened a Discussion before this PR
  • Schema changes validate against the JSON Schema spec
  • I described the impact on downstream implementers below

No structural schema change. One description in handshake.json now points at §3. Matches the usual semver reading for 0.x, and stops a 0.1 implementation from evaluating 0.2 messages under 0.1 rules.

Breaking for implementers? Only for one that accepted other 0.y versions, or a step whose version differs from the negotiated one. Both now get -32001.

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

Closes GenAI-Security-Project#196

Signed-off-by: aaronormerod <137795541+aaronormerod@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

This pull request's description says it closes #196, 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.

@acs-board-reconciler acs-board-reconciler Bot moved this from Needs triage to In progress in ACS Project Tracker Oct 12, 2026
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 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.

Aaron, your new row is the rule issue #196 suggests, and PR #348 and PR #169's Go Guardian already follow it in code. I ran the merged tree on integration 19b1197. Pytest, mkdocs build --strict, typecheck and bun test on Bun 1.3.11 and 1.3.14 give the same counts as integration.

@bar-capsule @afogel, this PR waits on item 5 on issue #32. Your answer decides if it merges as written.

It also sets what PR #229, PR #348 and PR #22 change to. PR #183 belongs on that list too, because its Python Guardian follows the old row and negotiates 0.2.0. I ran each of those PRs, and the row leaves three things open that your answer should cover:

  1. Which number does the rule compare? Item 5 used 0.1.4 as its example, but that's a release number. acs_version has stayed 0.1.0 through every release, and CHANGELOG.md:7 says "the handshake cannot tell the old rules from the new". So this row refuses 0.2.0 and can't see a breaking change that ships under 0.1.0. Does a breaking spec change have to move X.Y?

  2. Does the rule apply to params.acs_version on each step, or only at hello? §17.1 defines -32001 only "at handshake". With 0.2.0 on a step after a 0.1.0 hello, PR #229 allows the step, and PR #348 and PR #169 answer -32001. Your answer decides what is left of PR #348 and whether PR #229 can close issue #139.

  3. What does a 0.1.0 Guardian put in negotiated_version for a client that offers only 0.1.1? handshake.json:75 says it selects from the client's list. PR #169 and PR #183 send 0.1.1. Integration, PR #348 and PR #22 send 0.1.0, and PR #22's test requires that. PR #229 refuses the client.

Once you answer, Aaron can push and one of you can approve and merge.

Aaron, please wait for that answer, so you only push once. If they accept your rule, I need one change:

  1. specification/v0.1.0/handshake.json:75, the negotiated_version description, still says "MUST match the client's major version." After your row, §3 refuses a 0.2.0 client and this line accepts it, so the problem in issue #196 stays for anyone who reads the schema. Replace that sentence with:

    MUST satisfy the Forward compat rule in Specification §3.
    

    The rule then lives in one place, your row. I tried this on a scratch copy and every suite gave the same counts. No test reads this text, so check it with git grep -n "client's major version" docs specification, which should print nothing.

If Bar or Ariel answer differently, change the text to match their answer.

Please also comment on issue #196, as I asked on PR #253, so I can assign it to you.

@drk-ram drk-ram 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.

I checked 99b5908 against #196. The 0.x exception now excludes different minors while preserving patch compatibility and the existing post-0.x rule.

The three specification guard files pass: 10 tests. Reverting the row leaves those guards green, so they do not enforce this wording. A focused text assertion catches the reversion.

This clarifies the contract; it does not implement runtime negotiation.

I could not build the documentation locally because mkdocs is unavailable. I stopped the broader test run after errors; I am not claiming full-suite success.

No blocker in the changed rule.

@bar-capsule

Copy link
Copy Markdown
Collaborator

@rocklambros @aaronormerod, following my answer on #196:

  1. The rule compares acs_version, the version the peers exchange in the handshake, not the release number. My [Bug] §3 forward compat accepts any matching major version, which under 0.x accepts every 0.y release #196 example meant acs_version values. Under 0.x, a breaking spec change moves its minor, so the handshake can tell the old rules from the new.
  2. Both. The hello negotiates by this rule, and every step's params.acs_version must equal the session's negotiated_version; anything else is refused with -32001. The -32001 row in §17.1 should say so, since today it says "at handshake" only, and this PR is the place for it.
  3. negotiated_version comes from the client's list, as handshake.json already says: a 0.1.0 Guardian and a client that offers only 0.1.1 negotiate 0.1.1. A patch doesn't break anything, so they can still talk.

@aaronormerod, with that, please make Rock's handshake.json change and the §17.1 row, and this is ready for approval.

cc: @afogel

negotiated_version now defers to the Forward compat rule instead of
repeating a major-version match. The -32001 row also covers a step whose
params.acs_version is not the session's negotiated_version.

Signed-off-by: aaronormerod <137795541+aaronormerod@users.noreply.github.com>
(cherry picked from commit 2beeb549317f756fef717a439ce4823691b3bc5f)
@aaronormerod

Copy link
Copy Markdown
Collaborator Author

Done. handshake.json now defers to §3, and the -32001 row covers per-step params.acs_version. git grep for "client's major version" is empty.

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.

[Bug] §3 forward compat accepts any matching major version, which under 0.x accepts every 0.y release

4 participants