Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f062bb6
V2 scaffold: Envelope Inspector
afogel Aug 9, 2026
f485e1f
Plan V2 (Envelope Inspector), with its slices-doc amendments
afogel Aug 9, 2026
ce838ad
Add writeEnvelopeTap and S6's JSONL entry format (N26, S6)
afogel Aug 9, 2026
dc7e768
Fix: guard onError callback to maintain total-by-construction guarantee
afogel Aug 9, 2026
1bab5f8
Tap every ACS envelope crossing the Guardian's wire (N26, N20, S6)
afogel Aug 9, 2026
6e4d13e
Add the Inspector package and tailEnvelopeLog (N50)
afogel Aug 9, 2026
a6a40d0
Fix review findings: poll errors can't crash the process, timer start…
afogel Aug 9, 2026
4b93726
Add covering tests for the cross-poll buffer that round 1's fix stopp…
afogel Aug 9, 2026
fa30e82
Render the envelope stream and the decision badge (U20, U21)
afogel Aug 9, 2026
fb22f47
Add the Inspector CLI and the S6 round-trip contract test (U20, U21, …
afogel Aug 9, 2026
9b3f6c9
Gate the Inspector on R5.1/R5.2, add the V2 runbook and quickstart
afogel Aug 9, 2026
fa51e4a
Catch every throw out of dispatch, so no request can exit untapped
afogel Aug 9, 2026
73bc266
Stop a throwing malformed-line reporter from stranding parsed entries
afogel Aug 9, 2026
dd368d3
Widen the import gate, and stop all four gates passing vacuously
afogel Aug 9, 2026
8099e15
Say what S6 actually records, everywhere the tree overstated it
afogel Aug 9, 2026
81ac817
Guardian: redact absolute paths, stabilize test scratch dir, quiet st…
afogel Aug 10, 2026
5913ef6
Inspector: dim badge segments, pin two unspecified-but-real shapes, q…
afogel Aug 10, 2026
5acac30
Fix a path-fragile invariant filter, assert both sides of the tap con…
afogel Aug 10, 2026
25e756d
Name S6's two stringify-side exceptions; update the Verify count
afogel Aug 10, 2026
8f94f63
Guardian: make toRepoRelativeMessage total for any unknown, anchor th…
afogel Aug 10, 2026
b3b0826
Guardian: make toRepoRelativeMessage unconditionally total
afogel Aug 10, 2026
23486a5
Record why the Trace pillar isn't emittable from the v0.1.0 wire (D10)
afogel Aug 10, 2026
f4b8c45
Give V7 the Trace pillar (N49, U33), and close D10
afogel Aug 10, 2026
cdeb960
Keep V2's test doubles level with V1's module surface
afogel Aug 11, 2026
c4cc8f1
Name the envelope rail for its artifact, not for the writer's mechanism
afogel Aug 11, 2026
062dccf
Guardian barrel: governance verbs only, not S6's plumbing
afogel Aug 11, 2026
601789f
Tell the decision badge a decision, instead of handing it a log row
afogel Aug 11, 2026
5014c02
Stop the badge teaching a disposition ACS does not have, and make R5.…
afogel Aug 11, 2026
a714036
Point N26's docs at names that exist
afogel Aug 11, 2026
c2d026b
Stop N26's module doc describing docs it just corrected
afogel Aug 11, 2026
bc928dc
Correct V2's test count for V1's added coverage
afogel Aug 11, 2026
3a1bab4
Finish V2's rename, and stop calling an error a decision
afogel Aug 12, 2026
105aec4
Say what the code does now, not how it got here
afogel Aug 17, 2026
3da02a0
Drop the shaping identifiers from test names too
afogel Aug 17, 2026
2514816
Extend the same pass to the integration tests and the startup banner
afogel Aug 17, 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
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,27 @@ __pycache__/

# Subagent-driven-development scratch workspace (ledger, briefs, review packages)
.superpowers/

# S6, the ACS envelope log (N26). Local demo artifact; carries raw tool
# arguments unmodified, so it is never committed.
.acs/

# Scratch tree for packages/guardian/test/server.test.ts's outer-net test: an
# identical copy of packages/guardian/src, placed one directory deeper so its
# schema path resolves to nothing. The test removes it in a `finally`; this
# line only covers a run killed mid-test, so a stray copy of the Guardian's
# source can never be committed as if it were source. Also excluded from
# tsconfig.json, for the same reason: a killed run's leftover copy must not
# make `bun run typecheck` see a stray duplicate of the Guardian's own
# source. Fixed name (not mkdtemp's random one), so this one line covers it.
packages/guardian/tmp-schemaless-scratch/

# Same reasoning, two more scratch trees: server.test.ts's fake
# validate-envelope.ts test doubles (a non-string .message, and a throwing
# .message accessor), each alongside a real, unmodified copy of every other
# file in packages/guardian/src. Two separate directories, not one, because
# Bun's module cache keys by resolved path and these two fake sources differ
# from each other -- sharing one directory would silently hand the second
# test the first's already-loaded module.
packages/guardian/tmp-undefined-message-scratch/
packages/guardian/tmp-throwing-message-accessor-scratch/
64 changes: 52 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ One wire contract between agent hosts and policy runtimes, so governance integra

Today every policy vendor writes a module per agent, and every agent waits for a module per vendor. Microsoft's [Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit) ships four host packages with four different architectures — a Copilot CLI extension, subprocess hooks for Claude Code and Antigravity, an in-process plugin for OpenCode — and documents the capability divergence between them in its own READMEs.

This repository shows the other shape. A host implements [ACS](https://github.com/Agent-Control-Standard/ACS) once and is governable by any conformant runtime. A runtime implements ACS once and governs any conformant host. This slice (V1) wires one host — Claude Code — to AGT's policy engine running unforked, its stock Rego bundle deciding, entirely over the ACS wire.
This repository shows the other shape. A host implements [ACS](https://github.com/Agent-Control-Standard/ACS) once and is governable by any conformant runtime. A runtime implements ACS once and governs any conformant host. V1 wires one host — Claude Code — to AGT's policy engine running unforked, its stock Rego bundle deciding, entirely over the ACS wire. V2 makes that wire visible: every envelope crossing it is recorded to a log and rendered live by `bun run inspector`.

## What this proves

Expand All @@ -15,11 +15,19 @@ This repository shows the other shape. A host implements [ACS](https://github.co
| AGT's policy engine runs unforked, over the ACS wire | AGT's published policy library decides, used as shipped, at a pinned upstream commit, with no source changes (`agt.lock`, [`test/pin.test.ts`](test/pin.test.ts)) |
| The collapse is structural, not incidental | The host adapter contains no AGT-specific code and the AGT bridge contains no host-specific code — verifiable by reading the file list, and enforced by [`test/invariants.test.ts`](test/invariants.test.ts) |

**Delivered in V2** — the Envelope Inspector.

| Claim | How it is demonstrated |
|---|---|
| R5.1 — every hook firing is inspectable as an ACS envelope, in both directions, including envelopes that fail validation | The Guardian records every envelope crossing its wire to `.acs/envelopes.jsonl` before validation, and `bun run inspector` renders it live ([`test/envelope-log-sink-roundtrip.test.ts`](test/envelope-log-sink-roundtrip.test.ts), [`packages/guardian/test/envelope-log-sink-wiring.test.ts`](packages/guardian/test/envelope-log-sink-wiring.test.ts)) |
| R5.2 — an ACS-first reader can trace one action end to end without reading AGT source | The Inspector imports nothing from the Guardian or the AGT bridge and names neither AGT nor any host — enforced by two gates in [`test/invariants.test.ts`](test/invariants.test.ts) |

**Planned, not yet built** — the rest of the claim this project is working toward. None of the following exists yet, and there is no CI in this repository at all.

| Claim | Slice |
|---|---|
| AGT is completely expressible in ACS: a machine-checked mapping of all eight intervention points and five verdicts, with a round-trip conformance case per cell | V7 |
| A machine-checked mapping of all eight intervention points and five verdicts, with a round-trip conformance case per cell — every cell resolved, green where ACS v0.1.0 expresses AGT and red with a named reason where it does not. Four are already known red: the two model-call points have no v0.1.0 hook, and two attributes the Trace pillar marks required have no source on the wire | V7 |
| Which ACS profiles and pillars this implementation claims, and which it does not — the matrix is the declaration. Trace is a measured non-claim, not a silence | V7 |
| The same policy governs two structurally different coding agents, with the second host costing zero added AGT code | V5 |
| A scheduled harness run against AGT `main` catches upstream drift automatically | V8 |

Expand All @@ -38,7 +46,7 @@ The shaping doc is authoritative for requirements, shapes, and the breadboard. T
git clone --recurse-submodules https://github.com/afogel/ACS_reference_implementation
```

## Quickstart (R7.1 — one command on a laptop)
## Quickstart (R7.1 — starts with one command on a laptop)

Requires [`bun`](https://bun.sh) and the [Claude Code](https://docs.claude.com/en/docs/claude-code) CLI (`claude`) on your `PATH`.

Expand All @@ -58,41 +66,67 @@ This constructs the AGT bridge once, against the pinned stock policy bundle (`po

```
Guardian listening at http://localhost:8787/acs
Envelope log (S6): .acs/envelopes.jsonl
```

Leave it running. `hosts/claude-code/acs-hook.ts` defaults to exactly this URL; override with `ACS_GUARDIAN_URL` if it's listening elsewhere.

**3. Wire the hook into Claude Code.**
**3. Start the Envelope Inspector.** In a second terminal, after the Guardian (which is what creates `.acs/`):

```bash
bun run inspector
```

```
Envelope Inspector — tailing .acs/envelopes.jsonl
Ctrl-C to stop.
```

Every ACS envelope crossing the Guardian's wire is printed here as it happens — request and response, with a decision badge on responses:

```
── #2 20:44:33.130 ← RESPONSE steps/toolCallRequest id=e491180d-60a8-4982-b693-e63771c00e2d
● DENY reason_codes=[destructive_shell_command_blocked] policy_references=[agt_stock#destructive_shell_command_blocked]
```

`bun run inspector -- --from-start` replays a session already recorded. **`.acs/envelopes.jsonl` records each envelope the Guardian parsed, unmodified: nothing stripped, nothing redacted. So it carries raw tool arguments** — it is gitignored for that reason and never committed. Full walkthrough, with the real captured output for a deny, an allow, and a schema-invalid envelope: [`docs/demos/v2-runbook.md`](docs/demos/v2-runbook.md).

**4. Wire the hook into Claude Code.**

```bash
mkdir -p .claude
cp hosts/claude-code/settings.json .claude/settings.json
```

This registers `hosts/claude-code/acs-hook.ts` as a `PreToolUse` hook for the `Bash` tool — the "one hook" of this slice's name.
This registers `hosts/claude-code/acs-hook.ts` as a `PreToolUse` hook for the `Bash` tool — the "one hook" of V1's name.

**4. Run Claude Code with the hook.**
**5. Run Claude Code with the hook.**

```bash
claude
```

Ask it to run a destructive shell command, e.g. *"Use the Bash tool to run exactly this command: `rm -rf /`"*. The tool call is blocked, with the real policy-engine reasoning surfaced in the transcript — not a canned string, the actual text AGT's stock policy engine produces when it evaluates the pattern it matched. That pattern list is this project's own configuration (`policy/lib/data.json`), not something AGT ships — the stock bundle carries no shell/command patterns of its own, only generic PII regexes; what's stock is the *deciding module* (`agt.patterns`) and the priority chain that consults it, per R2.1 (zero Rego authored). See the framing note in [`docs/demos/v1-runbook.md`](docs/demos/v1-runbook.md) before narrating this demo. Ask for something harmless (`ls -la`) in the same session and it runs normally. Full walkthrough and what to watch for: [`docs/demos/v1-runbook.md`](docs/demos/v1-runbook.md).
Ask it to run a destructive shell command, e.g. *"Use the Bash tool to run exactly this command: `rm -rf /`"*. The tool call is blocked, with the real policy-engine reasoning surfaced in the transcript — not a canned string, the actual text AGT's stock policy engine produces when it evaluates the pattern it matched. That pattern list is this project's own configuration (`policy/lib/data.json`), not something AGT ships — the stock bundle carries no shell/command patterns of its own, only generic PII regexes; what's stock is the *deciding module* (`agt.patterns`) and the priority chain that consults it, per R2.1 (zero Rego authored). See the framing note in [`docs/demos/v1-runbook.md`](docs/demos/v1-runbook.md) before narrating this demo. Ask for something harmless (`ls -la`) in the same session and it runs normally. Full walkthrough and what to watch for: [`docs/demos/v1-runbook.md`](docs/demos/v1-runbook.md); with the Inspector running you also see both envelopes as they cross the wire.

Steps 1–2 were run against this exact tree to write this README: `bun install` completes clean, and `bun run guardian` prints the line above. Steps 3–4 were verified the same way the project's own tests verify them — piping a Claude Code–shaped `PreToolUse` payload on stdin straight into the hook shim against a running Guardian:
Watch the Inspector, not just the transcript, if the deny does not appear: the model may decline to issue the tool call at all on its own judgment, in which case no hook fires and the envelope log stays empty. And if you are scripting this rather than watching it, use `echo rm -rf /` as the payload — it matches the same pattern at offset 5 and is inert if it ever did execute, whereas an unattended `rm -rf /` is only safe for as long as the hook works, which is the thing under test.

**What was actually run against this tree to write this quickstart.** Steps 1–3 were run end to end: `bun install` completes clean, `bun run guardian` prints both lines above, and `bun run inspector` rendered every envelope quoted here — the badge line above is pasted from that run, not composed. Steps 4–5 were run twice, two different ways.

First, by piping a Claude Code–shaped `PreToolUse` payload on stdin straight into the hook shim against a running Guardian — the same way the project's own tests verify it. The shim never executes the command; it only asks the Guardian for a decision:

```bash
echo '{"session_id":"demo","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' \
| bun run hosts/claude-code/acs-hook.ts
# {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"matched pattern ... at offset 0"}}
# {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"matched pattern (?i)rm\\s+-[a-z]*r[a-z]*f[a-z]*\\s+/(?:\\s|$) at offset 0"}}
```

which is exactly the JSON Claude Code's own hook protocol sends and expects back; running `claude` interactively for step 4 exercises the identical path through the real CLI.
Second, through the real `claude` CLI with `.claude/settings.json` installed — Claude Code spawning `acs-hook.ts` as an actual `PreToolUse` subprocess and honouring the decision. That run was **headless** (`claude -p '<prompt>' --allowedTools Bash`), not an interactive TUI session, and the payload was `echo rm -rf /` rather than `rm -rf /`: the configured pattern matches the raw command string with no argv parse, so it is denied by the same rule at offset 5 while being inert if it ever did execute. An unattended `rm -rf /` is only safe for as long as the hook works, which is the thing under test. The interactive TUI session was not run, so nothing here describes how the TUI renders the block. See [`docs/demos/v2-runbook.md`](docs/demos/v2-runbook.md) for the full captured output of both.

### Verify

```bash
bun test # 97 tests across 12 files (96 pass, 1 skip), including the R3.2/R3.3 gates below
bun test # 169 tests across 17 files (168 pass, 1 skip), including the R3.2/R3.3
# and R5.1/R5.2 gates below
# the skip is the byte-identity check, which needs UPSTREAM_BUNDLE — see verify:pin
bun run typecheck # whole-workspace strict TypeScript check, zero errors
```
Expand All @@ -101,7 +135,13 @@ bun run typecheck # whole-workspace strict TypeScript check, zero errors

## Status

V1 ("one host, one hook") is implemented: a Claude Code `PreToolUse` hook, a Guardian process serving ACS over HTTP, and AGT's unforked stock policy bundle deciding behind it — see the quickstart above and [`slices/v1/README.md`](slices/v1/README.md). R3.2 and R3.3 (no AGT vocabulary in the host adapter, no host *output* vocabulary in it either, and no host vocabulary in the AGT bridge) are enforced by [`test/invariants.test.ts`](test/invariants.test.ts), not left to inspection. Slices V2–V8 are shaped and sliced but not started; they are tracked as issues on the project board, each with a stacked pull request.
V1 ("one host, one hook") is implemented: a Claude Code `PreToolUse` hook, a Guardian process serving ACS over HTTP, and AGT's unforked stock policy bundle deciding behind it — see the quickstart above and [`slices/v1/README.md`](slices/v1/README.md).

V2 ("Envelope Inspector") is implemented: the Guardian records every ACS envelope crossing its wire to `.acs/envelopes.jsonl`, and `bun run inspector` tails and renders it live — see [`slices/v2/README.md`](slices/v2/README.md) and [`docs/demos/v2-runbook.md`](docs/demos/v2-runbook.md). One boundary worth stating up front: a schema-invalid envelope surfaces as a JSON-RPC **error**, not a `deny` decision. `N27 denyOnInvalidEnvelope()`, which turns Guardian-side failures into honoured ACS decisions, is V3.

Four of this project's architectural claims are enforced by [`test/invariants.test.ts`](test/invariants.test.ts) rather than left to inspection: R3.2 and R3.3 (no AGT vocabulary in the host adapter, no host *output* vocabulary in it either, and no host vocabulary in the AGT bridge), and R5.1 and R5.2 (the Inspector imports nothing from the Guardian or the AGT bridge, and names neither AGT nor any host).

Slices V3–V8 are shaped and sliced but not started; they are tracked as issues on the project board, each with a stacked pull request.

## License

Expand Down
6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading