Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a967b8d
V1 scaffold: One host, one hook, a real AGT decision
afogel Aug 9, 2026
b0746a2
V1 plan, with ten corrections amended into the shaped docs
afogel Aug 9, 2026
04007c0
Task 1: workspace, vendored AGT bundle, and the pin
afogel Aug 9, 2026
cbe24b8
Fix: verify-pin.sh cleanup must not fall back to rm -rf
afogel Aug 9, 2026
774fb98
Add the AGT bridge: stateless snapshot-to-verdict decision function
afogel Aug 9, 2026
0572d31
Amend: bundle: resolves against the manifest's own directory
afogel Aug 9, 2026
465ea37
Add mapping.yaml and mapVerdict: AGT verdict to ACS decision
afogel Aug 9, 2026
ccebb52
Add assembleSnapshot: envelope to AGT pre_tool_call snapshot
afogel Aug 9, 2026
0e73fd6
Add validateEnvelope: ACS envelope schema validation via Ajv
afogel Aug 9, 2026
6fbba1d
Add the Guardian JSON-RPC endpoint and handshake responder
afogel Aug 9, 2026
6f82389
Add buildEnvelope (N2) and the Claude Code hookmap (S1)
afogel Aug 9, 2026
603882b
Add guardianClient.post, handshake, renderDecision, session config st…
afogel Aug 9, 2026
826754d
Add acs-hook.ts shim, Claude Code hook wiring, and the V1 demo runboo…
afogel Aug 9, 2026
243bcdf
Task 10: architectural invariant gates, workspace typecheck, quicksta…
afogel Aug 9, 2026
45a9a2f
Fix wave: close Guardian fail-open, README overclaims, hookmap bugs, …
afogel Aug 9, 2026
b9d54c6
Correct the README test count after the fix wave
afogel Aug 9, 2026
2d4fb82
Record two cross-slice findings V1 execution surfaced
afogel Aug 9, 2026
104f29f
Pin the shim's exact stdout for every decision the hookmap declares
afogel Aug 11, 2026
03a4cd1
Name the validated envelope for what it is: an ACS request of any method
afogel Aug 11, 2026
5a374fa
Resolve the AGT intervention point from mapping.yaml instead of hardc…
afogel Aug 11, 2026
6d559d6
One noun for the ACS decision message on both sides of the seam
afogel Aug 11, 2026
921356e
Move Claude Code's output field names out of the shared adapter
afogel Aug 11, 2026
3a9f6b4
Give the Guardian client a role, and answer with a decision-or-failure
afogel Aug 11, 2026
a75ddaa
Name each half of the handshake once, and check the config instead of…
afogel Aug 11, 2026
c872d11
Depend on a bridge role, and make the snapshot a message
afogel Aug 11, 2026
7d7fa1e
Name V1's affordances what V1's code actually calls them
afogel Aug 11, 2026
067fcbd
Fix the last V1 rename left behind in live prose
afogel Aug 11, 2026
d66512f
Cover the three V1 fixes that landed with less test than they need
afogel Aug 11, 2026
14765af
Read the wrap mode the mapping declares, instead of one written in code
afogel Aug 12, 2026
b032dea
Ask for a decision in ACS's nouns, not the transport's
afogel Aug 12, 2026
ed274c7
Answer with the verdict, keep the snapshot named, and name the result
afogel Aug 12, 2026
a72c718
Say what the code does now, not how it got here
afogel Aug 17, 2026
55b1d56
Drop the shaping identifiers from test names too
afogel Aug 17, 2026
46394c7
Extend the same pass to the host shim and the integration tests
afogel Aug 17, 2026
176c181
Drop a fix-wave citation a reader cannot resolve
afogel Aug 17, 2026
e2639ca
Bind the Guardian to loopback, not to every interface
afogel Aug 21, 2026
44edf98
Correlate the decision to the request, not just the transport
afogel Aug 21, 2026
546be94
Pin Ajv exactly, since Ajv is what decides envelope validity
afogel Aug 21, 2026
6786fbf
Give a bypass somewhere private to go, and own the paths it would hid…
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
23 changes: 23 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Review is required where a silent change is a policy bypass rather than a
# regression. That is a narrower set than "the whole repo": these are the paths
# where a diff nobody read can make a governed tool call proceed ungoverned,
# and nothing here will fail a test to tell you.
#
# Not a claim that everything else is unimportant — it is a claim that
# everything else fails loudly.

# The two sides of the wire. The Guardian decides; the adapter is the only
# thing a host shim is allowed to trust about that decision.
/packages/guardian/ @afogel
/packages/host-adapter/ @afogel

# Host shims. Each one is a process whose exit code an agent reads as
# permission, so its failure modes are the fail-open surface.
/hosts/ @afogel

