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.
Files changed (266) hide show
  1. package/README.md +584 -553
  2. package/SPEC.md +42 -13
  3. package/dist/src/adapters/agentmail.d.ts +426 -0
  4. package/dist/src/adapters/agentmail.js +2 -2
  5. package/dist/src/adapters/conformance.d.ts +149 -0
  6. package/dist/src/adapters/contract.d.ts +628 -0
  7. package/dist/src/adapters/contract.js +110 -16
  8. package/dist/src/adapters/contract.js.map +1 -1
  9. package/dist/src/adapters/email.d.ts +324 -0
  10. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  11. package/dist/src/adapters/public.d.ts +11 -0
  12. package/dist/src/adapters/public.js +11 -0
  13. package/dist/src/adapters/public.js.map +1 -0
  14. package/dist/src/adapters/registry.d.ts +59 -0
  15. package/dist/src/adapters/registry.js +2 -1
  16. package/dist/src/adapters/registry.js.map +1 -1
  17. package/dist/src/adapters/smtp.d.ts +213 -0
  18. package/dist/src/adapters/vault-provider.d.ts +114 -0
  19. package/dist/src/adapters/vault-provider.js +3 -3
  20. package/dist/src/adapters/zzz.d.ts +66 -0
  21. package/dist/src/adapters/zzz.js +299 -0
  22. package/dist/src/adapters/zzz.js.map +1 -0
  23. package/dist/src/channels/batch.d.ts +109 -0
  24. package/dist/src/channels/cli.d.ts +193 -0
  25. package/dist/src/channels/conformance.d.ts +92 -0
  26. package/dist/src/channels/contract.d.ts +623 -0
  27. package/dist/src/channels/payload-view.d.ts +35 -0
  28. package/dist/src/channels/render-queue.d.ts +149 -0
  29. package/dist/src/channels/tagging.d.ts +196 -0
  30. package/dist/src/channels/telegram.d.ts +1832 -0
  31. package/dist/src/channels/web.d.ts +341 -0
  32. package/dist/src/cli/adapter.d.ts +90 -0
  33. package/dist/src/cli/adapter.js +25 -15
  34. package/dist/src/cli/adapter.js.map +1 -1
  35. package/dist/src/cli/amend.d.ts +59 -0
  36. package/dist/src/cli/args.d.ts +43 -0
  37. package/dist/src/cli/attest.d.ts +41 -0
  38. package/dist/src/cli/audit-card.d.ts +62 -0
  39. package/dist/src/cli/audit.d.ts +59 -0
  40. package/dist/src/cli/channel-telegram.d.ts +806 -0
  41. package/dist/src/cli/channel-web.d.ts +131 -0
  42. package/dist/src/cli/channel.d.ts +71 -0
  43. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  44. package/dist/src/cli/codex.d.ts +2 -0
  45. package/dist/src/cli/codex.js +172 -0
  46. package/dist/src/cli/codex.js.map +1 -0
  47. package/dist/src/cli/coverage.d.ts +61 -0
  48. package/dist/src/cli/daemon.d.ts +120 -0
  49. package/dist/src/cli/doctor.d.ts +129 -0
  50. package/dist/src/cli/doctor.js +119 -5
  51. package/dist/src/cli/doctor.js.map +1 -1
  52. package/dist/src/cli/env.d.ts +65 -0
  53. package/dist/src/cli/execute.d.ts +202 -0
  54. package/dist/src/cli/exit-codes.d.ts +73 -0
  55. package/dist/src/cli/feedback.d.ts +60 -0
  56. package/dist/src/cli/gate-window.d.ts +40 -0
  57. package/dist/src/cli/gate.d.ts +68 -0
  58. package/dist/src/cli/git-scope.d.ts +190 -0
  59. package/dist/src/cli/gloss-attach.d.ts +85 -0
  60. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  61. package/dist/src/cli/gloss-codex.d.ts +24 -0
  62. package/dist/src/cli/gloss-options.d.ts +42 -0
  63. package/dist/src/cli/gloss.d.ts +265 -0
  64. package/dist/src/cli/help.d.ts +103 -0
  65. package/dist/src/cli/help.js +173 -51
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook-codex.d.ts +78 -0
  68. package/dist/src/cli/hook-codex.js +167 -0
  69. package/dist/src/cli/hook-codex.js.map +1 -0
  70. package/dist/src/cli/hook.d.ts +331 -0
  71. package/dist/src/cli/hook.js +186 -80
  72. package/dist/src/cli/hook.js.map +1 -1
  73. package/dist/src/cli/import.d.ts +35 -0
  74. package/dist/src/cli/init.d.ts +84 -0
  75. package/dist/src/cli/init.js +2 -2
  76. package/dist/src/cli/init.js.map +1 -1
  77. package/dist/src/cli/instructions.d.ts +23 -0
  78. package/dist/src/cli/journal.d.ts +41 -0
  79. package/dist/src/cli/log-advance.d.ts +287 -0
  80. package/dist/src/cli/log-advance.js +102 -11
  81. package/dist/src/cli/log-advance.js.map +1 -1
  82. package/dist/src/cli/log-anchor.d.ts +176 -0
  83. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  84. package/dist/src/cli/log-sync.d.ts +243 -0
  85. package/dist/src/cli/log-verbs.d.ts +16 -0
  86. package/dist/src/cli/log-verbs.js +7 -1
  87. package/dist/src/cli/log-verbs.js.map +1 -1
  88. package/dist/src/cli/long-help.d.ts +70 -0
  89. package/dist/src/cli/main.d.ts +77 -0
  90. package/dist/src/cli/main.js +155 -5
  91. package/dist/src/cli/main.js.map +1 -1
  92. package/dist/src/cli/mcp.d.ts +52 -0
  93. package/dist/src/cli/paths.d.ts +56 -0
  94. package/dist/src/cli/payload.d.ts +58 -0
  95. package/dist/src/cli/policy.d.ts +43 -0
  96. package/dist/src/cli/preflight.d.ts +363 -0
  97. package/dist/src/cli/preflight.js +294 -7
  98. package/dist/src/cli/preflight.js.map +1 -1
  99. package/dist/src/cli/progress.d.ts +78 -0
  100. package/dist/src/cli/prompt.d.ts +209 -0
  101. package/dist/src/cli/quickstart.d.ts +46 -0
  102. package/dist/src/cli/quickstart.js +297 -0
  103. package/dist/src/cli/quickstart.js.map +1 -0
  104. package/dist/src/cli/records.d.ts +34 -0
  105. package/dist/src/cli/render.d.ts +22 -0
  106. package/dist/src/cli/sandbox.d.ts +51 -0
  107. package/dist/src/cli/scaffold.d.ts +79 -0
  108. package/dist/src/cli/setup-adapter.d.ts +137 -0
  109. package/dist/src/cli/setup-adapter.js +38 -4
  110. package/dist/src/cli/setup-adapter.js.map +1 -1
  111. package/dist/src/cli/setup-channel.d.ts +117 -0
  112. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  113. package/dist/src/cli/setup-common.d.ts +275 -0
  114. package/dist/src/cli/setup-flow.d.ts +287 -0
  115. package/dist/src/cli/setup-service.d.ts +96 -0
  116. package/dist/src/cli/setup.d.ts +202 -0
  117. package/dist/src/cli/style.d.ts +320 -0
  118. package/dist/src/cli/token.d.ts +39 -0
  119. package/dist/src/cli/up.d.ts +155 -0
  120. package/dist/src/cli/up.js +4 -2
  121. package/dist/src/cli/up.js.map +1 -1
  122. package/dist/src/cli/usage.d.ts +37 -0
  123. package/dist/src/cli/values.d.ts +40 -0
  124. package/dist/src/cli/vault.d.ts +59 -0
  125. package/dist/src/cli/vault.js +2 -2
  126. package/dist/src/cli/vault.js.map +1 -1
  127. package/dist/src/cli/verb-registry.d.ts +76 -0
  128. package/dist/src/cli/verb-registry.js +176 -8
  129. package/dist/src/cli/verb-registry.js.map +1 -1
  130. package/dist/src/cli/wordmark.d.ts +31 -0
  131. package/dist/src/cli/wordmark.js +2 -2
  132. package/dist/src/codex/doctor.d.ts +13 -0
  133. package/dist/src/codex/doctor.js +41 -0
  134. package/dist/src/codex/doctor.js.map +1 -0
  135. package/dist/src/codex/manifest.d.ts +49 -0
  136. package/dist/src/codex/manifest.js +103 -0
  137. package/dist/src/codex/manifest.js.map +1 -0
  138. package/dist/src/codex/templates.d.ts +41 -0
  139. package/dist/src/codex/templates.js +319 -0
  140. package/dist/src/codex/templates.js.map +1 -0
  141. package/dist/src/codex/trust.d.ts +19 -0
  142. package/dist/src/codex/trust.js +183 -0
  143. package/dist/src/codex/trust.js.map +1 -0
  144. package/dist/src/codex/workspace-plan.d.ts +131 -0
  145. package/dist/src/codex/workspace-plan.js +561 -0
  146. package/dist/src/codex/workspace-plan.js.map +1 -0
  147. package/dist/src/core/actor.d.ts +2 -0
  148. package/dist/src/core/actor.js +5 -0
  149. package/dist/src/core/actor.js.map +1 -0
  150. package/dist/src/core/advance-cycle.d.ts +170 -0
  151. package/dist/src/core/agents-md.d.ts +276 -0
  152. package/dist/src/core/apply-patch.d.ts +49 -0
  153. package/dist/src/core/apply-patch.js +266 -0
  154. package/dist/src/core/apply-patch.js.map +1 -0
  155. package/dist/src/core/attest.d.ts +420 -0
  156. package/dist/src/core/attest.js +13 -1
  157. package/dist/src/core/attest.js.map +1 -1
  158. package/dist/src/core/audit.d.ts +492 -0
  159. package/dist/src/core/budgets.d.ts +238 -0
  160. package/dist/src/core/checkpoint.d.ts +500 -0
  161. package/dist/src/core/child-env.d.ts +88 -0
  162. package/dist/src/core/clock.d.ts +52 -0
  163. package/dist/src/core/command-class.d.ts +543 -0
  164. package/dist/src/core/command-class.js +43 -8
  165. package/dist/src/core/command-class.js.map +1 -1
  166. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  167. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  168. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  169. package/dist/src/core/coverage.d.ts +217 -0
  170. package/dist/src/core/credential-spec.d.ts +72 -0
  171. package/dist/src/core/dark-session.d.ts +331 -0
  172. package/dist/src/core/decision-refusal.d.ts +185 -0
  173. package/dist/src/core/env-file.d.ts +450 -0
  174. package/dist/src/core/execute.d.ts +858 -0
  175. package/dist/src/core/execute.js +44 -6
  176. package/dist/src/core/execute.js.map +1 -1
  177. package/dist/src/core/frontmatter.d.ts +78 -0
  178. package/dist/src/core/gate-window.d.ts +312 -0
  179. package/dist/src/core/gate.d.ts +1364 -0
  180. package/dist/src/core/gate.js +68 -13
  181. package/dist/src/core/gate.js.map +1 -1
  182. package/dist/src/core/git-run.d.ts +73 -0
  183. package/dist/src/core/harness-version.d.ts +157 -0
  184. package/dist/src/core/harness-version.js +2 -1
  185. package/dist/src/core/harness-version.js.map +1 -1
  186. package/dist/src/core/harness-wait.d.ts +55 -0
  187. package/dist/src/core/head-retry.d.ts +107 -0
  188. package/dist/src/core/instance.d.ts +253 -0
  189. package/dist/src/core/intake-limits.d.ts +247 -0
  190. package/dist/src/core/jcs.d.ts +52 -0
  191. package/dist/src/core/journal.d.ts +144 -0
  192. package/dist/src/core/live-draw.d.ts +436 -0
  193. package/dist/src/core/log-reconcile.d.ts +89 -0
  194. package/dist/src/core/log-subscribe.d.ts +36 -0
  195. package/dist/src/core/log-subscribe.js +162 -0
  196. package/dist/src/core/log-subscribe.js.map +1 -0
  197. package/dist/src/core/log.d.ts +278 -0
  198. package/dist/src/core/loop.d.ts +274 -0
  199. package/dist/src/core/loop.js +11 -0
  200. package/dist/src/core/loop.js.map +1 -1
  201. package/dist/src/core/md-fence.d.ts +41 -0
  202. package/dist/src/core/money.d.ts +147 -0
  203. package/dist/src/core/payload-census.d.ts +74 -0
  204. package/dist/src/core/payload-store.d.ts +175 -0
  205. package/dist/src/core/payload.d.ts +71 -0
  206. package/dist/src/core/policy-diff.d.ts +292 -0
  207. package/dist/src/core/policy-diff.js +27 -4
  208. package/dist/src/core/policy-diff.js.map +1 -1
  209. package/dist/src/core/policy-expectations.d.ts +199 -0
  210. package/dist/src/core/policy-explain.d.ts +150 -0
  211. package/dist/src/core/policy-explain.js +31 -3
  212. package/dist/src/core/policy-explain.js.map +1 -1
  213. package/dist/src/core/policy-load.d.ts +527 -0
  214. package/dist/src/core/policy-load.js +15 -3
  215. package/dist/src/core/policy-load.js.map +1 -1
  216. package/dist/src/core/policy-match.d.ts +281 -0
  217. package/dist/src/core/policy-match.js +20 -9
  218. package/dist/src/core/policy-match.js.map +1 -1
  219. package/dist/src/core/policy-proposal.d.ts +265 -0
  220. package/dist/src/core/prompt-layout.d.ts +221 -0
  221. package/dist/src/core/protected-path-guard.d.ts +453 -0
  222. package/dist/src/core/protected-path-guard.js +514 -35
  223. package/dist/src/core/protected-path-guard.js.map +1 -1
  224. package/dist/src/core/registration.d.ts +25 -0
  225. package/dist/src/core/reindex.d.ts +99 -0
  226. package/dist/src/core/sampler.d.ts +313 -0
  227. package/dist/src/core/sandbox.d.ts +290 -0
  228. package/dist/src/core/seal.d.ts +165 -0
  229. package/dist/src/core/state.d.ts +505 -0
  230. package/dist/src/core/task-file.d.ts +185 -0
  231. package/dist/src/core/telegram-config.d.ts +93 -0
  232. package/dist/src/core/token.d.ts +409 -0
  233. package/dist/src/core/token.js +21 -38
  234. package/dist/src/core/token.js.map +1 -1
  235. package/dist/src/core/validate.d.ts +138 -0
  236. package/dist/src/core/values.d.ts +137 -0
  237. package/dist/src/core/vault.d.ts +291 -0
  238. package/dist/src/core/verified-snapshot.d.ts +204 -0
  239. package/dist/src/core/verify.d.ts +336 -0
  240. package/dist/src/core/version.d.ts +8 -0
  241. package/dist/src/core/wysiwys.d.ts +370 -0
  242. package/dist/src/daemon/advance-child.d.ts +39 -0
  243. package/dist/src/daemon/advance.d.ts +466 -0
  244. package/dist/src/daemon/audit.d.ts +87 -0
  245. package/dist/src/daemon/daemon.d.ts +1180 -0
  246. package/dist/src/daemon/dark-session.d.ts +64 -0
  247. package/dist/src/daemon/draw-child.d.ts +36 -0
  248. package/dist/src/daemon/draw.d.ts +154 -0
  249. package/dist/src/daemon/git-evidence.d.ts +173 -0
  250. package/dist/src/daemon/git-evidence.js +1 -1
  251. package/dist/src/daemon/projection.d.ts +180 -0
  252. package/dist/src/daemon/prune.d.ts +207 -0
  253. package/dist/src/mcp/http.d.ts +113 -0
  254. package/dist/src/mcp/server.d.ts +265 -0
  255. package/dist/src/mcp/server.js +9 -1
  256. package/dist/src/mcp/server.js.map +1 -1
  257. package/docs/adapter-api.md +106 -0
  258. package/docs/cli-reference.md +389 -36
  259. package/docs/codex-enforced-session.md +30 -0
  260. package/package.json +12 -2
  261. package/schema/codex-instance.schema.json +82 -0
  262. package/schema/event.schema.json +2 -1
  263. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  264. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  265. package/schema/policy.schema.json +21 -1
  266. package/templates/codex/README.md +9 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Which harness binary issued a hook-written record (APRV-227).
