approval-md 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +584 -553
- package/SPEC.md +42 -13
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +119 -5
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +173 -51
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +186 -80
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +155 -5
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +294 -7
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +4 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +176 -8
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +170 -0
- package/dist/src/core/agents-md.d.ts +276 -0
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +420 -0
- package/dist/src/core/attest.js +13 -1
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +43 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +44 -6
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +68 -13
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +2 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +278 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +31 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +15 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +20 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +514 -35
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +9 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +389 -36
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +12 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +2 -1
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/policy.schema.json +21 -1
- package/templates/codex/README.md +9 -0
package/README.md
CHANGED
|
@@ -2,132 +2,181 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/approval-md/approval.md/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
**
|
|
6
|
-
|
|
7
|
-
Your agent is about to send the email, spend the money, delete the folder, or
|
|
8
|
-
publish the post. A bad diff is revertible, so coding agents have a safety net.
|
|
9
|
-
Once an agent leaves the repository that net disappears: a sent message has no
|
|
10
|
-
revert, and the action carries your name.
|
|
11
|
-
|
|
12
|
-
The permissions section in an AGENTS.md file is prose: two lists, one headed
|
|
13
|
-
"allowed without prompting" and one headed "require approval first", written for
|
|
14
|
-
an agent trusted to obey them. Nothing checks. approval.md is the layer that
|
|
15
|
-
checks:
|
|
16
|
-
|
|
17
|
-
- **A policy file you wrote.** `APPROVAL.md` is human-authored markdown at the
|
|
18
|
-
root of your project, declaring which classes of side effect an agent may take
|
|
19
|
-
on its own, which need you, and under what budgets.
|
|
20
|
-
- **The approve button on your phone.** A request arrives over Telegram (the
|
|
21
|
-
reference channel) carrying what the runtime computed, what the agent claimed,
|
|
22
|
-
and the exact bytes about to leave. You tap Approve or Reject.
|
|
23
|
-
- **A single-use execution token**, minted at one site in the codebase, only as a
|
|
24
|
-
human decision is recorded, spent once, stored nowhere. Adapters holding real
|
|
25
|
-
credentials answer to nothing else.
|
|
26
|
-
- **A log that cannot be quietly rewritten.** Every proposal, decision, and
|
|
27
|
-
execution is an append-only, hash-chained JSONL record, and `approval log
|
|
28
|
-
verify` answers for the chain.
|
|
29
|
-
|
|
30
|
-
Not everything is worth a tap: a class declared `supervised` runs immediately,
|
|
31
|
-
and a policy-declared fraction of those runs is sampled for your retrospective
|
|
32
|
-
review, using a secret the agent cannot read.
|
|
33
|
-
|
|
34
|
-
Spec site: https://approval.md · Specification: [SPEC.md](SPEC.md)
|
|
5
|
+
**A harness-agnostic, open-source framework for approving agent actions with a
|
|
6
|
+
human in the loop.**
|
|
35
7
|
|
|
36
|
-
|
|
8
|
+
Your agent is about to send the email, push to main, spend the money, delete
|
|
9
|
+
the folder, or publish the post. A bad diff can be reverted. A sent message
|
|
10
|
+
cannot, and it carries your name.
|
|
37
11
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
The honest posture, from [SPEC.md](SPEC.md) section 11: this is an oversight
|
|
61
|
-
layer for broadly cooperative agents, with hard enforcement at the adapter
|
|
62
|
-
boundaries that hold the credentials. Identity in v0.1 is config-declared, so
|
|
63
|
-
the trust boundary is the machine rather than cryptography.
|
|
64
|
-
["Can't the agent just go around it?"](#cant-the-agent-just-go-around-it) works
|
|
65
|
-
through each evasion and says where the boundary actually is.
|
|
66
|
-
|
|
67
|
-
The design mantra is **files are the interface, the log is the truth, the
|
|
68
|
-
database is a cache**. Routing, gating, budget math, and chain verification are
|
|
69
|
-
deterministic code. Models propose, and the runtime decides.
|
|
70
|
-
|
|
71
|
-
## Install
|
|
12
|
+
approval.md puts a button between the agent and that action. You write a
|
|
13
|
+
short policy file saying which kinds of action need you. The agent runs freely
|
|
14
|
+
inside those lines. When it reaches one, a message arrives on your phone with
|
|
15
|
+
exactly what is about to happen, and nothing happens until you tap.
|
|
16
|
+
|
|
17
|
+
Two things people use it for first:
|
|
18
|
+
|
|
19
|
+
- **Signing off an email.** The agent drafts, you read the recipients, subject
|
|
20
|
+
and body on your phone, you tap Approve, and the adapter sends it once with a
|
|
21
|
+
credential the agent never held.
|
|
22
|
+
- **Watching a coding agent.** A hook classifies every command Claude Code or
|
|
23
|
+
Cursor runs. Reads and edits go through; `git push origin main`, `npm
|
|
24
|
+
install`, `curl -d`, `rm -rf` come to your phone first, and every decision is
|
|
25
|
+
in a log you can verify.
|
|
26
|
+
|
|
27
|
+
Spec site: https://approval.md · Specification: [SPEC.md](SPEC.md) · Package:
|
|
28
|
+
`approval-md` on npm.
|
|
29
|
+
|
|
30
|
+
## Five minutes to a working gate
|
|
31
|
+
|
|
32
|
+
**1. Install.** No source checkout is required for the published CLI.
|
|
72
33
|
|
|
73
34
|
```sh
|
|
74
35
|
npm install -g approval-md
|
|
75
36
|
```
|
|
76
37
|
|
|
77
|
-
|
|
78
|
-
|
|
38
|
+
**2. Make a gate.** For the published 0.1.0 package, run `approval init`, edit
|
|
39
|
+
and read `APPROVAL.md`, run `approval setup identity`, optionally run
|
|
40
|
+
`approval setup channel telegram`, then run `approval policy attest --as human:<id>`.
|
|
79
41
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
42
|
+
The upcoming `quickstart` command combines these steps. It is currently
|
|
43
|
+
available from a source checkout (`npm ci` and `npm run build`), pending a
|
|
44
|
+
package release. Run the three-question ceremony in the project directory.
|
|
45
|
+
It asks who you are, whether decisions appear in this terminal or on Telegram,
|
|
46
|
+
and which five class families always ask. It shows the exact policy and requires
|
|
47
|
+
the typed word `understood` before attesting it.
|
|
83
48
|
|
|
84
49
|
```sh
|
|
85
|
-
|
|
86
|
-
approval init # APPROVAL.md, .approval/log/, QUEUE.md, .gitignore
|
|
87
|
-
approval setup identity # writes where APPROVAL_HUMAN comes from
|
|
88
|
-
eval "$(approval env)" # put the resolved variables in this shell
|
|
89
|
-
approval policy attest # a human signs for these exact policy bytes
|
|
90
|
-
approval doctor # can this machine run the system at all?
|
|
50
|
+
node /path/to/approval.md/cli.js quickstart
|
|
91
51
|
```
|
|
92
52
|
|
|
53
|
+
Then run the `activate:` command quickstart prints. It includes the absolute
|
|
54
|
+
project directory, so it resolves this instance even if the next shell starts
|
|
55
|
+
somewhere else.
|
|
56
|
+
|
|
93
57
|
```
|
|
94
|
-
|
|
58
|
+
ready: 5 selected class families ask human:yourname on cli; other classified reversible actions use the autonomous default
|
|
95
59
|
```
|
|
96
60
|
|
|
97
|
-
|
|
98
|
-
|
|
61
|
+
The gate is operative. `.approval/env` remains inert until you run the explicit
|
|
62
|
+
`eval` line. Protected controls, failed policy loads, irreversible declarations,
|
|
63
|
+
and commands the classifier cannot read still take their stricter paths.
|
|
99
64
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
65
|
+
**3. Run the local service.** If you chose Telegram, message **@BotFather**
|
|
66
|
+
with `/newbot` before quickstart so you have the token it asks for. After setup
|
|
67
|
+
and human attestation, start the full runtime from this policy project's directory:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
cd /path/to/your/project
|
|
71
|
+
eval "$(approval env)" # explicitly load this instance's environment
|
|
72
|
+
approval up # daemon and configured channels, one foreground process
|
|
106
73
|
```
|
|
107
74
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
75
|
+
For a source checkout, use `node /path/to/approval.md/cli.js env` inside the
|
|
76
|
+
`eval` line and `node /path/to/approval.md/cli.js up` to start the service.
|
|
77
|
+
Leave the service running. Requests use your configured channel; Telegram
|
|
78
|
+
requests reach your phone. `up` does not load `.approval/env` itself. An already
|
|
79
|
+
exported approval variable wins over the environment map, so start with a clean
|
|
80
|
+
shell or unset another instance's approval variables before evaluating it.
|
|
81
|
+
|
|
82
|
+
Use `approval up` for normal operation. `approval channel telegram listen` runs
|
|
83
|
+
only the Telegram component, for focused use or diagnosis. Never run both
|
|
84
|
+
against the same bot, or run two instances polling that bot: Telegram returns
|
|
85
|
+
HTTP 409. Stop the polling runtime before rerunning `approval setup channel
|
|
86
|
+
telegram`, then reload the environment and start `up` again.
|
|
87
|
+
|
|
88
|
+
By default the daemon scans `backlog/tasks/`. If your envelopes live elsewhere,
|
|
89
|
+
use `approval up --tasks /path/to/existing/task-folder`. The scan reads `.md`
|
|
90
|
+
files directly inside that folder, without descending into subdirectories.
|
|
91
|
+
Creating an empty default folder does not monitor envelopes stored elsewhere.
|
|
92
|
+
A missing default folder warns about envelope drift coverage; TTL sweeping,
|
|
93
|
+
queue rendering and configured channels can still run. See [runtime startup
|
|
94
|
+
checks](docs/cli-reference.md#up) for the draw socket and optional web channel.
|
|
95
|
+
|
|
96
|
+
**4. Pick your first experience.**
|
|
97
|
+
|
|
98
|
+
- *A coding agent*: [gate your coding agent](#gate-your-coding-agent) is two
|
|
99
|
+
more steps, a classification you can try immediately and a hook you paste
|
|
100
|
+
into `.claude/settings.json`.
|
|
101
|
+
- *An email*: [hand a grant to a real credential](#hand-a-grant-to-a-real-credential)
|
|
102
|
+
adds an SMTP or AgentMail credential to the vault, and
|
|
103
|
+
[examples/email-demo.md](examples/email-demo.md) walks the whole send.
|
|
104
|
+
|
|
105
|
+
When something does not work, `approval doctor` prints one line per check with
|
|
106
|
+
a `fix:` line under each failure. It is described under [Running the
|
|
107
|
+
checks](#running-the-checks), and it is not a step you need on the way in.
|
|
108
|
+
|
|
109
|
+
## What it is made of
|
|
110
|
+
|
|
111
|
+
- **A policy file you wrote.** `APPROVAL.md` is markdown at the root of your
|
|
112
|
+
project with one YAML block declaring which classes of side effect an agent
|
|
113
|
+
may take on its own, which need you, and under what budgets. A human signs
|
|
114
|
+
for its exact bytes; an edit makes it inoperative until someone signs again.
|
|
115
|
+
- **A message on your phone.** A request arrives over Telegram carrying what
|
|
116
|
+
the runtime computed, what the agent claimed, and the exact bytes about to
|
|
117
|
+
leave. You tap Approve or Reject. A local web page and the terminal are the
|
|
118
|
+
other two channels.
|
|
119
|
+
- **A single-use execution token.** Minted at one place in the code, only as a
|
|
120
|
+
human decision is recorded, spent once, stored nowhere. Manual and selected
|
|
121
|
+
live executions require it. An attested class rule may instead explicitly
|
|
122
|
+
authorize an irreversible supervised or autonomous execution.
|
|
123
|
+
- **A log nobody can quietly rewrite.** Every proposal, decision and execution
|
|
124
|
+
is an append-only, hash-chained JSONL record. `approval log verify` answers
|
|
125
|
+
for the chain.
|
|
126
|
+
|
|
127
|
+
Not everything is worth a tap. A class declared `supervised` runs at once, and
|
|
128
|
+
a fraction of those runs is sampled for your retrospective review using a
|
|
129
|
+
secret the agent cannot read, so you see one in a hundred `gh pr merge` calls
|
|
130
|
+
rather than all of them.
|
|
131
|
+
|
|
132
|
+
The design rule is **files are the interface, the log is the truth, the
|
|
133
|
+
database is a cache**. Routing, gating, budget math and chain verification are
|
|
134
|
+
deterministic code. Models propose; the runtime decides.
|
|
135
|
+
|
|
136
|
+
## How the gate holds
|
|
137
|
+
|
|
138
|
+
- **Credentials live in an encrypted vault**, never in the policy file and never
|
|
139
|
+
in the agent's environment. `APPROVAL.md` carries the *name* of an environment
|
|
140
|
+
variable, and there is no `approval vault get`.
|
|
141
|
+
- **Adapters answer only inside a verified execution.** Manual and selected-live
|
|
142
|
+
executions present and consume a valid token. An irreversible supervised or
|
|
143
|
+
autonomous execution must be explicitly enabled by its attested class rule.
|
|
144
|
+
The adapter opens the credential window only after the runtime authorizes the
|
|
145
|
+
declared action, then closes it as soon as the adapter returns.
|
|
146
|
+
- **Tokens are minted at one site**, in the path that records a human decision,
|
|
147
|
+
and the log holds only their SHA-256. A second spend is refused
|
|
148
|
+
`token-consumed`.
|
|
149
|
+
- **The log makes tampering evident.** Each record chains to the previous one.
|
|
150
|
+
Projections rebuild from the log and never write back.
|
|
151
|
+
- **The harness hook covers the direct-shell path.** `approval hook claude-code`
|
|
152
|
+
classifies the commands a coding agent runs on its own and answers allow or
|
|
153
|
+
deny, fail-closed.
|
|
154
|
+
- **The escape hatch is a recorded ceremony.** When the gate itself is broken, a
|
|
155
|
+
human opens a time-boxed window with `approval gate open`: a terminal, a
|
|
156
|
+
required `--reason`, and the typed word `understood`. Every call it lets
|
|
157
|
+
through is logged as `gate.bypassed`, human-only classes stay refused, and
|
|
158
|
+
`approval status` reports unhealthy until it closes
|
|
159
|
+
([docs/cli-reference.md#gate](docs/cli-reference.md#gate)).
|
|
160
|
+
|
|
161
|
+
This is an oversight layer for broadly cooperative agents, with hard
|
|
162
|
+
enforcement at the adapter boundaries that hold the credentials (SPEC.md
|
|
163
|
+
section 11). Identity in v0.1 is config-declared, so the trust boundary is the
|
|
164
|
+
machine rather than cryptography. ["Can't the agent just go around
|
|
165
|
+
it?"](#cant-the-agent-just-go-around-it) works through each evasion.
|
|
120
166
|
|
|
121
167
|
## Gate your coding agent
|
|
122
168
|
|
|
123
169
|
`approval run` gates the commands an agent hands to the runtime. It cannot gate
|
|
124
170
|
the ones the harness runs directly, and those are most of them. Two surfaces
|
|
125
|
-
close that gap
|
|
126
|
-
harness that speaks MCP
|
|
127
|
-
|
|
171
|
+
close that gap: a PreToolUse hook for Claude Code and an MCP server for any
|
|
172
|
+
harness that speaks MCP. Both resolve against the same policy and append to the
|
|
173
|
+
same log as the CLI.
|
|
174
|
+
|
|
175
|
+
Codex support is opt-in while native compatibility and everyday activation are
|
|
176
|
+
still being verified. See the bounded [Codex hook operator
|
|
177
|
+
runbook](docs/codex-hook.md) before installing or trusting it.
|
|
128
178
|
|
|
129
|
-
**1. See how a command classifies.** This touches nothing
|
|
130
|
-
way to understand a verdict.
|
|
179
|
+
**1. See how a command classifies.** This touches nothing.
|
|
131
180
|
|
|
132
181
|
```
|
|
133
182
|
$ approval hook classify -- npm install left-pad
|
|
@@ -137,23 +186,12 @@ deps.add npm-install-package npm install left-pad
|
|
|
137
186
|
classes: deps.add
|
|
138
187
|
```
|
|
139
188
|
|
|
140
|
-
Every segment of a command line is classified and the command takes the union,
|
|
141
|
-
`git status && curl -d
|
|
189
|
+
Every segment of a command line is classified and the command takes the union,
|
|
190
|
+
so `git status && curl -d …` is gated as `network.call`.
|
|
142
191
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
scratch root the agent made itself; everything not provably scratch keeps the old
|
|
147
|
-
class. `vcs.remote.meta` (APRV-268) is exactly three `gh` forms against the
|
|
148
|
-
checkout's own origin, `gh api graphql`, `gh pr update-branch` and `gh run
|
|
149
|
-
rerun`, split out of `network.call` because asking a forge about the repository
|
|
150
|
-
it already tracks is not the send that `network.call` exists for. Any flag
|
|
151
|
-
pointing `gh` at another repository or another host falls back to today's class,
|
|
152
|
-
since the classifier is pure and cannot resolve `origin`.
|
|
153
|
-
|
|
154
|
-
**2. Install the hook.** It lives in `.claude/settings.json`, and a human commits
|
|
155
|
-
that file: an agent that could write its own hook entry could write itself out
|
|
156
|
-
of it.
|
|
192
|
+
**2. Install the hook.** It lives in `.claude/settings.json`, and a human
|
|
193
|
+
commits that file: an agent that could write its own hook entry could write
|
|
194
|
+
itself out of it.
|
|
157
195
|
|
|
158
196
|
```json
|
|
159
197
|
{ "hooks": { "PreToolUse": [ {
|
|
@@ -163,29 +201,28 @@ of it.
|
|
|
163
201
|
} ] } }
|
|
164
202
|
```
|
|
165
203
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
`
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
**5. Or connect the MCP server
|
|
187
|
-
|
|
188
|
-
`approval instructions --schemas` prints.
|
|
204
|
+
Register the same command under `PostToolUse` as well, without `--timeout`, so
|
|
205
|
+
the runtime learns how each command ended. `--dir` resolves the policy and the
|
|
206
|
+
log together, so a session inside a linked worktree still writes to the one
|
|
207
|
+
log. Keep `--timeout` (how long the hook waits for you) below `timeout` (Claude
|
|
208
|
+
Code's cap on the process).
|
|
209
|
+
|
|
210
|
+
**3. Watch a verdict.** An `autonomous` class allows and logs nothing. A
|
|
211
|
+
`supervised` class allows and records the action for sampling. A `manual` class
|
|
212
|
+
waits for your tap. Anything the classifier cannot read denies. There is no
|
|
213
|
+
"ask" answer: a decision taken outside the log is a decision nothing can audit.
|
|
214
|
+
The deny reason is `<code>: <detail>`, and the codes are frozen
|
|
215
|
+
(`hook-unclassified`, `hook-opaque`, `hook-rejected`, `hook-timeout` and the
|
|
216
|
+
rest in [docs/claude-code-hook.md](docs/claude-code-hook.md)).
|
|
217
|
+
|
|
218
|
+
**4. Know the sharp edges.** The hook never creates a log: pointed at a path
|
|
219
|
+
with no log it denies `hook-log-unreachable` rather than forking a second
|
|
220
|
+
chain. A wait that runs out keeps its question open for a short grace and then
|
|
221
|
+
withdraws it, so nobody is pinged about a question whose asker has left. A hook
|
|
222
|
+
grant mints no token: the harness runs the command itself.
|
|
223
|
+
|
|
224
|
+
**5. Or connect the MCP server.** `approval mcp serve` is a stdio server
|
|
225
|
+
publishing the agent's verbs as tools.
|
|
189
226
|
|
|
190
227
|
```sh
|
|
191
228
|
claude mcp add approval -- \
|
|
@@ -194,46 +231,37 @@ claude mcp add approval -- \
|
|
|
194
231
|
--dir /path/to/project
|
|
195
232
|
```
|
|
196
233
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
`
|
|
200
|
-
|
|
201
|
-
the
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`--as` is deleted from every published input schema, so a tool call cannot name
|
|
205
|
-
an actor. Provoke `unknown tool "grant"` once, deliberately, so you have seen it.
|
|
206
|
-
Walkthrough: [examples/mcp-demo.md](examples/mcp-demo.md).
|
|
207
|
-
|
|
208
|
-
A harness that can simply run commands needs neither surface: `request`, `wait`,
|
|
209
|
-
`run` is how sessions in this repository take manual-class actions
|
|
210
|
-
([docs/dogfood-cutover.md](docs/dogfood-cutover.md)). The task-file side of that
|
|
211
|
-
flow, on a Backlog.md board with a policy of its own, is the worked example in
|
|
212
|
-
[examples/backlog-md-project/README.md](examples/backlog-md-project/README.md):
|
|
213
|
-
one envelope on one task file, then `register`, `request`, `wait`, `run`, with
|
|
214
|
-
what each prints. There is no Backlog.md adapter, and the example says why.
|
|
215
|
-
|
|
216
|
-
## Put approvals on your phone
|
|
217
|
-
|
|
218
|
-
**1. Create a bot and let setup do the rest.** Message **@BotFather** with
|
|
219
|
-
`/newbot`, then:
|
|
234
|
+
`register`, `request`, `wait`, `run`, `queue`, `status` and the rest of the
|
|
235
|
+
agent's surface are there. `grant`, `reject`, `revoke`, `policy attest` and
|
|
236
|
+
`vault set` are not: an MCP client is the agent's harness, and a `grant` tool
|
|
237
|
+
on it would hand the agent the overseer's pen. Grant never travels over MCP,
|
|
238
|
+
and neither does the token it mints. The identity is fixed at startup and
|
|
239
|
+
`--as` is removed from every published schema, so a tool call cannot name an
|
|
240
|
+
actor. Walkthrough: [examples/mcp-demo.md](examples/mcp-demo.md).
|
|
220
241
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
242
|
+
A harness that can run commands needs neither surface: `request`, `wait`, `run`
|
|
243
|
+
is how sessions in this repository take manual-class actions
|
|
244
|
+
([docs/dogfood-cutover.md](docs/dogfood-cutover.md)). The task-file side of
|
|
245
|
+
that flow, on a Backlog.md board, is
|
|
246
|
+
[examples/backlog-md-project/README.md](examples/backlog-md-project/README.md).
|
|
226
247
|
|
|
227
|
-
|
|
228
|
-
the OS keystore (macOS Keychain, or `secret-tool` on Linux) and the file records
|
|
229
|
-
only where it lives. It is interactive by refusal (a pipe or `--json` exits 2 and
|
|
230
|
-
prints the non-interactive commands), because a setup a CI job could drive would
|
|
231
|
-
be a way for a CI job to declare a human identity. `approval env` is the only
|
|
232
|
-
command that reads that file, and evaluating it is a step a human takes. Full
|
|
233
|
-
walkthrough: [examples/telegram-demo.md](examples/telegram-demo.md).
|
|
248
|
+
## Put approvals on your phone
|
|
234
249
|
|
|
235
|
-
**
|
|
236
|
-
|
|
250
|
+
**1. Setup writes the environment map, not the secrets.** `approval setup
|
|
251
|
+
channel telegram` puts the bot token in the OS keystore (macOS Keychain, or
|
|
252
|
+
`secret-tool` on Linux) and records in `.approval/env` only where it lives. The
|
|
253
|
+
verbs are interactive by refusal: a pipe or `--json` exits 2 and prints the
|
|
254
|
+
non-interactive commands, because a setup a CI job could drive would let a CI
|
|
255
|
+
job declare a human identity. `approval env` is the only command that reads
|
|
256
|
+
that file, and evaluating it is a step a human takes. Stop any `approval up`
|
|
257
|
+
process or standalone listener polling this bot before setup, because setup
|
|
258
|
+
also polls to discover the chat. After setup, from this project's directory run
|
|
259
|
+
`eval "$(approval env)"` and `approval up`. Full walkthrough:
|
|
260
|
+
[examples/telegram-demo.md](examples/telegram-demo.md).
|
|
261
|
+
|
|
262
|
+
**2. A request binds to exact bytes.** The payload lives in a file, the
|
|
263
|
+
envelope declares its `payload_hash`, and `--payload` supplies the bytes at
|
|
264
|
+
request time:
|
|
237
265
|
|
|
238
266
|
```sh
|
|
239
267
|
approval payload hash payload.json # the binding the envelope declares
|
|
@@ -246,47 +274,39 @@ registered task-demo at seq 2: 1 action(s)
|
|
|
246
274
|
requested task-demo task-demo:chaser at seq 3 (manual)
|
|
247
275
|
```
|
|
248
276
|
|
|
249
|
-
Material that hashes to something else is refused `payload-mismatch`, and
|
|
250
|
-
is stored
|
|
277
|
+
Material that hashes to something else is refused `payload-mismatch`, and
|
|
278
|
+
nothing is stored or appended. Class, cost and reversibility come from the
|
|
251
279
|
registered envelope rather than from flags, so an agent cannot rename its own
|
|
252
|
-
class between registering and asking. An approval is about specific bytes,
|
|
253
|
-
about a description of them.
|
|
254
|
-
|
|
255
|
-
**3.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
280
|
+
class between registering and asking. An approval is about specific bytes,
|
|
281
|
+
never about a description of them.
|
|
282
|
+
|
|
283
|
+
**3. The runtime delivers it.** `approval up` prints `notified
|
|
284
|
+
task-demo:chaser (message 501)` and your phone has it. That one foreground
|
|
285
|
+
process is the daemon loop (envelope drift, expiry, queue regeneration,
|
|
286
|
+
retrospective sampling) plus every channel the policy configures. A channel
|
|
287
|
+
whose credential is unset is not started and says so; a channel that falls over
|
|
288
|
+
is restarted with backoff while the loop keeps ticking. `approval setup service`
|
|
289
|
+
writes the launchd or systemd user unit that starts it at login, and prints the
|
|
290
|
+
whole unit for you to read first.
|
|
291
|
+
|
|
292
|
+
The message shows the action key, a **COMPUTED** block the runtime derived
|
|
293
|
+
(class, task, binding, budget verdicts, chain head), a **CLAIMED** block naming
|
|
294
|
+
the agent and marked unverified, the **FULL PAYLOAD**, and two buttons. It says
|
|
295
|
+
how long an answer still has: `waiting: requested 4 min ago · expires 13:07
|
|
296
|
+
UTC`, or, for a request a process is blocked on, `requester waits until 13:07
|
|
297
|
+
UTC`. An email-shaped payload is rendered field by field with real line
|
|
298
|
+
breaks, with the canonical JSON and its hash underneath. Detection is
|
|
299
|
+
structural and never reads a self-declared type. Agent-authored text is
|
|
300
|
+
HTML-escaped, so markup stays inert.
|
|
301
|
+
|
|
302
|
+
An optional gloss, a one-line plain-English reading of the payload by a model,
|
|
303
|
+
can sit above the computed block. It is marked unverified, it never changes a
|
|
304
|
+
verdict, and a failed gloss is omitted while delivery continues:
|
|
269
305
|
|
|
270
306
|
```sh
|
|
271
|
-
approval up --gloss-provider codex --gloss-model gpt-5.4-mini
|
|
307
|
+
approval up --gloss-provider codex --gloss-model gpt-5.4-mini # or the default, claude/haiku
|
|
272
308
|
```
|
|
273
309
|
|
|
274
|
-
Claude with model `haiku` remains the default. The runtime never falls back
|
|
275
|
-
between providers; a failed gloss is omitted and approval delivery continues.
|
|
276
|
-
The message shows the action key, a **COMPUTED** block the runtime derived (class,
|
|
277
|
-
task, state, binding, budget verdicts, chain head), a **CLAIMED** block naming the
|
|
278
|
-
agent and marked unverified, the **FULL PAYLOAD**, and two buttons. It also says
|
|
279
|
-
how long an answer still has: `waiting: requested 4 min ago · expires 13:07 UTC
|
|
280
|
-
(clock)`, or, for a request some process is blocked on, `requester waits until
|
|
281
|
-
13:07 UTC`, the deadline that actually applies to you.
|
|
282
|
-
|
|
283
|
-
A payload the runtime recognises by shape is laid out so a human can read it: an
|
|
284
|
-
email-shaped payload (recipients, subject, body) is rendered field by field with
|
|
285
|
-
real line breaks, and the canonical JSON and its bound hash sit underneath
|
|
286
|
-
unchanged. Detection is structural and never reads a self-declared type, because
|
|
287
|
-
a field the requesting agent authored must not choose its own presentation.
|
|
288
|
-
Agent-authored text is HTML-escaped, so markup stays inert.
|
|
289
|
-
|
|
290
310
|
**4. Tap Approve.** The prompt rewrites itself in place. The buttons go away and
|
|
291
311
|
the text becomes the outcome:
|
|
292
312
|
|
|
@@ -297,16 +317,16 @@ task-demo:chaser
|
|
|
297
317
|
by human:alice at 10:20 UTC (seq 4)
|
|
298
318
|
```
|
|
299
319
|
|
|
300
|
-
One edit
|
|
301
|
-
|
|
302
|
-
revocations, expiries and withdrawals settle the same way
|
|
303
|
-
|
|
304
|
-
poll cycle, and a tap on a stale button is answered with a toast and records
|
|
305
|
-
nothing.
|
|
320
|
+
One edit carries the annotation and the disarming together, so there is no
|
|
321
|
+
moment when the message reads "approved" and still offers a tap. Rejections,
|
|
322
|
+
revocations, expiries and withdrawals settle the same way, and a tap on a stale
|
|
323
|
+
button records nothing.
|
|
306
324
|
|
|
307
|
-
**5.
|
|
308
|
-
|
|
309
|
-
|
|
325
|
+
**5. The token stays off the chat.** The grant mints a single-use execution
|
|
326
|
+
token. With `defaults.token_delivery: sealed` the requesting process opens it
|
|
327
|
+
itself and no human ever sees it, which is how this repository releases. With
|
|
328
|
+
the default `manual` delivery it is printed once, in a panel, at the surface
|
|
329
|
+
that recorded the decision:
|
|
310
330
|
|
|
311
331
|
```
|
|
312
332
|
granted task-demo:chaser at seq 4 by human:alice
|
|
@@ -317,30 +337,27 @@ granted task-demo:chaser at seq 4 by human:alice
|
|
|
317
337
|
─────────────────────────────────────────────────────────────
|
|
318
338
|
```
|
|
319
339
|
|
|
320
|
-
For a tap on your phone
|
|
321
|
-
runtime, and its last line reads `not sent to Telegram
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
at. In both cases the log holds only the token's SHA-256, it never appears in a
|
|
328
|
-
URL, and nothing can recover it. Lose it, revoke the grant, and request again.
|
|
340
|
+
For a tap on your phone that panel appears on the terminal running the
|
|
341
|
+
runtime, and its last line reads `not sent to Telegram`: a chat transcript
|
|
342
|
+
lives on servers you do not control, so a credential does not go there. The
|
|
343
|
+
local **web** channel shows the token once in the response page for the grant
|
|
344
|
+
that minted it, served over loopback, gone on reload, because there the browser
|
|
345
|
+
is already the surface you are looking at. In every case the log holds only the
|
|
346
|
+
token's SHA-256. Lose it, revoke the grant, and request again.
|
|
329
347
|
|
|
330
348
|
**6. Spend it.** `approval run <action> --token "$TOKEN" -- <command>` appends
|
|
331
|
-
`execution.started` before spawning the child and
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
twice and it refuses:
|
|
349
|
+
`execution.started` before spawning the child and `execution.completed` after,
|
|
350
|
+
and exits with the child's own exit code, so it composes with `make`, CI and
|
|
351
|
+
`&&`. Run it before the approval and it refuses `token-required` at exit 5.
|
|
352
|
+
Run it twice and it refuses:
|
|
336
353
|
|
|
337
354
|
```
|
|
338
355
|
✗ token-consumed action task-demo:chaser already executed: execution.started at seq 5 spent this token. A token is single-use and the log is the proof.
|
|
339
356
|
```
|
|
340
357
|
|
|
341
|
-
A request is not owed an answer forever
|
|
342
|
-
|
|
343
|
-
--withdraw-on-timeout` does it
|
|
358
|
+
A request is not owed an answer forever. `approval withdraw` lets the party
|
|
359
|
+
that opened one take it back while it is pending, and `approval wait
|
|
360
|
+
--withdraw-on-timeout` does it when your own wait elapses.
|
|
344
361
|
|
|
345
362
|
**7. Read the whole story.** Two actors, one clean chain:
|
|
346
363
|
|
|
@@ -353,16 +370,30 @@ party that opened one take it back while it is pending, and `approval wait
|
|
|
353
370
|
6 2026-08-19T19:04:41.499Z execution.completed agent:drafter task-demo
|
|
354
371
|
```
|
|
355
372
|
|
|
356
|
-
That is `approval log tail
|
|
357
|
-
a terminal
|
|
358
|
-
|
|
373
|
+
That is `approval log tail`, tab-separated for `cut` when piped, aligned and
|
|
374
|
+
coloured on a terminal. `approval log verify` answers for the chain: `clean: 6
|
|
375
|
+
record(s), head seq 6 843705c6bbea…`.
|
|
376
|
+
|
|
377
|
+
Downstream services can follow the same channel-independent record with
|
|
378
|
+
`approval log follow --from <seq> --cursor-hash <hash> --json`. The sequence is
|
|
379
|
+
exclusive and the hash binds the resume point to the prefix already consumed.
|
|
380
|
+
Each JSON line is emitted only after a complete chain verification. Delivery
|
|
381
|
+
across reconnects is at least once: apply an idempotent effect, then persist the
|
|
382
|
+
event's `seq` and `hash`. See [the CLI reference](docs/cli-reference.md#log-follow)
|
|
383
|
+
for failure behavior, resource costs, and the weaker sequence-only bootstrap.
|
|
384
|
+
|
|
385
|
+
**8. Review what ran without you.** Supervised actions the sampler picks arrive
|
|
386
|
+
on the same chat as review cards, after the fact: what ran, when, and that the
|
|
387
|
+
runtime allowed it unasked. ✅ records that you looked, 🛑 twice records a
|
|
388
|
+
denial and opens a reconciliation obligation, and 👎 😐 👍 ❤️ leave a graded
|
|
389
|
+
reaction. `approval audit list` and `approval audit review` are the same
|
|
390
|
+
backlog at the terminal.
|
|
359
391
|
|
|
360
392
|
## The other half of the word
|
|
361
393
|
|
|
362
|
-
Everything above is control
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
what you love, like and dislike in the work, what you want from an agent as
|
|
394
|
+
Everything above is control. The file carries your voice too. Below the policy
|
|
395
|
+
block, `APPROVAL.md` may hold one optional `yaml approval-values` block: what
|
|
396
|
+
you love, like and dislike in the work, what you want from an agent as
|
|
366
397
|
behaviour, and how you read and answer.
|
|
367
398
|
|
|
368
399
|
```sh
|
|
@@ -371,26 +402,24 @@ approval feedback # the reactions and notes humans left on this log's actions
|
|
|
371
402
|
```
|
|
372
403
|
|
|
373
404
|
A retrospective review or a grant can carry a graded reaction (`disliked`,
|
|
374
|
-
`indifferent`, `liked`, `loved`; the two extremes need a note), and
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
"What I value" heading into a `wants` list for you to grade.
|
|
405
|
+
`indifferent`, `liked`, `loved`; the two extremes need a note), and `approval
|
|
406
|
+
feedback` reads them back to the agent whose work they were about. Both verbs
|
|
407
|
+
print human-authored guidance behind a banner that says so, and neither reaches
|
|
408
|
+
enforcement: no verdict, sample, budget or token moves because of them (SPEC.md
|
|
409
|
+
section 11.1, invariant 10). They mirror `approval journal write`, the agent's
|
|
410
|
+
outlet the gate does not stand in front of. `approval import agents-md` drafts
|
|
411
|
+
the block from a "What I value" heading in an AGENTS.md.
|
|
382
412
|
|
|
383
413
|
## Define what needs approval
|
|
384
414
|
|
|
385
415
|
A policy is a fenced `yaml approval-policy` block inside a markdown file named
|
|
386
416
|
`APPROVAL.md`. The prose around the block is for you; the runtime parses the
|
|
387
|
-
block and ignores the rest.
|
|
388
|
-
for is text you read.
|
|
417
|
+
block and ignores the rest. The thing you sign for is text you read.
|
|
389
418
|
|
|
390
419
|
**1. Name the classes.** A class is a dotted path from the side-effect taxonomy
|
|
391
420
|
of SPEC.md section 7 (`communicate.email.external`, `financial.spend`,
|
|
392
|
-
`public.post`, `data.delete`, `read.*`). Matching is most-specific-first, `*`
|
|
393
|
-
a single-segment wildcard, a trailing `.*` matches any depth, and at equal
|
|
421
|
+
`public.post`, `data.delete`, `read.*`). Matching is most-specific-first, `*`
|
|
422
|
+
is a single-segment wildcard, a trailing `.*` matches any depth, and at equal
|
|
394
423
|
specificity the strictest rule wins.
|
|
395
424
|
|
|
396
425
|
**2. Pick an autonomy for each.** Six values, strictest first: `human-only` (a
|
|
@@ -399,76 +428,66 @@ an agent with `class-human-only`), `manual` (a human decides before execution),
|
|
|
399
428
|
`supervised-live` (a policy-declared fraction blocks on the gate exactly as
|
|
400
429
|
`manual` does, and the rest proceed, so the rule carries a `live_rate`),
|
|
401
430
|
`supervised-retro` (executes immediately, a sampled fraction escalated for
|
|
402
|
-
retrospective review), `supervised` (
|
|
403
|
-
`
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
431
|
+
retrospective review), `supervised` (an alias of `supervised-retro`), and
|
|
432
|
+
`autonomous` (executes freely). A truthful `reversible: false` declaration
|
|
433
|
+
normally engages section 7's manual floor. An operator who deliberately accepts
|
|
434
|
+
irreversible execution for one nonmanual class can add
|
|
435
|
+
`allow_irreversible: true` to that class rule. Every equally most-specific rule
|
|
436
|
+
must opt in, and the edit has no effect until the policy is re-attested.
|
|
408
437
|
|
|
409
438
|
**3. Set the budgets.** Class `limits` and the `budgets` scopes are conjunctive,
|
|
410
439
|
so an action must pass both, and consumption is computed from the log over
|
|
411
|
-
rolling windows rather than from a mutable counter. An action whose class
|
|
412
|
-
no rule takes `defaults.autonomy`, and a policy that does not parse
|
|
413
|
-
class to `manual`: unattested and unparseable are both strict
|
|
440
|
+
rolling windows rather than from a mutable counter. An action whose class
|
|
441
|
+
matches no rule takes `defaults.autonomy`, and a policy that does not parse
|
|
442
|
+
resolves every class to `manual`: unattested and unparseable are both strict.
|
|
414
443
|
|
|
415
444
|
**4. Widen the protected paths.** `APPROVAL.md`, the agent instruction files,
|
|
416
|
-
`.approval/`, the harness settings and the release configuration are protected
|
|
417
|
-
the runtime whatever a policy says. `protected_paths` adds repo-relative
|
|
418
|
-
(an exact file, `SPEC.md`, or a directory prefix, `design/`), so a
|
|
419
|
-
its own governing documents behind the gate
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
An entry can also be an object, `{path, class}`,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
`policy.edit.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
organs. A route aimed at a built-in protected path must land at least as strictly
|
|
433
|
-
as the `policy.edit` line itself, and a policy that breaks that floor is refused
|
|
434
|
-
at load with `protected-route-floor`.
|
|
445
|
+
`.approval/`, the harness settings and the release configuration are protected
|
|
446
|
+
by the runtime whatever a policy says. `protected_paths` adds repo-relative
|
|
447
|
+
literals (an exact file, `SPEC.md`, or a directory prefix, `design/`), so a
|
|
448
|
+
project can put its own governing documents behind the same gate. The key can
|
|
449
|
+
only widen, and globs are a schema violation.
|
|
450
|
+
|
|
451
|
+
An entry can also be an object, `{path, class}`, routing that path family to a
|
|
452
|
+
named `policy.edit` sub-class with its own autonomy and live rate. Four names
|
|
453
|
+
are reserved: `policy.edit.spec` (the governing specification),
|
|
454
|
+
`policy.edit.harness` (agent instruction files and harness configuration),
|
|
455
|
+
`policy.edit.ci` (continuous-integration and release configuration),
|
|
456
|
+
`policy.edit.design` (design documents and decision records). Any other
|
|
457
|
+
lowercase word may be minted beside them, and nothing outside `policy.edit` may
|
|
458
|
+
be named: a route to `policy.core` or `log.mutate` is refused. A route aimed at
|
|
459
|
+
a built-in protected path must land at least as strictly as the `policy.edit`
|
|
460
|
+
line itself, or the policy is refused at load with `protected-route-floor`.
|
|
435
461
|
|
|
436
462
|
**5. Attest it.** `approval policy attest` is what makes a policy operative. An
|
|
437
|
-
attestation records that a human saw these exact bytes,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
463
|
+
attestation records that a human saw these exact bytes, as their SHA-256. Edit
|
|
464
|
+
`APPROVAL.md` afterwards and every gated operation refuses `hash-mismatch`
|
|
465
|
+
until you attest again. Attestation is human-only, and identity in v0.1 is
|
|
466
|
+
config-declared, so what one proves is that someone with local control signed
|
|
467
|
+
off.
|
|
442
468
|
|
|
443
469
|
**6. Amend it with the verb, not by hand.** Changing a policy is two facts that
|
|
444
470
|
have to land together, the new bytes and a human's attestation of them, and
|
|
445
|
-
`approval policy amend` owns the
|
|
446
|
-
`--require-load` refuses to attest a policy that does not load, `--commit`
|
|
447
|
-
the two files as one commit). It prints a
|
|
448
|
-
approver changes, defaults, limits) rather
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
actually parses. Attesting one that does not is still allowed, since attestation
|
|
453
|
-
records bytes and not correctness, but such a policy fails closed to all-manual.
|
|
471
|
+
`approval policy amend` owns the ceremony (`--dry-run` reports only,
|
|
472
|
+
`--require-load` refuses to attest a policy that does not load, `--commit`
|
|
473
|
+
lands the two files as one commit and opens the pull request). It prints a
|
|
474
|
+
semantic diff (class resolutions, approver changes, defaults, limits) rather
|
|
475
|
+
than a text diff, then a load advisory saying whether the edited policy parses.
|
|
476
|
+
Attesting one that does not parse is allowed, since attestation records bytes
|
|
477
|
+
rather than correctness, and such a policy fails closed to all-manual.
|
|
454
478
|
|
|
455
479
|
### Why this verb exists: seq 2
|
|
456
480
|
|
|
457
481
|
Read this repository's own log. At **seq 2** a policy amendment was attested at
|
|
458
|
-
11:56:07. It was **superseded** seven minutes later, at seq 3
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
This account originally said eleven minutes. The log says seven, and the log
|
|
464
|
-
won: the figure was corrected against the chain after being misremembered, which
|
|
465
|
-
is the whole thesis of keeping one.
|
|
482
|
+
11:56:07. It was **superseded** seven minutes later, at seq 3, because the edit
|
|
483
|
+
broke a pinned assertion and nobody found out until the test suite ran against
|
|
484
|
+
it. The operator attested bytes whose consequences had never been shown to
|
|
485
|
+
them. (This account originally said eleven minutes. The log says seven, and
|
|
486
|
+
the log won.)
|
|
466
487
|
|
|
467
|
-
That is the failure the load advisory is for. Had `approval policy amend`
|
|
468
|
-
that morning, the load failure would have been on screen while the
|
|
469
|
-
deciding, and `--require-load` would have refused to attest at all.
|
|
470
|
-
is cited by number on purpose: it is in the log, it is checkable, and the log is
|
|
471
|
-
the truth.
|
|
488
|
+
That is the failure the load advisory is for. Had `approval policy amend`
|
|
489
|
+
existed that morning, the load failure would have been on screen while the
|
|
490
|
+
human was deciding, and `--require-load` would have refused to attest at all.
|
|
472
491
|
|
|
473
492
|
## Hand a grant to a real credential
|
|
474
493
|
|
|
@@ -485,43 +504,46 @@ approval adapter email task-042:chaser --token "$TOKEN" \
|
|
|
485
504
|
--payload message.json --as agent:claude-admin
|
|
486
505
|
```
|
|
487
506
|
|
|
488
|
-
**1.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
507
|
+
**1. Two stores.** `.approval/env` says where the values that unlock the
|
|
508
|
+
machine come from, and `approval setup vault` writes the passphrase line under
|
|
509
|
+
whatever name `vault.passphrase_env` declares. The SMTP password is an adapter
|
|
510
|
+
credential, so it goes in the vault, where a gated adapter spends it inside a
|
|
511
|
+
verified execution window.
|
|
493
512
|
|
|
494
513
|
**2. Setup fills the vault and proves it.** `approval setup adapter email` reads
|
|
495
514
|
the credential manifest the adapter declares, then probes the server without
|
|
496
|
-
sending anything
|
|
515
|
+
sending anything.
|
|
497
516
|
|
|
498
517
|
**3. A credential's only journey is into an adapter.** `approval vault set`
|
|
499
|
-
stores one credential in `.approval/vault.enc`, encrypted under a passphrase
|
|
500
|
-
policy names and never carries. The value comes from stdin or `--value-env
|
|
518
|
+
stores one credential in `.approval/vault.enc`, encrypted under a passphrase
|
|
519
|
+
the policy names and never carries. The value comes from stdin or `--value-env
|
|
501
520
|
<VAR>`; there is no `--value` flag, because a secret on a command line is a
|
|
502
|
-
secret in the shell history
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
**4. The send happens inside the
|
|
506
|
-
|
|
507
|
-
appends `execution.started`, opens the vault, reads the
|
|
508
|
-
|
|
509
|
-
`execution.completed`.
|
|
510
|
-
|
|
511
|
-
|
|
521
|
+
secret in the shell history. There is no `approval vault get`; `approval vault
|
|
522
|
+
list` shows the names.
|
|
523
|
+
|
|
524
|
+
**4. The send happens inside the execution window.** `approval adapter email`
|
|
525
|
+
re-hashes `message.json` against the declaration or grant binding, applies the
|
|
526
|
+
attested policy, appends `execution.started`, opens the vault, reads the SMTP
|
|
527
|
+
settings, sends over STARTTLS, closes the window, and appends
|
|
528
|
+
`execution.completed`. Manual and selected-live paths verify and spend the
|
|
529
|
+
grant token; an explicitly opted-in supervised or autonomous path has no grant
|
|
530
|
+
and mints no token. On that no-token path, the vault passphrase must already be
|
|
531
|
+
in the adapter process environment. The `.approval/env` fallback remains
|
|
532
|
+
token-only. The credential exists for one send and appears in no event, output
|
|
533
|
+
or error message.
|
|
512
534
|
|
|
513
535
|
**5. Check two properties in your own mailbox.** The bytes that left are the
|
|
514
536
|
bytes you approved, since the hash the token spend verified is the hash of the
|
|
515
537
|
payload your phone displayed. And the `Message-ID` is derived from the action
|
|
516
|
-
key, the payload hash and the sender, so the header in a mailbox and the
|
|
517
|
-
in the chain identify each other months later.
|
|
538
|
+
key, the payload hash and the sender, so the header in a mailbox and the
|
|
539
|
+
binding in the chain identify each other months later.
|
|
518
540
|
|
|
519
541
|
### The same grant over AgentMail
|
|
520
542
|
|
|
521
|
-
`communicate.email.external` has a second adapter. Where the email adapter
|
|
522
|
-
an SMTP session, `approval adapter agentmail` calls the AgentMail API, and
|
|
523
|
-
mail
|
|
524
|
-
|
|
543
|
+
`communicate.email.external` has a second adapter. Where the email adapter
|
|
544
|
+
opens an SMTP session, `approval adapter agentmail` calls the AgentMail API, and
|
|
545
|
+
a mail the agent has already composed as a Draft leaves only when a grant says
|
|
546
|
+
so. Walkthrough: [examples/agentmail-demo.md](examples/agentmail-demo.md).
|
|
525
547
|
|
|
526
548
|
```sh
|
|
527
549
|
approval setup adapter agentmail # inbox id + sending key, into the vault
|
|
@@ -534,22 +556,65 @@ approval adapter agentmail task-042:chaser --token "$TOKEN" \
|
|
|
534
556
|
per-permission booleans, and `draft_create`, `draft_update` and `draft_read` are
|
|
535
557
|
separate from `draft_send` and `message_send`. Give the agent a key holding the
|
|
536
558
|
first three and none of the last two, and put a key holding the send permissions
|
|
537
|
-
in the vault
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
environment is a full bypass of the gate, which is why `AGENTMAIL_` is withheld
|
|
542
|
-
from every child `approval run` spawns.
|
|
559
|
+
in the vault. The agent composes all day and cannot send at all: an ungated
|
|
560
|
+
send is refused by AgentMail itself, `agentmail-unauthorized`, before this
|
|
561
|
+
runtime is involved. `AGENTMAIL_` is withheld from every child `approval run`
|
|
562
|
+
spawns, so a key in the agent's environment cannot ride into a command.
|
|
543
563
|
|
|
544
564
|
**A draft is mutable, so the grant binds its bytes.** `approval payload
|
|
545
565
|
agentmail-draft` snapshots the draft's recipients, subject and text at request
|
|
546
|
-
time, and that snapshot is what the
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
566
|
+
time, and that snapshot is what the hash binds and what your phone displays.
|
|
567
|
+
Before it sends, the adapter re-fetches the draft and compares; a draft edited
|
|
568
|
+
after the grant refuses `agentmail-draft-drifted`, sends nothing, and names
|
|
569
|
+
which fields differ without quoting text nobody approved. That comparison runs
|
|
570
|
+
before the token is spent, so the refusal costs no authority: restore the
|
|
571
|
+
approved text and the same token still sends.
|
|
572
|
+
|
|
573
|
+
### First-class zzz.bot messages
|
|
574
|
+
|
|
575
|
+
`approval adapter zzz` creates a thread or replies through zzz.bot's versioned
|
|
576
|
+
HTTP API. Put the invited write credential in the vault, then approve the
|
|
577
|
+
complete tagged payload. The environment, destination, body, metadata, tags and
|
|
578
|
+
references all sit inside the payload hash.
|
|
579
|
+
|
|
580
|
+
This adapter is available from a source checkout containing APRV-320 until the
|
|
581
|
+
next approval.md package release. The published npm `approval-md@0.1.0`
|
|
582
|
+
predates it, and this change does not publish a package.
|
|
583
|
+
|
|
584
|
+
```sh
|
|
585
|
+
approval setup adapter zzz
|
|
586
|
+
approval adapter zzz task-320:announce --token "$TOKEN" \
|
|
587
|
+
--payload zzz-message.json --as agent:codex
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Thread payload:
|
|
591
|
+
|
|
592
|
+
```json
|
|
593
|
+
{"environment":"production","operation":"create_thread",
|
|
594
|
+
"room_id":"<room-id-from-GET-api-v1-rooms>",
|
|
595
|
+
"title":"Release ready","body":"The verified build is ready for review.",
|
|
596
|
+
"tags":["release"],"references":[]}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
A reply uses `"operation":"create_reply"` and `"thread_id"` instead of
|
|
600
|
+
`room_id` and `title`. The adapter chooses only fixed production or preview
|
|
601
|
+
origins, rejects redirects, and derives zzz.bot's idempotency key from the
|
|
602
|
+
approval action key and payload hash.
|
|
603
|
+
|
|
604
|
+
Public writes require an invited credential with write scope. Private writes
|
|
605
|
+
also require active room membership and accepted, unexpired approval.md workflow
|
|
606
|
+
evidence. The setup probe performs one authenticated room-list GET. It proves
|
|
607
|
+
that zzz.bot accepts the credential and does not prove those write or private
|
|
608
|
+
room prerequisites. A local non-guest MCP server exposes the same adapter verb,
|
|
609
|
+
but MCP use is voluntary; custody is enforced only when the write credential is
|
|
610
|
+
kept solely in the approval.md vault.
|
|
611
|
+
|
|
612
|
+
### Build a third-party adapter
|
|
613
|
+
|
|
614
|
+
Adapter authors can import the supported ESM API from `approval-md/adapters`.
|
|
615
|
+
It exposes the shared execution contract, conformance runner, vault credential
|
|
616
|
+
provider, refusal unions, and TypeScript types without making internal package
|
|
617
|
+
paths public. See the [adapter API guide](docs/adapter-api.md).
|
|
553
618
|
|
|
554
619
|
## The APPROVAL.md dictionary
|
|
555
620
|
|
|
@@ -574,6 +639,7 @@ believed was in force. Full semantics: SPEC.md section 5.
|
|
|
574
639
|
| `classes.<pattern>.autonomy` | Required on every class rule, so it has no default. Six levels, strictest first: `human-only`, `manual`, `supervised-live`, `supervised-retro`, `autonomous`, and `supervised`, which is the pre-split spelling and an alias of `supervised-retro` (§5.2, APRV-127, APRV-185). |
|
|
575
640
|
| `classes.<pattern>.live_rate` | The fraction of a `supervised-live` class that blocks on the gate, in (0, 1]. Required there and refused everywhere else, so it has no default: a live mode with no fraction declares a control without saying how much of it runs. Selection is HMAC-SHA-256 over the payload hash under the operator's secret (§5.2, APRV-127). |
|
|
576
641
|
| `classes.<pattern>.retro_rate` | This class's retrospective sampling rate, in (0, 1], overriding `audit.supervised_sample_rate` for it alone. Optional on `supervised`, `supervised-retro` and `supervised-live`, refused on the rest. Absent means the global rate (§5.2, APRV-183). |
|
|
642
|
+
| `classes.<pattern>.allow_irreversible` | Explicit operator permission for a truthful `reversible: false` action to retain this rule's `autonomous` or supervised behavior. Optional boolean; absent or `false` preserves the manual floor. `true` is refused on `manual` and `human-only`, cannot appear in `defaults`, and takes effect only when every equally most-specific matching rule says `true` (§5.2, §7, APRV-317). |
|
|
577
643
|
| `classes.<pattern>.approvers` | Approver ids permitted to decide this class. Absent restricts nobody, since the list is a narrowing and a narrowing nobody wrote narrows nothing; a named list refuses everyone else with `actor-not-approver` (§5.1). |
|
|
578
644
|
| `classes.<pattern>.limits` | Per-class ceilings, every value a positive number: `per_action_usd`, `daily_usd`, and the request-volume counts `max_pending` and `requests_per_hour`. Absent means this class carries no ceiling of its own (§5.1, §5.2). |
|
|
579
645
|
| `budgets.global.daily_usd` | Repo-wide spend ceiling per rolling day, computed from the log. Absent means no spend ceiling (§5.1). |
|
|
@@ -600,27 +666,26 @@ believed was in force. Full semantics: SPEC.md section 5.
|
|
|
600
666
|
|
|
601
667
|
Every key ending in `_env` carries a variable's *name* and never its value:
|
|
602
668
|
agents may read `APPROVAL.md`, so a secret it carried would be a secret they
|
|
603
|
-
hold. Where those values live is recorded in `.approval/env`, which a single
|
|
604
|
-
reads, `approval env`, whose output is an export block a human evaluates.
|
|
669
|
+
hold. Where those values live is recorded in `.approval/env`, which a single
|
|
670
|
+
verb reads, `approval env`, whose output is an export block a human evaluates.
|
|
605
671
|
|
|
606
672
|
## How this compares
|
|
607
673
|
|
|
608
674
|
Three kinds of thing already exist in this space, and each solves a different
|
|
609
|
-
part of the problem.
|
|
610
|
-
Bountify.ai; it is optional, and nothing in the format depends on it
|
|
611
|
-
[GOVERNANCE.md](GOVERNANCE.md).
|
|
675
|
+
part of the problem. A hosted daemon and reviewer layer is operated by
|
|
676
|
+
Bountify.ai; it is optional, and nothing in the format depends on it
|
|
677
|
+
([GOVERNANCE.md](GOVERNANCE.md)).
|
|
612
678
|
|
|
613
679
|
**Harness-native permission prompts** (Claude Code permission rules and hooks,
|
|
614
680
|
Cursor auto-run, Codex CLI approval modes) enforce inside the one harness they
|
|
615
681
|
ship with. That enforcement is real: a Claude Code PreToolUse deny holds even
|
|
616
|
-
under its bypass mode, and Codex backs its gate with an OS-level sandbox,
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
the session, and it lands in a verifiable log.
|
|
682
|
+
under its bypass mode, and Codex backs its gate with an OS-level sandbox, which
|
|
683
|
+
this project does not attempt. What they lack is a durable record and
|
|
684
|
+
portability. None writes an append-only log of what was asked, who decided and
|
|
685
|
+
what ran; the decision reaches a human only as a terminal prompt; and the
|
|
686
|
+
mechanism does not travel to another harness. approval.md's Claude Code hook is
|
|
687
|
+
built on that PreToolUse mechanism and adds the two missing pieces: the
|
|
688
|
+
decision comes from an attested policy file, and it lands in a verifiable log.
|
|
624
689
|
|
|
625
690
|
**AGENTS.md permissions prose** states the policy in English and trusts the
|
|
626
691
|
agent to obey. Nothing parses it, nothing blocks a call against it, and no
|
|
@@ -631,62 +696,57 @@ this repository's own CLAUDE.md is the first import fixture.
|
|
|
631
696
|
**Framework interrupts** (LangGraph `interrupt()`, CrewAI human input, AutoGen
|
|
632
697
|
`UserProxyAgent`, the OpenAI Agents SDK's `needsApproval`, Temporal signal
|
|
633
698
|
approvals) give a developer a pause-and-resume primitive and leave policy,
|
|
634
|
-
audit format, the human channel
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
log rather than as policy-attested files in your repo.
|
|
699
|
+
audit format, the human channel and the credential boundary to them. They also
|
|
700
|
+
require adopting the framework. Temporal's event history is a real append-only
|
|
701
|
+
execution record with crash recovery this project does not claim, though it
|
|
702
|
+
lives in Temporal's storage rather than as policy-attested files in your repo.
|
|
639
703
|
|
|
640
704
|
**Hosted approval platforms** (HumanLayer, gotoHuman, Permit.io's access
|
|
641
705
|
requests) are the closest relatives: multi-channel human routing, review UIs,
|
|
642
706
|
and in Permit.io's case a real authorization engine richer than autonomy
|
|
643
707
|
classes. Their model is a third-party service in the decision path, with the
|
|
644
708
|
audit trail in the platform's backend, and the agent's own process still
|
|
645
|
-
choosing to honor the returned verdict. They bring
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
there is no OS-level sandbox, no compliance certification, and the reference
|
|
658
|
-
phone channel is one app, Telegram.
|
|
709
|
+
choosing to honor the returned verdict. They bring hosted infrastructure,
|
|
710
|
+
escalation and team routing, and compliance certifications.
|
|
711
|
+
|
|
712
|
+
The difference is the combination: policy as a hash-attested markdown file in
|
|
713
|
+
your repo; an append-only, hash-chained log you verify locally with one
|
|
714
|
+
command; and an execution boundary where the credential is inert until a
|
|
715
|
+
single-use token is minted at the moment a human decides. Every framework
|
|
716
|
+
primitive and hosted API above relies on the agent's process honoring a
|
|
717
|
+
returned decision. Here the thing the agent needs, the credential, answers only
|
|
718
|
+
to the thing it cannot make, the token. The tradeoffs: you run the daemon and
|
|
719
|
+
listener yourself, there is no OS-level sandbox, no compliance certification,
|
|
720
|
+
and the reference phone channel is one app, Telegram.
|
|
659
721
|
|
|
660
722
|
## Can't the agent just go around it?
|
|
661
723
|
|
|
662
724
|
**Edit the policy?** An attestation records the SHA-256 of the policy's bytes,
|
|
663
725
|
and every gated operation refuses `hash-mismatch` when the live file disagrees
|
|
664
|
-
with it. An unattested policy refuses too, and attesting is human-only. Under
|
|
665
|
-
harness hook the edit itself is classified `policy.edit` before it happens,
|
|
666
|
-
because `APPROVAL.md` is in the built-in protected set no policy can narrow.
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
so an edited or reordered record breaks the chain and `approval log verify` says
|
|
674
|
-
so. Appends go through compare-and-append against the head, and projections
|
|
675
|
-
(`QUEUE.md`, the SQLite index) rebuild from the log and never write back to it.
|
|
676
|
-
Tampering is made evident, which is what an audit trail is for.
|
|
726
|
+
with it. An unattested policy refuses too, and attesting is human-only. Under
|
|
727
|
+
the harness hook the edit itself is classified `policy.edit` before it happens,
|
|
728
|
+
because `APPROVAL.md` is in the built-in protected set no policy can narrow.
|
|
729
|
+
|
|
730
|
+
**Fabricate or rewrite the log?** Each record chains to the previous one's
|
|
731
|
+
hash, so an edited or reordered record breaks the chain and `approval log
|
|
732
|
+
verify` says so. Appends go through compare-and-append against the head, and
|
|
733
|
+
projections (`QUEUE.md`, the SQLite index) rebuild from the log and never write
|
|
734
|
+
back to it.
|
|
677
735
|
|
|
678
736
|
**Mint its own token, or reuse one?** Tokens are minted at one site, inside the
|
|
679
|
-
path that records a human decision, and the log stores only the hash. No verb
|
|
680
|
-
no tool returns a token for a grant it did not just record, and a hook
|
|
681
|
-
mints none at all.
|
|
682
|
-
|
|
683
|
-
second record is appended.
|
|
737
|
+
path that records a human decision, and the log stores only the hash. No verb
|
|
738
|
+
and no tool returns a token for a grant it did not just record, and a hook
|
|
739
|
+
grant mints none at all. The second spend is refused `token-consumed`, naming
|
|
740
|
+
the seq of the `execution.started` that spent it.
|
|
684
741
|
|
|
685
742
|
**Call the adapter or the credential directly?** Credentials live in
|
|
686
|
-
`.approval/vault.enc` under a passphrase the policy names and never carries,
|
|
687
|
-
an adapter opens the vault only inside
|
|
688
|
-
`approval vault get`.
|
|
689
|
-
|
|
743
|
+
`.approval/vault.enc` under a passphrase the policy names and never carries,
|
|
744
|
+
and an adapter opens the vault only inside an execution window admitted by the
|
|
745
|
+
attested policy. There is no `approval vault get`. Manual and selected-live
|
|
746
|
+
paths need a grant token. An explicitly opted-in supervised or autonomous path
|
|
747
|
+
needs the passphrase already present in the adapter process environment; it
|
|
748
|
+
cannot use the token-scoped `.approval/env` fallback. This is where the hard
|
|
749
|
+
enforcement lives.
|
|
690
750
|
|
|
691
751
|
**Run the side effect from its own shell?** That is what the PreToolUse hook is
|
|
692
752
|
for: it classifies the command text, resolves the class, and answers allow or
|
|
@@ -697,30 +757,32 @@ stricter class. `bash -c`, `eval`, `sudo`, `xargs` and `node -e` deny as
|
|
|
697
757
|
`hook-opaque`.
|
|
698
758
|
|
|
699
759
|
**POST straight at the web queue?** It can. `approval channel web` binds
|
|
700
|
-
`127.0.0.1` with
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
with
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
a
|
|
709
|
-
|
|
710
|
-
control. A session, or a wider bind address, would need a real anti-CSRF token.
|
|
760
|
+
`127.0.0.1` with no `--host` flag, because with no authentication the loopback
|
|
761
|
+
interface is the entire access control, and every decision the page collects is
|
|
762
|
+
recorded against the human the runtime was started with: it proves "someone
|
|
763
|
+
with access to this machine approved". There is no CSRF token in v0.1,
|
|
764
|
+
deliberately: a CSRF token defends a session, and with no session anything that
|
|
765
|
+
can open a socket to `127.0.0.1:4680` can POST with or without one. The
|
|
766
|
+
same-origin check that rejects a POST naming a non-loopback `Origin` allows one
|
|
767
|
+
carrying neither header, since `curl` sends neither: treat it as a speed bump
|
|
768
|
+
rather than a control. A session, or a wider bind address, would need a real
|
|
769
|
+
anti-CSRF token.
|
|
711
770
|
|
|
712
771
|
**Lie in its self-report?** Self-reported fields never reduce scrutiny (SPEC.md
|
|
713
772
|
section 11.1, invariant 4). They may raise it, inform display, or feed
|
|
714
|
-
retrospective analysis, and they can never downgrade a resolved autonomy level
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
773
|
+
retrospective analysis, and they can never downgrade a resolved autonomy level
|
|
774
|
+
or supply `allow_irreversible`. Only an explicit class rule in the attested
|
|
775
|
+
operator policy can retain nonmanual autonomy for a truthful `reversible:
|
|
776
|
+
false` declaration. The prompt keeps computed facts apart from claimed fields.
|
|
777
|
+
|
|
778
|
+
**So what is left?** A fully adversarial agent on a machine it controls.
|
|
779
|
+
Identity in v0.1 is config-declared, so anyone who can set that variable and
|
|
780
|
+
write to the log is inside the trust boundary; cryptographic identity is future
|
|
781
|
+
work ([docs/proposals/hardened-authorization.md](docs/proposals/hardened-authorization.md)).
|
|
782
|
+
What holds regardless of what the harness believes it is running: the vault,
|
|
783
|
+
the adapter boundary, and the single-use token. Section 7 keeps irreversible
|
|
784
|
+
classes at `manual` unless the attested policy explicitly opts a class into the
|
|
785
|
+
exception.
|
|
724
786
|
|
|
725
787
|
## Running the checks
|
|
726
788
|
|
|
@@ -731,34 +793,49 @@ npm run check:tier -- <path> # classify the given paths and print the tier
|
|
|
731
793
|
approval doctor # the other check: this machine, not the code
|
|
732
794
|
```
|
|
733
795
|
|
|
734
|
-
`approval doctor` prints **
|
|
796
|
+
`approval doctor` prints **28 rows** and a tally, in the order their failures
|
|
735
797
|
cascade: build freshness, identity, attestation, the log chain, the channels
|
|
736
|
-
(`telegram`, `web-port`), the payload store, audit sampling, envelope
|
|
737
|
-
the vault, the environment source map, then the rows that ask git
|
|
738
|
-
what happened (`log-drift`, `reconciliation`,
|
|
739
|
-
`harness-hook-
|
|
740
|
-
`
|
|
741
|
-
`harness-version-unverified`, `live-draw`,
|
|
742
|
-
`gate-organs`, `sealed-keys
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
798
|
+
(`telegram`, `web-port`), the payload store, audit sampling, envelope
|
|
799
|
+
integrity, the vault, the environment source map, then the rows that ask git
|
|
800
|
+
and the harness what happened (`log-drift`, `reconciliation`,
|
|
801
|
+
`harness-hook-outcomes`, `harness-hook-wiring`, `keychain-scope`,
|
|
802
|
+
`log-advance-cadence`, `dark-sessions`, `verified-snapshot`, `read-proof`,
|
|
803
|
+
`main-behind-origin`, `harness-version-unverified`, `live-draw`,
|
|
804
|
+
`values-block`, `checkpoint`, `gate-organs`, `sealed-keys`,
|
|
805
|
+
`codex-hook-wiring`). Each failure
|
|
806
|
+
carries a `fix:` line you run yourself. Doctor appends nothing, sends nothing
|
|
807
|
+
and repairs nothing, and no credential value appears in its output. Three
|
|
808
|
+
of the 28 lines from a fresh directory, plus the tally:
|
|
809
|
+
|
|
810
|
+
```
|
|
811
|
+
✓ identity APPROVAL_HUMAN=human:alice (config-declared: the trust boundary is this machine, not cryptography)
|
|
812
|
+
✓ log /your/project/.approval/log/events.jsonl verifies: 1 record(s), head seq 1 0f3c4a19187a…
|
|
813
|
+
✗ audit-sampling disabled (secret-env-unnamed): APPROVAL.md sets audit.supervised_sample_rate to 0.1 but names no audit.sampling_secret_env. …
|
|
814
|
+
fix: approval policy attest --as human:<id> — after setting audit.supervised_sample_rate and audit.sampling_secret_env in the policy; then export the named variable where the daemon runs
|
|
815
|
+
9 ok · 18 not applicable · 1 failed
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
That one failure is expected on the scaffolded policy: it samples supervised
|
|
819
|
+
actions for audit, sampling needs an operator-held secret the policy only
|
|
820
|
+
names, and a control that looks on while the party under oversight could steer
|
|
821
|
+
it is worse than one that is visibly off. Name the secret when you want
|
|
822
|
+
sampling, or delete the `audit` block if one person's gate has no use for it.
|
|
823
|
+
|
|
824
|
+
**18 of the 28 report `not applicable` in a fresh directory**, and each names
|
|
825
|
+
the absence it skipped on: `telegram` (no bot variables), `envelope-integrity`
|
|
826
|
+
(no task folder), `vault` (no vault file), `environment` (no `.approval/env`),
|
|
827
|
+
`read-proof` (no `daemon` block), `live-draw` (no `supervised-live` class),
|
|
828
|
+
`checkpoint` (no `audit.checkpoint_keys`), `harness-hook-outcomes`,
|
|
829
|
+
`harness-hook-wiring`, `codex-hook-wiring`, `harness-version-unverified` and
|
|
750
830
|
`gate-organs` (no harness settings file), `verified-snapshot` (no daemon has
|
|
751
|
-
run), and
|
|
752
|
-
`
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
the enforcement for one of those is the protected-path guard in CI. Doctor
|
|
760
|
-
appends nothing, sends nothing and repairs nothing, and no credential value
|
|
761
|
-
appears in its output.
|
|
831
|
+
run), and
|
|
832
|
+
`log-drift`, `log-advance-cadence`, `dark-sessions`, `main-behind-origin` and
|
|
833
|
+
`sealed-keys` (not a git checkout). `sealed-keys` asks git what it tracks:
|
|
834
|
+
`.approval/payloads/` is tracked on purpose, and a sealed-delivery private key
|
|
835
|
+
swept in by a `git add` of that directory would open that action's token for
|
|
836
|
+
everyone holding the log. `gate-organs` is informational wherever it lands: it
|
|
837
|
+
lists the harness files whose current bytes carry no `approval policy attest
|
|
838
|
+
--organ` record, and never moves the exit code.
|
|
762
839
|
|
|
763
840
|
Checks come in three tiers.
|
|
764
841
|
|
|
@@ -771,68 +848,34 @@ Checks come in three tiers.
|
|
|
771
848
|
A denylist forces the full tier regardless of file extension: `APPROVAL.md`,
|
|
772
849
|
`CLAUDE.md`, `.claude/**`, `SPEC.md`, `schema/**`, `**/fixtures/**`,
|
|
773
850
|
`backlog/**`, `scripts/**`, `.github/**`, the packaging files, and `cli.js`.
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
effect, since their acceptance criteria are instructions to future agents. That
|
|
779
|
-
earns them every check which can observe a task file, and the records tier is
|
|
780
|
-
exactly those; it does not earn them a matrix of ~1800 tests on two Node
|
|
781
|
-
majors, none of which reads one. `MILESTONES.md` rides along because the
|
|
782
|
-
milestones guard checks the two against each other.
|
|
783
|
-
|
|
784
|
-
Classification is computed from the changed paths by
|
|
785
|
-
`scripts/classify-tier.mjs`, never asserted by the author of the change. Every
|
|
786
|
-
merge to `main` runs the full suite unconditionally, and anything ambiguous, an
|
|
787
|
-
empty path set included, resolves to full.
|
|
851
|
+
`backlog/**` sits on both that denylist and the records list, so a task file
|
|
852
|
+
mixed with any other path takes the full tier. Classification is computed from
|
|
853
|
+
the changed paths by `scripts/classify-tier.mjs`, never asserted by the author
|
|
854
|
+
of the change, and every merge to `main` runs the full suite.
|
|
788
855
|
|
|
789
856
|
### Before the push: `npm run ci:local`
|
|
790
857
|
|
|
791
|
-
The merge queue is serial, so every red run there costs a slot
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
What it cannot reproduce it says, rather than passing over. The Node 20 floor
|
|
810
|
-
legs need Node 20, and this host runs whatever it runs. CI's runner is
|
|
811
|
-
`ubuntu-latest`, so on any other platform the report names the suites whose
|
|
812
|
-
meaning differs here, the temp root's shape and the symlink cases among them. A
|
|
813
|
-
cross-check with no reachable merge base, or with the records branches
|
|
814
|
-
unfetched, is reported unresolved and kept out of the verdict. A red step exits
|
|
815
|
-
non-zero and names the files that failed. Nothing in CI consults any of this: a
|
|
816
|
-
green run locally is a prediction, and the workflow remains the verdict.
|
|
817
|
-
|
|
818
|
-
A full-tier CI job compiles once. It builds, then runs `node
|
|
819
|
-
scripts/run-tests.mjs` over what it built, because `npm test` and `npm run
|
|
820
|
-
typecheck` would each recompile the same tree and neither pass can fail where
|
|
821
|
-
the build passed. `npm test` keeps its build-then-run shape for anyone running
|
|
822
|
-
it by hand. `scripts/run-tests.mjs --shard <k>/<n>` takes shard `k` of the
|
|
823
|
-
sorted file list, where the file at position `i` belongs to shard `(i mod n) +
|
|
824
|
-
1`, so the shards of a matrix are a partition of the suite: every file in
|
|
825
|
-
exactly one shard, and the matrix covers all of them. An out-of-range index, an
|
|
826
|
-
empty shard, and `--shard` combined with `--only` are refused rather than run.
|
|
827
|
-
The Node 20 floor moved to the merge queue and to pushes to `main` because the
|
|
828
|
-
queue candidate is what stands between a change and the branch, and a pull
|
|
829
|
-
request now gets its verdict from the shards alone. The floor leg is sharded
|
|
830
|
-
three ways too, so it proves the same whole suite in roughly a third of the
|
|
831
|
-
wall clock it took as one run.
|
|
858
|
+
The merge queue is serial, so every red run there costs a slot and another
|
|
859
|
+
wait. `npm run ci:local` asks the same classifier the workflow asks and runs
|
|
860
|
+
the jobs `.github/workflows/ci.yml` declares for that tier: the docs guard for
|
|
861
|
+
light, the record-reading tests for records, the three shards plus lint for
|
|
862
|
+
full, and the protected-path grant cross-check on every tier when a merge base
|
|
863
|
+
is computable. `--base <ref>` picks the base, `--working-tree` and explicit
|
|
864
|
+
paths are the other path sources, `--dry-run` prints the plan, `--json` prints
|
|
865
|
+
it as data, and `--parallel` runs the tier's jobs concurrently. What it cannot
|
|
866
|
+
reproduce it says: the Node 20 legs need Node 20, and CI's runner is
|
|
867
|
+
`ubuntu-latest`. A green run locally is a prediction; the workflow is the
|
|
868
|
+
verdict.
|
|
869
|
+
|
|
870
|
+
`npm run check:changed` answers a different question: it classifies the
|
|
871
|
+
working tree and runs the tier in its own shape, which for full is `npm test`,
|
|
872
|
+
`npm run lint` and `npm run typecheck`. Use it while working, and `ci:local`
|
|
873
|
+
before pushing. `scripts/run-tests.mjs --shard <k>/<n>` takes shard `k` of the
|
|
874
|
+
sorted file list, so the shards of a matrix partition the suite.
|
|
832
875
|
|
|
833
876
|
## Exit codes
|
|
834
877
|
|
|
835
|
-
An agent branches on the exit code before it
|
|
878
|
+
An agent branches on the exit code before it reads stdout, so these numbers
|
|
836
879
|
are frozen. Adding one is a spec change; changing a meaning is breaking.
|
|
837
880
|
|
|
838
881
|
| Code | Meaning |
|
|
@@ -845,61 +888,49 @@ are frozen. Adding one is a spec change; changing a meaning is breaking.
|
|
|
845
888
|
| 5 | no valid execution token (approval run only) |
|
|
846
889
|
| 6 | timeout (approval wait only) |
|
|
847
890
|
|
|
848
|
-
Code 1 and code 4 are kept apart deliberately
|
|
849
|
-
"the file has been tampered with" are different facts
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
`error.code` under `--json` rather than retrying with different flags.
|
|
891
|
+
Code 1 and code 4 are kept apart deliberately: "I could not read the file" and
|
|
892
|
+
"the file has been tampered with" are different facts, and conflating them
|
|
893
|
+
either cries wolf over a permission bit or lets tampering read as a filesystem
|
|
894
|
+
hiccup. Code 3, a torn tail, is the signature of a crashed write, and nothing
|
|
895
|
+
is repaired automatically: truncating a torn line is a human decision. A gate
|
|
896
|
+
refusal is exit 1 and never 2, since the command was well-formed and the answer
|
|
897
|
+
is no; branch on `error.code` under `--json`.
|
|
856
898
|
|
|
857
899
|
## Where to look next
|
|
858
900
|
|
|
859
901
|
[SPEC.md](SPEC.md) is the source of truth for every design decision, and this
|
|
860
902
|
README defers to it wherever the two could be read differently.
|
|
861
|
-
[CLAUDE.md](CLAUDE.md) describes how this repository builds itself
|
|
862
|
-
|
|
903
|
+
[CLAUDE.md](CLAUDE.md) describes how this repository builds itself behind its
|
|
904
|
+
own gate; the 0.1.0 release was published, tagged and pushed through three
|
|
905
|
+
grants from a phone.
|
|
863
906
|
|
|
864
|
-
Every command carries its own instructions
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
907
|
+
Every command carries its own instructions. `approval --help` lists them
|
|
908
|
+
grouped by purpose, `approval <command> --help` gives one command's flags,
|
|
909
|
+
refusal codes and JSON shape, and `--help --long` appends that verb's
|
|
910
|
+
reasoning from [docs/cli-reference.md](docs/cli-reference.md). `approval
|
|
911
|
+
instructions` is the agent-facing guide, and `--schemas` prints the verb
|
|
912
|
+
registry as JSON.
|
|
870
913
|
|
|
871
914
|
Every external adapter, harness, updater or gateway this project has weighed
|
|
872
|
-
|
|
873
|
-
[docs/integrations-considered.md](docs/integrations-considered.md)
|
|
874
|
-
exposes, how it fits, the verdict, and the next step, so the question is
|
|
875
|
-
answered once.
|
|
876
|
-
|
|
877
|
-
One of those entries has a runbook of its own.
|
|
915
|
+
has an entry in
|
|
916
|
+
[docs/integrations-considered.md](docs/integrations-considered.md).
|
|
878
917
|
[examples/grok-bot-connector/runbook.md](examples/grok-bot-connector/runbook.md)
|
|
879
|
-
puts a Grok Bot agent on the far end of `approval mcp serve --http --guest
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
evidence seq or `none`.
|
|
886
|
-
|
|
887
|
-
Designs that are proposed and not yet built live under `docs/proposals/`.
|
|
888
|
-
[docs/proposals/hardened-authorization.md](docs/proposals/hardened-authorization.md)
|
|
889
|
-
is the longest of them: what a grant in this log can and cannot prove to a
|
|
890
|
-
service that does not trust the operator, and what an optional stronger tier
|
|
891
|
-
would have to be. Identity in v0.1 is config-declared, so the honest ceiling
|
|
892
|
-
today is "a party with write access to this log recorded a decision", and the
|
|
893
|
-
proposal works through device-bound keys, WebAuthn on a separately controlled
|
|
894
|
-
surface, per-decision signatures over the existing checkpoint machinery, and
|
|
895
|
-
third-party witnesses, with the phasing, the receipt format, and the negative
|
|
896
|
-
tests each would need. Nothing in it is implemented, and nothing in it amends
|
|
897
|
-
SPEC.md. Two shorter ones,
|
|
918
|
+
puts a Grok Bot agent on the far end of `approval mcp serve --http --guest`
|
|
919
|
+
and rehearses both halves of the story: the agent asking for a branch push and
|
|
920
|
+
an email and a human deciding on a phone, then the agent skipping the gate and
|
|
921
|
+
finding the credential inert.
|
|
922
|
+
|
|
923
|
+
Designs proposed and not yet built live under `docs/proposals/`.
|
|
898
924
|
[docs/proposals/solo-dev-quickstart.md](docs/proposals/solo-dev-quickstart.md)
|
|
899
|
-
and [docs/proposals/no-daemon-mode.md](docs/proposals/no-daemon-mode.md)
|
|
900
|
-
|
|
901
|
-
one `guard
|
|
902
|
-
|
|
925
|
+
and [docs/proposals/no-daemon-mode.md](docs/proposals/no-daemon-mode.md) are
|
|
926
|
+
the next step for the path at the top of this page: a three-question
|
|
927
|
+
`approval quickstart`, one `approval guard -- <command>` verb that replaces
|
|
928
|
+
the register, request, wait, run quartet, and a runtime that lives inside the
|
|
929
|
+
waiting command instead of a daemon.
|
|
930
|
+
[docs/proposals/hardened-authorization.md](docs/proposals/hardened-authorization.md)
|
|
931
|
+
works through what a grant in this log can and cannot prove to a service that
|
|
932
|
+
does not trust the operator, and what a stronger identity tier would have to
|
|
933
|
+
be.
|
|
903
934
|
|
|
904
935
|
## License and governance
|
|
905
936
|
|