# The policy bundle itself, the verdict-to-ACS translation table, and the
# upstream pin. A change to any of these changes what is allowed without
# changing a line of TypeScript.
/policy/ @afogel
/mapping.yaml @afogel
/agt.lock @afogel
43 changes: 43 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Dependency updates for a repository whose decision path runs through third
# party code: Ajv decides whether an ACS envelope is schema-valid, and AGT's
# published policy library decides everything after that. A silent drift there
# is a change in what this project allows.
#
# Ecosystem is `bun`, not `npm`. Dependabot separated the two: `bun` is its own
# ecosystem, keyed on the text-based `bun.lock` (the legacy binary `bun.lockb`
# is unsupported), requiring bun >= 1.1.39 — this repo uses 1.3.x and the
# text-based lockfile. Pointing `npm` at this tree would read `package.json`,
# find no npm or yarn lockfile, and open PRs that move a declared range while
# leaving `bun.lock` behind — the two would then disagree about what is
# installed, which is the exact failure the exact pins in
# packages/guardian/package.json exist to prevent.
#
# One caveat worth knowing rather than discovering: Dependabot supports
# *version* updates for bun but not *security* updates. A published CVE in Ajv
# will not open its own PR here; it arrives with the ordinary weekly version
# bump, or not at all.
#
# The github-actions entry is deliberately ahead of its subject: this
# repository has no workflows yet (V8 adds the drift harness). It is a no-op
# until then, and live the day the first workflow lands, which is better than
# remembering to add it that day.
version: 2
updates:
- package-ecosystem: "bun"
# Listed explicitly rather than relying on workspace traversal from the
# root manifest. `bun.lock` is at the root, but the dependency that matters
# most — Ajv — is declared in packages/guardian, and `directories` (which,
# unlike `directory`, takes globs) covers both without assuming how
# Dependabot resolves a workspace.
directories:
- "/"
- "/packages/*"
- "/hosts/*"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ build/
*.log
.venv/
__pycache__/

# Subagent-driven-development scratch workspace (ledger, briefs, review packages)
.superpowers/
81 changes: 75 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ 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. AGT's policy engine runs unforked, with its stock Rego bundle deciding, across two structurally different coding agents — and adding the second host costs zero AGT code.
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.

## What this proves

**Delivered in V1** — true of this tree today; verifiable by running the commands in Quickstart below.

| Claim | How it is demonstrated |
|---|---|
| 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 |
| Interop is real | AGT's published policy library decides, used as shipped, at a pinned upstream commit, with no source changes |
| The collapse is structural | The host adapter contains no AGT-specific code and the AGT bridge contains no host-specific code — verifiable by reading the file list |
| It stays true | The same harness runs against AGT `main` on a schedule, so upstream drift surfaces as a named failing case |
| 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) |

**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 |
| 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 |

## Layout

Expand All @@ -30,9 +38,70 @@ 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)

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

**1. Install.**

```bash
bun install
```

**2. Start the Guardian.** In its own terminal, from the repo root:

```bash
bun run guardian
```

This constructs the AGT bridge once, against the pinned stock policy bundle (`policy/lib`, per `agt.lock`), and serves ACS's `POST /acs` JSON-RPC endpoint:

```
Guardian listening at http://localhost:8787/acs
```

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.**

```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.

**4. 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).

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:

```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"}}
```

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.

### Verify

```bash
bun test # 97 tests across 12 files (96 pass, 1 skip), including the R3.2/R3.3 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
```

`bun run verify:pin` additionally re-clones AGT at the pinned ref and byte-diffs the vendored bundle against it (R2.2/R2.3) — it needs network access to GitHub, so it isn't part of the offline quickstart above.

## Status

Shaped and sliced; implementation has not started. Slices V1–V8 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). 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.

## License

Expand Down
64 changes: 64 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Security Policy

## Supported versions

None, and that is not a placeholder. This repository has no tagged release, no
published package, and no CI. `main` is the only thing that exists, and it is a
reference implementation under construction — the README's own "Planned, not
yet built" table is the honest inventory.

So there is no support matrix and no backport path: a fix lands on `main` or it
does not exist. Anything here that is running in front of real traffic is doing
so ahead of every claim this project makes.

## What a vulnerability looks like here

This is a governance tool, so the interesting failures are not the usual ones.
The bug that matters is a **bypass**: any way to make a tool call proceed when
the policy would have stopped it, or to make a host act on a decision the
policy runtime never issued.

Concretely, reports are wanted for:

- A host shim (`hosts/`) that can be made to fail in a way its agent reads as
"the hook never fired", so the tool call proceeds ungoverned.
- A Guardian (`packages/guardian/`) that can be induced to allow, or to answer
one request with another request's decision.
- Anything that reaches the Guardian's socket and changes an outcome. The ACS
wire is unauthenticated at every slice in this tree — the endpoint binds
loopback precisely because reachability is the only access control it has, so
a way around that bind is in scope.
- A crafted envelope, policy bundle, or `mapping.yaml` that turns a `deny` into
anything else, including an error the host treats as permission.

Out of scope, because they are known and written down rather than hidden: the
absence of wire authentication, described in the header of
`packages/guardian/src/server.ts`, and the fail-open on an unreachable Guardian
at the slices that have not built the posture negotiation yet, described in
`docs/demos/v1-runbook.md`. A way to *trigger* either one against a host that
believes it is governed is very much in scope.

## Reporting

Report privately, through GitHub's private vulnerability reporting on this
repository:

**<https://github.com/afogel/ACS_reference_implementation/security/advisories/new>**

Please do not open a public issue, discussion, or pull request for a bypass.
A public report is a working recipe for evading a policy engine, and it reaches
every reader before it reaches a fix.

Include what you have of:

- The shortest sequence that reproduces it — ideally the hook payload or JSON-RPC
envelope, verbatim, and the commit you ran it against.
- Which side you believe is at fault: the host shim, the Guardian, the mapping,
or the policy bundle.
- What the correct outcome would have been, and what happened instead.
- Whether it needs anything beyond reaching the Guardian's port — a particular
policy, a particular host, a race, local file access.

A report with a reproducer and no analysis is more useful than analysis with no
reproducer. Expect an acknowledgement rather than a schedule: there is no
release process here to promise a fix window against.
7 changes: 7 additions & 0 deletions agt.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"agt_repo": "https://github.com/microsoft/agent-governance-toolkit",
"agt_ref": "81955d48025c6b11deb3fc9dabf89f74f4145775",
"bundle_path": "policy-engine/policy/lib",
"sdk_package": "agent-control-specification",
"sdk_version": "0.3.1-beta.0"
}
Loading