3
+ *
4
+ * ## The hole
5
+ *
6
+ * A harness upgrade — `claude update`, a global npm install, an unattended
7
+ * updater on a launchd timer — swaps the binary that hosts the PreToolUse hook
8
+ * without a line anywhere. A new harness release can change the hook envelope
9
+ * semantics and quietly stop the gate firing, and the first evidence would be
10
+ * an action nobody was asked about. The gate cannot stop a person upgrading
11
+ * their own machine, and should not. What it can do is notice the effect: the
12
+ * records the hook writes name the harness that issued them, and `approval
13
+ * doctor` says so when the installed binary no longer matches the last one the
14
+ * log saw.
15
+ *
16
+ * ## What this field is, and what it is emphatically not
17
+ *
18
+ * It is INFORMATIONAL. Nothing in the runtime reads it back as an input:
19
+ * no class resolution, no irreversibility floor, no loop-escalation streak, no
20
+ * budget arithmetic, no sampling draw. SPEC.md §11.1 invariant 4 says a
21
+ * self-reported field never reduces scrutiny, and the discipline that keeps
22
+ * that true here is that the value has exactly one reader — a doctor row that
23
+ * can only ADD a red line — and exactly one direction it can move a human's
24
+ * attention, which is toward the log rather than away from it. A harness that
25
+ * lies about its own version buys itself nothing: the row it defeats is a row
26
+ * that would have asked for a look.
27
+ *
28
+ * It is also OPTIONAL and additive. A record written before the field existed
29
+ * carries neither half and still validates and still verifies; a hook that
30
+ * cannot establish a version writes neither half rather than a guess.
31
+ *
32
+ * ## Where the value comes from, in order
33
+ *
34
+ * 1. The hook event's own version field, where the harness supplies one.
35
+ * Claude Code's PreToolUse event may carry it; Cursor's does not.
36
+ * 2. `<binary> --version`, read at most ONCE per process ({@link
37
+ * installedHarnessVersion} memoizes, including the failures) with a short
38
+ * hard timeout.
39
+ * 3. Absent.
40
+ *
41
+ * Step 2 is why this module is careful about cost. A hook process exists per
42
+ * gated tool call, and APRV-186/188/212 each removed a term from what that
43
+ * process pays. So the probe is reached only on a path that is about to WRITE a
44
+ * record — a registration or a bypass — and never on the pass-through path that
45
+ * answers `cat README.md`. The memo makes a multi-class command pay once.
46
+ *
47
+ * ## Why the binary name is not configurable
48
+ *
49
+ * `cli/gloss.ts` already settled this: a runtime that let a policy or an
50
+ * environment variable name the executable it runs would have invented a new
51
+ * way to be told what to execute. The map below is the whole list, PATH is the
52
+ * only seam, and a test puts a stub binary in front of it.
53
+ */
54
+ /** The harnesses this runtime speaks a hook protocol for. */
55
+ export declare const HARNESS_KINDS: readonly ["claude-code", "cursor", "codex"];
56
+ export type HarnessKind = (typeof HARNESS_KINDS)[number];
57
+ /** Is `value` one of the harnesses this runtime knows? */
58
+ export declare function isHarnessKind(value: unknown): value is HarnessKind;
59
+ /**
60
+ * The executable each harness installs on PATH.
61
+ *
62
+ * Not configurable, deliberately (see the header). A harness whose binary is
63
+ * not one of these has no version to read here, and the field is simply absent.
64
+ */
65
+ export declare const HARNESS_BINARY: Readonly<Record<HarnessKind, string>>;
66
+ /**
67
+ * How long a version string may be, and the reason there is a cap at all.
68
+ *
69
+ * `<binary> --version` is the output of a third-party process, and this value
70
+ * is appended to an append-only log. SPEC.md §11.1 invariant 3 has no exception
71
+ * for provenance: a field that accepted arbitrary bytes is a field where a
72
+ * banner, a stack trace, or the credential quoted inside one arrives and stays
73
+ * forever. So the write boundary takes one line, printable ASCII only, capped.
74
+ */
75
+ export declare const HARNESS_VERSION_LIMIT = 64;
76
+ /**
77
+ * The one spelling of a harness version this runtime ever writes or compares.
78
+ *
79
+ * First line, trimmed, and nothing else: `claude --version` prints
80
+ * `2.0.14 (Claude Code)` and a future release may add a second line of banner.
81
+ * Both the hook (which records) and doctor (which compares) call this, so the
82
+ * comparison is between two values normalized identically — a row that failed
83
+ * because one side kept a trailing newline would be a false alarm, and a false
84
+ * alarm in a health check trains an operator to ignore it.
85
+ *
86
+ * Returns `null` for anything that is not a usable version: empty output, a
87
+ * line that is not printable ASCII, or one longer than the cap. Absence is
88
+ * always available and always honest.
89
+ */
90
+ export declare function normalizeHarnessVersion(raw: unknown): string | null;
91
+ /**
92
+ * How long the probe may take before it is killed and reported as absent.
93
+ *
94
+ * A bound against a HUNG binary, not a latency target: `<binary> --version`
95
+ * answers in milliseconds on any machine that is not already in trouble, and
96
+ * this number is only ever reached by one that is. It is generous for the same
97
+ * reason `cli/gloss.ts`'s is (20s there): a timeout tuned to a healthy machine
98
+ * turns a loaded one into a silent, intermittent absence, and an absence that
99
+ * appears under load is the least useful failure a provenance field could have.
100
+ * The cost is bounded by the fact that this runs only where a record is being
101
+ * written, and at most once per process.
102
+ */
103
+ export declare const HARNESS_PROBE_TIMEOUT_MS = 10000;
104
+ /**
105
+ * Run `<binary> --version` once, uncached, and normalize what came back.
106
+ *
107
+ * Every failure is a value rather than an exception, in the manner of
108
+ * {@link spawnGloss}: a missing binary, a non-zero exit and a timeout kill are
109
+ * all reported on the result object, and all of them are simply "no version".
110
+ *
111
+ * The child is STARVED, for the reason the gloss runner is: this is a
112
+ * third-party CLI spawned by a process that may be holding a bot token and a
113
+ * vault passphrase, and it has no use for either. Nothing is declared, because
114
+ * reading a version is not a granted action.
115
+ */
116
+ export declare function probeHarnessVersion(kind: HarnessKind): string | null;
117
+ /**
118
+ * The installed version of `kind`, read at most once per process.
119
+ *
120
+ * This is the only entry point callers should use. `probeHarnessVersion` is
121
+ * exported for the test that proves the memo is a memo.
122
+ */
123
+ export declare function installedHarnessVersion(kind: HarnessKind): string | null;
124
+ /** Drop the memo. TEST ONLY: a process reads a version once, by design. */
125
+ export declare function resetHarnessVersionCache(): void;
126
+ /**
127
+ * The provenance pair a hook-written record carries, or `null` when this
128
+ * process could not establish one.
129
+ *
130
+ * Both halves or neither. A version with no harness beside it is a string
131
+ * doctor cannot attribute to a binary — one log holds the records of every
132
+ * harness that ever wrote to it — and a harness with no version is a field that
133
+ * says nothing.
134
+ */
135
+ export interface HarnessProvenance {
136
+ harness: HarnessKind;
137
+ harness_version: string;
138
+ }
139
+ /**
140
+ * Build the pair for `kind`, preferring the version the hook event supplied.
141
+ *
142
+ * `eventVersion` is whatever the harness put in its own event, unvalidated:
143
+ * it goes through {@link normalizeHarnessVersion} exactly as the probe's
144
+ * output does, because it arrives from the same untrusted side of the boundary
145
+ * and the write boundary does not have two standards.
146
+ */
147
+ export declare function harnessProvenance(kind: HarnessKind, eventVersion?: unknown): HarnessProvenance | null;
148
+ /**
149
+ * The provenance pair carried by an already-written record's payload, or
150
+ * `null` when it carries none (or carries half of one).
151
+ *
152
+ * The read side of the same contract, used by `approval doctor`. Strict on
153
+ * purpose: a record whose `harness` is a string this runtime does not know is a
154
+ * record from a harness this build cannot probe, so it is not evidence about
155
+ * any binary here and is skipped rather than guessed at.
156
+ */
157
+ export declare function readHarnessProvenance(payload: unknown): HarnessProvenance | null;
@@ -54,7 +54,7 @@
54
54
  import { spawnSync } from "node:child_process";
