Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
be331e9
Shape two more AGT gates, and measure what stands between them
afogel Aug 17, 2026
5ab3908
Breadboard V9, and find the store eight slices never had
afogel Aug 17, 2026
a0557cf
Freeze V9's names before there is any code to inherit them
afogel Aug 17, 2026
7a63e27
Give two tool shapes one policy target, and one argument declaration …
afogel Aug 18, 2026
bd9492f
Fix review findings: stale into_argument comment, hand-typed leaf lit…
afogel Aug 18, 2026
29616cc
Pull a destination out of a shell command, or answer that there is none
afogel Aug 18, 2026
403719e
Fix docstring: correct the measured behavior of host_of()
afogel Aug 18, 2026
b60d0dd
Carry the command line ACS has always typed and no hookmap declared
afogel Aug 18, 2026
4b1cba1
Turn on a fourth gate class with one config key, and stop treating a …
afogel Aug 18, 2026
c930d5f
Give the deployment's bridge one recipe, and pin the drift manifest t…
afogel Aug 18, 2026
f3d3327
Let a second tool reach the shim, and say which gates are still one-tool
afogel Aug 18, 2026
f81d1a8
Capture V9 against the shipped build, and retract the claims it falsi…
afogel Aug 18, 2026
11a897b
Retract the mechanism where it still stood, and hand the reader the c…
afogel Aug 18, 2026
7e32dbf
Close the userinfo bypass where it is ours, and publish the half that…
afogel Aug 18, 2026
5991af1
Bound the authority at every delimiter, not just the one that was obv…
afogel Aug 18, 2026
f3ceb24
Re-capture the route the last two commits changed, and publish the ne…
afogel Aug 18, 2026
f7d3c79
Stop hand-rolling the URL parse, and hand the gate an origin
afogel Aug 18, 2026
172777c
Re-capture what the origin changed, and publish the shape that surviv…
afogel Aug 18, 2026
00d1e3d
Block rm -rf on the working directory, not just the root
afogel Aug 18, 2026
49e14e3
Stop computing the host, and deny the shapes nobody can agree on
afogel Aug 18, 2026
1d1fcd8
Re-capture the route that changed, and say plainly which route is weaker
afogel Aug 18, 2026
dc9f379
Point V3's runbook at the lines its config actually occupies now
afogel Aug 18, 2026
3ead0d3
Record the tools list this gate actually declares, per gate
afogel Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"hooks": {
"PreToolUse": [
{
"matcher": "^Bash$",
"matcher": "^(Bash|WebFetch)$",
"hooks": [
{
"type": "command",
Expand Down
24 changes: 20 additions & 4 deletions README.md

Large diffs are not rendered by default.

50 changes: 48 additions & 2 deletions docs/demos/v3-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ every point in this narration except where a section is actively showing its own
No edit needed — this is the tracked `policy/lib/data.json` exactly as committed. When V3 shipped
that meant the destructive-command patterns alone; **since V4 the tracked file also carries a
`redact` block**, of which the one the `transform` section below used to add is a subset — V4
ships a second pattern beside it (`policy/lib/data.json:12-15`). Re-run against it, `ls -la` is still
ships a second pattern beside it (`policy/lib/data.json:17-20`). Re-run against it, `ls -la` is still
the clean allow captured here — the redaction patterns match secrets, and this command carries
none.

Expand Down Expand Up @@ -267,7 +267,7 @@ The diff, as V3 captured it (against blob `2530d81`; V4's tracked file is `7130e
`redact` block below is **a subset of what ships** there — V4 added a second pattern,
`AKIA[0-9A-Z]{16}`, beside this one and put the array on a single line, so the tracked block is
neither this text nor this formatting. Read the diff as the edit V3 actually made, not as a
quotation of the current file; `policy/lib/data.json:12-15` is the current file):
quotation of the current file; `policy/lib/data.json:17-20` is the current file):

```diff
diff --git a/policy/lib/data.json b/policy/lib/data.json
Expand Down Expand Up @@ -421,6 +421,52 @@ carries a non-empty `policy_references` and the second carries none at all — t
presence/absence is the entire signal that a policy fired versus stayed silent, per R1.2
and `mapping.yaml`'s `warn: { decision: allow, require_policy_references: true }`.

> **Re-run (V9, slice #28) — the verdict is unchanged, and one field of the capture is not.**
> V9 moved `policy/manifest.drift.yaml`'s `policy_target` — and its annotation's `from` — off
> `$.tool_call.args.command` and onto the shared normalised leaf, because a target naming one
> tool's own argument denies every call by a tool that has no such argument. The drift gate reads
> an annotation rather than the target, so the verdict *should* be unaffected; "should be" is not
> this repository's standard, so it was re-run rather than reasoned about.
>
> Same script above with its port changed to `8792`, same `drift.warn_threshold: 0.5` edit to
> `policy/lib/data.json`, same `ls -la`, and the `request_id` deliberately set to this section's
> own so the two blocks are comparable. The JSON-RPC `id` is the client's own and was not matched;
> it is `1` below and this section's `request_id` above. Captured against commit `16a3ab0`:
>
> ```json
> {
> "jsonrpc": "2.0",
> "id": 1,
> "result": {
> "type": "final",
> "acs_version": "0.1.0",
> "request_id": "64144466-0a20-4564-8e26-92c76e13a597",
> "decision": "allow",
> "reasoning": "This step was allowed, but flagged: the agent's behaviour drifted from its baseline. Policy: drift_detected, from AGT's stock bundle (agt_stock). AGT reported: drift_score 0.9 reached threshold 0.5.",
> "reason_codes": [
> "drift_detected"
> ],
> "policy_references": [
> {
> "policy_id": "agt_stock",
> "rule_id": "drift_detected"
> }
> ]
> }
> }
> ```
>
> `decision`, `reason_codes` and `policy_references` are identical to the capture above, which is
> the whole of what the moved target could have broken. `reasoning` is not, and the cause is not
> V9: `mapping.yaml` no longer sources that field from `verdict.message` verbatim but composes it
> through `field_synthesis.reasoning`'s template and per-rule summaries. AGT's own sentence is
> still in there, at the end, word for word. That change landed after V8 and before V9 (commit
> `1534a59`); every `reasoning` string captured in this file predates it.
>
> The `ls -la` envelope also now carries `raw_command`, which V9 put on the wire. It changes
> nothing here — this manifest declares a `drift_score` annotator, not an `egress` one — and it is
> mentioned so the re-run's envelope is not mistaken for this section's original.

## Both failure postures, live: kill the Guardian mid-session

This drives `hosts/claude-code/acs-hook.ts` directly on stdin, the same mechanism
Expand Down
28 changes: 23 additions & 5 deletions docs/demos/v5-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ always making the one choice this runbook needs it to make.

**The manifest edit is the whole deployment-side change, and it is additive.** OpenCode reports
its shell tool as `bash` (lowercase); Claude Code reports `Bash`. AGT resolves
`policy/manifest.yaml`'s fixed `pre_tool_call.policy_target` (`$.tool_call.args.command`)
**before any authored rule runs**, and a `tool_call.name` the manifest has not registered fails
`policy/manifest.yaml`'s fixed `pre_tool_call.policy_target` — which was `$.tool_call.args.command`
when this slice shipped — **before any authored rule runs**, and a `tool_call.name` the manifest has
not registered fails
that resolution closed — `runtime_error:tool_unknown` for `bash` itself, `runtime_error:path_missing`
for every other tool OpenCode can call. So `policy/manifest.yaml` and `policy/manifest.drift.yaml`
each gained one `tools:` entry, `bash`, beside the existing `Bash`/`run_shell` — nothing removed,
Expand All @@ -48,9 +49,26 @@ Rego authored, `policy/lib` byte-identical under `bun run verify:pin`, `data.agt
unchanged, and zero lines changed in the Guardian, the bridge, or AGT — `bun run verify:zero-diff`
proves that mechanically, captured near the end of this file.

**Both gates are scoped `tools: [bash]`, which makes the two hosts symmetric rather than
asymmetric.** Host #1 is `Bash`-only at both its hooks too, via the anchored `^Bash$` matcher in
`hosts/claude-code/settings.json`. An earlier note in this project's own planning claimed the
⚠️ *That target has moved since, and the sentence above is scoped to when this slice shipped rather
than rewritten, because the mechanism it explains is unchanged: AGT still resolves one fixed
`policy_target` per intervention point, before any rule runs, and an unregistered `tool_call.name`
still fails closed. What changed in V9 (slice #28) is which leaf that one target names. It is now
`$.tool_call.args.acs_policy_target` — a normalised leaf the Guardian writes — because a target
naming one tool's own argument denies every call by a tool that has no such argument, which is
exactly the `runtime_error:path_missing` this paragraph describes, reached from the other side.
`mapping.yaml`'s `policy_target_argument` table says which of each tool's own arguments is copied
into that leaf. See [`docs/demos/v9-runbook.md`](v9-runbook.md).*

**Both gates carry a `tools:` scope, which makes the two hosts symmetric rather than asymmetric.**
When this slice shipped that scope was `tools: [bash]` at both gates, matching host #1, which was
`Bash`-only at both its hooks via the anchored `^Bash$` matcher in
`hosts/claude-code/settings.json`. ⚠️ *Both halves of that sentence are stale as of V9 (slice #28),
and the symmetry it describes survives the change: V9 widened each host's **request** gate to a
second tool and left each host's **result** gate alone. Today OpenCode's request gate declares
`tools: [bash, webfetch]` and its result gate `tools: [bash]`, while host #1's `PreToolUse` matcher
is `^(Bash|WebFetch)$` and its `PostToolUse` matcher is still `^Bash$`. The asymmetry is now
between the two gates rather than between the two hosts, and it is deliberate — see
[`docs/demos/v9-runbook.md`](v9-runbook.md).* An earlier note in this project's own planning claimed the
*request* gate needed no such list, because its hookmap paths (`$.tool`, `$.args`) resolve
whatever tool ran — true, and irrelevant: the manifest's policy target is checked independently of
the hookmap, and an unscoped request gate would not govern every tool, it would **deny every tool
Expand Down
18 changes: 16 additions & 2 deletions docs/demos/v7-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ bun run verify:pin # re-clones AGT and byte-diffs the pinned bundle — n
```

**`bun run verify:zero-diff` is deliberately not in the list above — it does not pass from
this HEAD, and that is not a V7 defect.** Captured real, run bare, exactly as a reader would
run it:
this HEAD, and that is not a V7 defect.** Captured real, run bare, from V7's own HEAD at the
time this file was written:

```bash
$ bun run verify:zero-diff
Expand All @@ -227,6 +227,20 @@ policy/lib/data.json
error: script "verify:zero-diff" exited with code 1
```

**⚠️ That block is a record of a run, not an instruction — it no longer reproduces verbatim, and
the sentence above it used to imply it would.** Three things have moved, all of them after V7,
and none of them changes what the block is here to show. The script's failure message was
rewritten: it now opens *"the second host must cost zero changed lines in the Guardian, the AGT
bridge, or AGT itself — but these frozen paths changed:"* rather than *"R3.4 violated…"*. The
`base=` line cited below moved from line 15 to line 16. And later slices added entries under the
frozen paths, so the list is longer: run from this branch's HEAD the same command names
seventeen paths rather than thirteen, `hosts/claude-code/claude-code.hookmap.yaml` and
`mapping.yaml` among them. What still reproduces is the part this section is about — exit code
`1`, for the reason given next, and still not a defect of V7 or of any slice after it. Re-run it
and expect different wording and a longer list, never a pass. The block is left exactly as
captured rather than re-taken, because re-taking it would make it a record of a different run
under a heading about V7's.

Exit code `1`. The measured reason: `scripts/verify-zero-diff.sh:15` reads
`base="${1:-slice/v4}"`, so a bare invocation always diffs HEAD against `slice/v4` — the base
V5's own R3.4 proof was written against (R3.4: "Adding the second host requires zero new AGT
Expand Down
Loading
Loading