55
55
  import { childEnvironment } from "./child-env.js";
56
56
  /** The harnesses this runtime speaks a hook protocol for. */
57
- export const HARNESS_KINDS = ["claude-code", "cursor"];
57
+ export const HARNESS_KINDS = ["claude-code", "cursor", "codex"];
58
58
  /** Is `value` one of the harnesses this runtime knows? */
59
59
  export function isHarnessKind(value) {
60
60
  return typeof value === "string" && HARNESS_KINDS.includes(value);
@@ -68,6 +68,7 @@ export function isHarnessKind(value) {
68
68
  export const HARNESS_BINARY = {
69
69
  "claude-code": "claude",
70
70
  cursor: "cursor-agent",
71
+ codex: "codex",
71
72
  };
72
73
  /**
73
74
  * How long a version string may be, and the reason there is a cap at all.
@@ -1 +1 @@
1
- {"version":3,"file":"harness-version.js","sourceRoot":"","sources":["../../../src/core/harness-version.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"harness-version.js","sourceRoot":"","sources":["../../../src/core/harness-version.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * How long a harness hook waits, and how long the question it opened outlives
3
+ * that wait (APRV-287).
4
+ *
5
+ * Two numbers with two readers each, which is the whole reason they live here
6
+ * rather than beside either one. `cli/hook.ts` uses them to decide when a
7
+ * request it opened has been abandoned; `cli/channel-telegram.ts` uses the wait
8
+ * to decide which pending requests on a listener's first cycle are stale enough
9
+ * to collapse into one message. Two copies of the same duration would be two
10
+ * answers to "is anybody still waiting on this", and the channel would collapse
11
+ * questions the hook was still holding open, or leave a dead queue on a phone.
12
+ *
13
+ * Nothing here reads a clock, a policy or the log: these are durations, and
14
+ * every judgment made with them is made by a caller that already holds the
15
+ * instant it is judging against.
16
+ */
17
+ /**
18
+ * The hook's default wait, chosen to sit inside Claude Code's own 60s hook
19
+ * default. The string form is what the flag parser takes; the millisecond form
20
+ * is what a comparison takes.
21
+ */
22
+ export declare const HOOK_DEFAULT_WAIT = "55s";
23
+ /** {@link HOOK_DEFAULT_WAIT} in milliseconds. */
24
+ export declare const HOOK_DEFAULT_WAIT_MS = 55000;
25
+ /**
26
+ * How long a request outlives the wait that opened it, before the hook takes it
27
+ * back (APRV-287).
28
+ *
29
+ * The window exists for exactly one thing: the retry. A hook whose wait expires
30
+ * leaves its question open because a decision inside the policy's TTL still
31
+ * authorizes an identical retry of the identical command (APRV-117), and the
32
+ * agent that was denied usually retries within a minute or two. So the question
33
+ * stays live for that long and no longer.
34
+ *
35
+ * Past it, nobody is coming back for the answer. The request was accruing on an
36
+ * approver's phone as one more message a restarted listener would re-deliver,
37
+ * and on 2026-09-06 a dozen of those arrived at once behind a dead daemon. A
38
+ * request nothing will adopt is withdrawn (reason `timeout`), which authorizes
39
+ * nothing, refuses a late tap in the approver's own words, and leaves the log
40
+ * saying exactly what happened: the asker gave up.
41
+ *
42
+ * Five minutes because it is longer than any retry loop this runtime has been
43
+ * observed to take and shorter than the shortest TTL an operator is likely to
44
+ * write. It is a default: `--retry-grace` moves it per invocation, and `0`
45
+ * withdraws at the moment the wait expires.
46
+ */
47
+ export declare const HOOK_RETRY_GRACE_MS: number;
48
+ /**
49
+ * How old a request opened under `waitMs` must be before the grace has run out.
50
+ *
51
+ * Measured from the `approval.requested` record's own timestamp, which is the
52
+ * instant the runtime wrote at the write boundary, and never from anything a
53
+ * caller states about when it started waiting.
54
+ */
55
+ export declare function abandonedAfterMs(waitMs: number, graceMs: number): number;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The bounded head-moved retry, in one place (APRV-150, APRV-236).
3
+ *
4
+ * ## The fact this module is about
5
+ *
6
+ * `core/log.ts` takes a compare-and-append precondition: a writer states the
7
+ * `(seq, hash)` its checks were made against, and the append is refused under
8
+ * the lock when the tail is something else. That refusal is `append-failed`
9
+ * with `append.code === "head-moved"`, and it is correct: every read-dependent
10
+ * check that authorized the write is stale.
11
+ *
12
+ * Stale is not the same as wrong. A moved head says the verdict must be
13
+ * computed again; it does not say what the verdict is. Treating the two as the
14
+ * same is what turned a busy log into a lottery, twice:
15
+ *
16
+ * - 2026-08-29 (APRV-150): a session's first `git status` — class `read.shell`,
17
+ * autonomous, no human anywhere near it — was denied by the Claude Code hook
18
+ * because an unrelated record landed a few milliseconds earlier.
19
+ * - 2026-09-02 (APRV-236): `approval grant` refused a human's decision with
20
+ * `head moved: expected seq 14218, found 14219` while two lanes and the
21
+ * daemon were appending. The human ran it again. Twice more.
22
+ *
23
+ * The second one is the worse of the two, because the caller it handed the
24
+ * retry to was a person. A compare-and-append refusal is a fact about timing
25
+ * rather than about authority, and asking a human to retype is the wrong party
26
+ * to hand it to.
27
+ *
28
+ * ## What a retry is here, and what it is not
29
+ *
30
+ * The unit of retry is the WHOLE read-check-append cycle: a new read of the
31
+ * verified log, a new read of the policy, a fresh attestation, a fresh
32
+ * derivation of the request's state, fresh escalation, single-use, intake and
33
+ * budget checks, and a new append against the head that the new read observed.
34
+ * Nothing crosses an attempt except the caller's own inputs.
35
+ *
36
+ * The append is never retried on its own. That distinction is the whole safety
37
+ * argument, and it is worth stating as three properties:
38
+ *
39
+ * - **compare-and-append is untouched.** Every attempt supplies the head it
40
+ * read, and a stale write is still refused under the lock. SPEC.md §11.1
41
+ * invariant 5 holds per attempt, which is where it has to hold.
42
+ * - **A changed verdict is the new verdict.** If the record that moved the head
43
+ * decided the request, spent the key, exhausted the budget, lapsed the TTL or
44
+ * re-attested a different policy, the next attempt derives that and refuses
45
+ * it with the code those fresh facts produce — `already-decided`,
46
+ * `request-withdrawn`, `already-executed`, `budget-exceeded`, `expired`,
47
+ * `policy-drift` — never `append-failed`. A retry cannot launder a denial
48
+ * into an allow, because it never replays the earlier conclusion.
49
+ * - **Only `head-moved` retries.** Every other refusal, a real verdict included,
50
+ * is returned on the first attempt. So is a lock timeout, a corrupt log and a
51
+ * schema refusal at the write boundary: retrying those would be either
52
+ * pointless or a second write.
53
+ *
54
+ * The bound is what keeps a busy log from turning one call into an unbounded
55
+ * write loop. When it is spent the last `head-moved` refusal is returned with
56
+ * its code and its `append` error unchanged, and with the attempt count added to
57
+ * its message so a reader can tell one lost race from a log under sustained
58
+ * contention. The caller fails closed on it exactly as it always did.
59
+ */
60
+ import type { AppendError } from "./log.js";
61
+ /**
62
+ * How many times a gate writer re-derives its verdict against a moved head
63
+ * before it gives up. Small, fixed, and not configurable upward.
64
+ *
65
+ * APRV-150 chose three for the hook's writers; APRV-236 gives the same three to
66
+ * every other writer a human or a session drives, rather than inventing a
67
+ * second number for the same fact.
68
+ */
69
+ export declare const HEAD_MOVED_ATTEMPTS = 3;
70
+ /**
71
+ * The shape this module needs of a writer's result: an `ok` discriminator, and
72
+ * on the refusal side a code and the append error beneath it.
73
+ *
74
+ * Deliberately structural. `GateRefusal`, `ExecuteRefusal`, `TokenRefusal` and
75
+ * `GateWindowRefusal` are four frozen unions in four modules that must not
76
+ * learn about each other; what they share is this, and this is all the helper
77
+ * reads.
78
+ */
79
+ export interface HeadRetryable {
80
+ ok: boolean;
81
+ code?: string;
82
+ message?: string;
83
+ append?: AppendError;
84
+ }
85
+ /** Did this refusal come from the compare-and-append precondition alone? */
86
+ export declare function isHeadMoved(result: HeadRetryable): boolean;
87
+ /**
88
+ * The attempt budget for one operation: `ceiling` unless the caller asked for
89
+ * fewer.
90
+ *
91
+ * Clamped rather than trusted, and clamped in the one direction that matters: a
92
+ * caller may ask for LESS tolerance of a moved head (a test pinning the
93
+ * unretried behaviour, a caller that would rather fail fast), never for more.
94
+ * Ambiguity — a non-integer, a zero, a negative — resolves to the value the
95
+ * runtime chose, not the caller's.
96
+ */
97
+ export declare function attemptsOf(asked: number | undefined, ceiling?: number): number;
98
+ /**
99
+ * Run one whole read-check-append cycle, and run it again from the top while it
100
+ * loses the race, up to `attempts` times.
101
+ *
102
+ * `cycle` MUST be the entire operation: it re-reads, re-runs every check on the
103
+ * head it just read, and returns either the append it attempted or the refusal
104
+ * those fresh facts produced. A `cycle` that closes over a stale read would
105
+ * defeat the only property that makes this safe.
106
+ */
107
+ export declare function withHeadRetry<T extends HeadRetryable>(attempts: number, cycle: () => T): T;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Which gate instance this process is talking about (APRV-178).
3
+ *
4
+ * Everything else in this runtime is directory-scoped already: the policy is
5
+ * the `APPROVAL.md` beside the working directory, the log is that directory's
6
+ * `.approval/log/events.jsonl`, the vault and `.approval/env` sit next to it.
7
+ * Two checkouts on one machine are two gates, and nothing they hold is shared.
8
+ *
9
+ * The OS keystore was the exception, and it cost a live incident: the item
10
+ * names were three fixed strings, so a demo instance provisioned in another
11
+ * directory stored its bot token over the production instance's item and then
12
+ * read the production token back. Both gates then long-polled ONE bot, the
13
+ * `getUpdates` offsets fought, and a human's approval tap was consumed by the
14
+ * listener that had not asked for it. A keystore is machine-global; the names
15
+ * put into it have to carry the scoping the filesystem gave everything else.
16
+ *
17
+ * ## What the identity is
18
+ *
19
+ * The absolute path of the instance's `.approval` directory, hashed. Not the
20
+ * repository root (a gate may be configured with `--log` somewhere else), not
21
+ * a random id written into a file (a file the operator can copy is an identity
22
+ * that travels, which is the bug), and not the log path itself (a `--log` that
23
+ * names the same instance's log by a different but equivalent spelling should
24
+ * not mint a second identity, and `envFilePathFor` already normalises the two
25
+ * spellings this runtime supports).
26
+ *
27
+ * Symlinks are deliberately NOT resolved. `realpathSync` would touch the
28
+ * filesystem, so the identity of an instance would depend on whether its
29
+ * directory currently exists — and `approval setup` must be able to name the
30
+ * item it is about to create before anything is on disk. Two paths that reach
31
+ * one directory by different routes are therefore two identities; that is the
32
+ * safe direction of the error (two names, no sharing) rather than the unsafe
33
+ * one.
34
+ *
35
+ * ## What the names look like
36
+ *
37
+ * ```
38
+ * approval-tg-token-3f2a9c11
39
+ * approval-vault-passphrase-3f2a9c11
40
+ * approval-sampling-secret-3f2a9c11
41
+ * ```
42
+ *
43
+ * Eight hex digits: long enough that two instances on one machine will not
44
+ * collide by accident, short enough that an operator reading
45
+ * `keychain:approval-tg-token-3f2a9c11` in `.approval/env` and the same suffix
46
+ * in `approval doctor` can compare them at a glance. The suffix is not a
47
+ * secret and is not derived from one — it is a hash of a directory path, which
48
+ * `.approval/env` already carries in the open.
49
+ */
50
+ import { type ResolvedVariable } from "./env-file.js";
51
+ import type { PolicyLoadResult } from "./policy-load.js";
52
+ /**
53
+ * The unscoped item names this project used before APRV-178.
54
+ *
55
+ * Kept, and kept named, for two reasons: an instance provisioned before the
56
+ * change still has its token under one of them, and the migration path is to
57
+ * READ them as a fallback and say so out loud, never to silently adopt them.
58
+ */
59
+ export declare const LEGACY_SERVICE_TELEGRAM_TOKEN = "approval-tg-token";
60
+ export declare const LEGACY_SERVICE_VAULT_PASSPHRASE = "approval-vault-passphrase";
61
+ export declare const LEGACY_SERVICE_SAMPLING_SECRET = "approval-sampling-secret";
62
+ /** Every unscoped name, for the "is this the legacy one?" test. */
63
+ export declare const LEGACY_SERVICES: readonly string[];
64
+ /** How many hex digits of the digest a scoped name carries. */
65
+ export declare const INSTANCE_ID_LENGTH = 8;
66
+ /**
67
+ * The `.approval` directory this log path belongs to, absolute as given.
68
+ *
69
+ * `envFilePathFor` already answers "which instance is this log in", by walking
70
+ * up out of `log/` when it is there; the env file's own directory IS the
71
+ * instance home, so this is that answer with the filename removed rather than
72
+ * a second rule that could disagree with it.
73
+ */
74
+ export declare function instanceHomeFor(logPath: string): string;
75
+ /** The instance's short identity: {@link INSTANCE_ID_LENGTH} hex digits. */
76
+ export declare function instanceIdFor(logPath: string): string;
77
+ /** The keystore item name `base` takes in the instance owning `logPath`. */
78
+ export declare function scopedService(base: string, logPath: string): string;
79
+ /**
80
+ * How a service name relates to the instance owning `logPath`.
81
+ *
82
+ * Answered from the NAME alone, with no keystore lookup: `approval doctor`
83
+ * reports on this and a diagnostic may not pop a keychain-unlock prompt (see
84
+ * `cli/doctor.ts`'s `NON_RESOLVING_RUNNER`). The name is enough, because the
85
+ * name is what decides which item a lookup would find.
86
+ *
87
+ * - `mine` — the scoped name this instance writes.
88
+ * - `legacy` — one of the unscoped pre-APRV-178 names, which every instance on
89
+ * the machine resolves to the same item.
90
+ * - `other-instance` — a scoped name whose suffix is some other instance's.
91
+ * - `unknown` — a name this runtime never wrote; the operator chose it, and
92
+ * nothing here can say whose it is.
93
+ */
94
+ export type ServiceScope = "mine" | "legacy" | "other-instance" | "unknown";
95
+ export declare function scopeOfService(service: string, logPath: string): ServiceScope;
96
+ /**
97
+ * The variable `approval env`'s export block uses to say what it exported.
98
+ *
99
+ * ## The bug this exists to close
100
+ *
101
+ * The bleed rule below reads no values, on purpose: it runs inside `approval
102
+ * doctor` and `approval up`, neither of which may pop a keystore-unlock dialog
103
+ * to produce a diagnostic (`NON_RESOLVING_RUNNER`). So it saw "this variable is
104
+ * exported, and the file has a line for it" and reported that the value in use
105
+ * was not the one the instance configured. It could not know that. The
106
+ * documented start ritual is
107
+ *
108
+ * ```sh
109
+ * eval "$(approval env)"
110
+ * ```
111
+ *
112
+ * which exports THIS instance's own values from THIS instance's own file, and
113
+ * leaves behind exactly the state the rule was calling an incident: exported,
114
+ * with a file line naming a source. Observed on 2026-09-06 in the primary,
115
+ * where `unset APPROVAL_SAMPLING_SECRET && eval "$(approval env)" && approval
116
+ * up` printed the finding for the variable the eval had just resolved from the
117
+ * gate's own keychain item. A check that asserts a fact it never tested is
118
+ * worse than no check: it teaches the operator to skip the line.
119
+ *
120
+ * ## What is exported, and why it is value-free
121
+ *
122
+ * ```
123
+ * APPROVAL_ENV_PROVENANCE=1:3f2a9c11:<64 hex>:APPROVAL_TG_TOKEN,APPROVAL_TG_CHAT
124
+ * │ │ │ └ the NAMES it exported from the file
125
+ * │ │ └ sha256 of the env file bytes it read
126
+ * │ └ the instance whose file that was
127
+ * └ the format version
128
+ * ```
129
+ *
130
+ * Four colon-separated fields; the names are shell variable names, which cannot
131
+ * contain a colon or a comma, so the shape needs no escaping. Every field is a
132
+ * NAME, an id or a digest: the same three kinds of thing `.approval/env` already
133
+ * carries in the open, and none of them is a value. The rule stays as value-free
134
+ * after this change as it was before it, which is the point — a check that had
135
+ * to read the secret to decide whether to complain about the secret would be a
136
+ * worse trade than the false positive.
137
+ *
138
+ * ## What it does NOT weaken
139
+ *
140
+ * Invariant 7 (no verb loads `.approval/env` implicitly) is untouched: this
141
+ * variable reaches a shell only when a human evaluates `approval env`'s output,
142
+ * exactly like every other line in that block, and no verb gains the ability to
143
+ * read the file for its values. And it cannot launder a foreign export, because
144
+ * `approval env` lists only the names it resolved FROM THE FILE. A value that
145
+ * was already in the environment is re-exported by that block for fidelity, and
146
+ * deliberately left out of this list, so a bled variable stays reported however
147
+ * many times the ritual is run over it.
148
+ *
149
+ * Trusting the variable is safe in the only direction that matters. Anything
150
+ * able to set it in this process's environment could already set the credential
151
+ * variables themselves, so it buys an attacker nothing; and being wrong here
152
+ * silences a WARNING rather than opening a gate. The reverse default — believing
153
+ * the exported value is foreign — is the bug being fixed.
154
+ */
155
+ export declare const ENV_PROVENANCE_VAR = "APPROVAL_ENV_PROVENANCE";
156
+ /** The only format version this build writes, and the only one it reads. */
157
+ export declare const ENV_PROVENANCE_VERSION = "1";
158
+ /** What one `approval env` export block claims about itself. */
159
+ export interface EnvProvenance {
160
+ /** The instance whose env file it resolved from. */
161
+ instanceId: string;
162
+ /** `envFileDigest` of the bytes it resolved from (`core/env-file.ts`). */
163
+ digest: string;
164
+ /** The names it exported FROM that file. Never the ones it passed through. */
165
+ names: ReadonlySet<string>;
166
+ }
167
+ /**
168
+ * The value for {@link ENV_PROVENANCE_VAR}, for an export block that resolved
169
+ * `names` out of the file `digest` identifies.
170
+ */
171
+ export declare function formatEnvProvenance(logPath: string, digest: string, names: readonly string[]): string;
172
+ /**
173
+ * Read the claim back, or `null` for anything this build does not recognise.
174
+ *
175
+ * Strict on every field, and `null` rather than a partial answer: a malformed
176
+ * claim is an unverified one, an unverified one earns no credit, and no credit
177
+ * means the finding is still reported. That is the fail-closed direction (the
178
+ * check stays loud) for a value whose whole job is to make the check quieter.
179
+ */
180
+ export declare function parseEnvProvenance(raw: string | undefined): EnvProvenance | null;
181
+ /**
182
+ * One way this instance's credentials can belong to somebody else.
183
+ *
184
+ * - `foreign-instance` — `.approval/env` names a keystore item whose scope
185
+ * suffix is another instance's. Two gates then hold one credential, which is
186
+ * the incident: one bot, two long pollers, and a human's tap consumed by the
187
+ * listener that did not ask the question. This is WRONG, not a state.
188
+ * - `legacy-shared` — the file names one of the unscoped pre-APRV-178 items.
189
+ * Correct on a machine with one gate and a trap on a machine with two, so it
190
+ * is reported and not failed: it is what every existing installation looks
191
+ * like, and the repair is one re-run of `approval setup channel telegram`.
192
+ * - `ambient-bleed` — the value was exported before this process started, the
193
+ * file names a source of its own, and the export is NOT one this instance's
194
+ * `approval env` made (see {@link ENV_PROVENANCE_VAR}). The shell wins on
195
+ * purpose (invariant 7); what was missing is anyone saying so. This is the
196
+ * half of the incident that survived fixing the file: the operator's rc
197
+ * exported the production token, so every fresh terminal kept using the
198
+ * production bot.
199
+ */
200
+ export type InstanceFindingKind = "foreign-instance" | "legacy-shared" | "ambient-bleed";
201
+ export interface InstanceFinding {
202
+ kind: InstanceFindingKind;
203
+ /** The environment variable the finding is about. */
204
+ variable: string;
205
+ /** The item name, for the two name-shaped findings. */
206
+ service?: string;
207
+ /** One sentence, and never a value. */
208
+ detail: string;
209
+ }
210
+ /**
211
+ * What is wrong with WHOSE credentials this instance is using (APRV-178).
212
+ *
213
+ * Answered from names alone. No keystore is consulted ({@link
214
+ * NON_RESOLVING_RUNNER}), so this may be called from `approval doctor` and from
215
+ * `approval up`'s start-up without either of them blocking on an unlock dialog.
216
+ * No value is read, compared or printed on any path: the inputs are a service
217
+ * name, a scheme, a variable name, and (since APRV-278) an instance id, a file
218
+ * digest and a list of variable names out of {@link ENV_PROVENANCE_VAR} — all of
219
+ * which `.approval/env` and `approval env --check` carry in the open. The
220
+ * exported VALUES this reports on are still never read, compared or printed.
221
+ *
222
+ * A file this runtime cannot read at all produces no findings rather than a
223
+ * guess; `approval env --check` and doctor's `environment` row are what report
224
+ * an unreadable or wrong-moded file, and saying it twice in different words is
225
+ * how two commands come to disagree.
226
+ */
227
+ export declare function instanceFindings(logPath: string, load: PolicyLoadResult, ambientEnv?: NodeJS.ProcessEnv): InstanceFinding[];
228
+ /**
229
+ * What the rule may consult beyond the names (APRV-278).
230
+ *
231
+ * Both fields are optional and an absent one only ever makes the report LOUDER:
232
+ * a caller that supplies neither gets the pre-APRV-278 behaviour, where every
233
+ * exported variable with a file line is reported.
234
+ */
235
+ export interface FindingsContext {
236
+ /** The environment the report is about. Read for {@link ENV_PROVENANCE_VAR}. */
237
+ ambientEnv?: NodeJS.ProcessEnv;
238
+ /** `envFileDigest` of this instance's env file as it now reads. */
239
+ envFileDigest?: string;
240
+ }
241
+ /**
242
+ * The exported names this instance's OWN current `approval env` vouches for
243
+ * (APRV-278).
244
+ *
245
+ * Empty unless {@link ENV_PROVENANCE_VAR} parses, names this instance, and
246
+ * matches the env file as it now reads. `approval env --check` and the finding
247
+ * rule below both answer "is this export the documented ritual's?" from this one
248
+ * function, because two spellings of that question are two chances for `approval
249
+ * env --check` and `approval up` to say different things about one shell.
250
+ */
251
+ export declare function ownEnvExports(logPath: string, context?: FindingsContext): ReadonlySet<string>;
252
+ /** The name-only rules, over an already-resolved variable set. */
253
+ export declare function findingsFor(logPath: string, variables: readonly ResolvedVariable[], context?: FindingsContext): InstanceFinding[];