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,64 @@
1
+ /**
2
+ * The dark-session sweep (APRV-192): the appending half, and only that.
3
+ *
4
+ * `core/dark-session.ts` observes git, resolves the payload store and reaches
5
+ * the verdict. This module does the one thing a reader must not: it writes the
6
+ * observation down. The split is not decorative — `approval doctor` reports the
7
+ * same findings from the same evaluator and must append nothing, and CLAUDE.md
8
+ * names the daemon as the log's single writer.
9
+ *
10
+ * ## What it appends, and what it deliberately does not
11
+ *
12
+ * A `dark` verdict appends one `audit.dark_session` (SPEC.md §8's event set,
13
+ * amended APRV-192), actor `system:daemon`, through `appendEvent` with
14
+ * `expectedHead` (§11.1 invariant 5) and a `ts` read from the injected clock at
15
+ * the write boundary (§11.1 invariant 2). The daemon records what IT saw, as
16
+ * itself; it never writes a record on behalf of the session it is reporting on,
17
+ * which would be fabricating exactly the evidence whose absence is the finding.
18
+ *
19
+ * An `undetermined` verdict appends NOTHING. It is reported on the daemon's
20
+ * event stream, as a warning, and by `approval doctor`, which is where an
21
+ * operator goes to ask what the runtime cannot see. The reasoning is the one
22
+ * `daemon/audit.ts` gives for a disabled sampler: a condition that recurs every
23
+ * tick and that nobody can act on from the record alone is noise, and a log full
24
+ * of "I could not tell" is a log people stop reading. Fail-closed binds the
25
+ * REPORT — an undetermined subject never counts as a pass and never clears
26
+ * `settled` — and not the append.
27
+ *
28
+ * ## Idempotent without remembering anything
29
+ *
30
+ * Same discipline as the audit sweep: no cursor, no seen-set, no state. Each
31
+ * finding carries an observation key (subject plus the state of the world it was
32
+ * seen in), and a key the verified log already carries is not appended again.
33
+ * A restarted daemon, a second daemon and an operator running a sweep by hand
34
+ * all converge on the same set, and a worktree that stays dark across a hundred
35
+ * ticks produces one record until it commits something new.
36
+ */
37
+ import { type DarkSessionFinding, type DarkSessionReport, type DarkSessionSweepOptions } from "../core/dark-session.js";
38
+ export type { DarkSessionSweepOptions, DarkSessionWatch } from "../core/dark-session.js";
39
+ export { DEFAULT_DARK_INTERVAL_MS, DEFAULT_DARK_WINDOW_MS, reportDarkSessions, } from "../core/dark-session.js";
40
+ /**
41
+ * SPEC.md §8: runtime-originated events carry a `system:` actor, and the daemon
42
+ * is the runtime. The same actor `envelope.drift` is written under, because the
43
+ * same process observed both.
44
+ */
45
+ export declare const DARK_SESSION_ACTOR = "system:daemon";
46
+ export interface DarkSessionSweepResult {
47
+ report: DarkSessionReport;
48
+ /** Findings that produced a new `audit.dark_session`, with its seq. */
49
+ appended: readonly {
50
+ finding: DarkSessionFinding;
51
+ seq: number;
52
+ }[];
53
+ /** Dark findings whose observation key the log already carried. */
54
+ repeated: readonly DarkSessionFinding[];
55
+ /** Findings that could not be established, with their codes. */
56
+ undetermined: readonly DarkSessionFinding[];
57
+ /** Appends that were refused, as messages for the caller's warning channel. */
58
+ refusals: readonly string[];
59
+ }
60
+ /**
61
+ * One sweep: observe, judge, append what is new. Decides nothing of its own —
62
+ * the verdict is `core/dark-session.ts`'s and the evidence rule is APRV-151's.
63
+ */
64
+ export declare function sweepDarkSessions(options: DarkSessionSweepOptions): DarkSessionSweepResult;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The live draw's relay child (APRV-208).
3
+ *
4
+ * ## Why a child process exists at all
5
+ *
6
+ * `core/gate.ts`'s request path is synchronous from end to end — `request`
7
+ * returns a value, the hook's wait loop sleeps synchronously — and Node has no
8
+ * synchronous Unix-socket client. APRV-188 hit the same wall and answered it
9
+ * with a published file, because the thing it needed (the log's verified head)
10
+ * is a fact the daemon can publish in advance. A live draw is not: the question
11
+ * contains bytes that did not exist when the daemon last woke up. It has to be
12
+ * asked.
13
+ *
14
+ * So: one `spawnSync` of this file, about 20-40 ms of Node start, paid only by a
15
+ * `supervised-live` class in a process with no sampling secret. It is off the
16
+ * pass-through path entirely.
17
+ *
18
+ * ## What it is allowed to do
19
+ *
20
+ * Open a socket, write one line, read one line, print it. That is the whole
21
+ * remit, and it is written to be worth nothing if it were replaced wholesale:
22
+ * it holds no secret, appends nothing, decides nothing, and the parent
23
+ * VALIDATES every field of what it relays (`core/live-draw.ts`'s
24
+ * `parseDrawAnswer`) rather than trusting it. A hostile relay can refuse to
25
+ * answer — which gates the action, which is the safe direction — and can put
26
+ * nothing past the parent that the daemon's MAC does not cover.
27
+ *
28
+ * ## The protocol
29
+ *
30
+ * `argv[2]` is the socket path. stdin is one JSON line, the question. stdout is
31
+ * one JSON line: `{"ok":true,"answer":{...}}` verbatim from the daemon, or
32
+ * `{"ok":false,"reason":"...","detail":"..."}` with one of
33
+ * `core/live-draw.ts`'s refusal reasons. Exit code is always 0: the parent
34
+ * reads the line, not the status.
35
+ */
36
+ export {};
@@ -0,0 +1,154 @@
1
+ /**
2
+ * The daemon's live-draw server (APRV-208).
3
+ *
4
+ * The daemon is the one process on this machine that legitimately holds the
5
+ * sampling secret: an operator starts it in a terminal where `eval "$(approval
6
+ * env)"` has run, and `core/child-env.ts` (APRV-205) strips `APPROVAL_*` from
7
+ * every child it spawns, so the secret goes no further. This module lets short
8
+ * lived gate processes that hold no secret — the harness hook, above all — ASK
9
+ * for a draw instead of failing closed on every action of a `supervised-live`
10
+ * class. `core/live-draw.ts` carries the protocol and the argument; this is the
11
+ * server half.
12
+ *
13
+ * ## Three things this server refuses to take on the asker's word
14
+ *
15
+ * 1. **The action.** It answers only for an `action_key` already carried by a
16
+ * `task.registered` record in the VERIFIED log, declaring exactly this
17
+ * `payload_hash`. That is what stops the socket being a private oracle: an
18
+ * agent that wants to grind candidate payloads until one draws unsampled must
19
+ * append a registration for each candidate first, in the append-only log,
20
+ * where the attempt is permanent and countable. See `core/live-draw.ts`'s
21
+ * header on why this matters more than the socket's file mode does.
22
+ * 2. **The rate.** It resolves the action's class against its own policy and
23
+ * uses the `live_rate` IT derives. A `live_rate` in the question is a
24
+ * proposal, never an instruction; the derived one is echoed back and the
25
+ * asker refuses an answer to a different question.
26
+ * 3. **The policy.** Same: it checks its own attestation and echoes its own
27
+ * `policy_sha256`. A daemon and a hook reading different policy files fail
28
+ * closed rather than agreeing on the wrong rules.
29
+ *
30
+ * A fresh verified read per question is what makes (1) usable at all: the hook
31
+ * registers the task microseconds before it asks, and a daemon answering out of
32
+ * a tick's stale memory would refuse every real request. The read is cheap for
33
+ * the reason APRV-188 exists — the daemon's cache is warm and only the appended
34
+ * tail is walked.
35
+ *
36
+ * ## The secret
37
+ *
38
+ * Held in one private field, closed over by nothing, and never emitted: not in
39
+ * an answer, not in a warning, not in the `started` line. What leaves this
40
+ * process is a verdict and a MAC.
41
+ */
42
+ import { type LiveSelectorUnavailableReason } from "../core/sampler.js";
43
+ /**
44
+ * The server this environment can run, or the reason it cannot (APRV-208).
45
+ *
46
+ * ## Where the secret comes from, and why that is the whole opt-in
47
+ *
48
+ * From the daemon's OWN environment, and nowhere else. `.approval/env` is a
49
+ * source MAP and `core/env-file.ts`'s load-bearing rule is that no verb reads it
50
+ * into its own environment (the reason is `APPROVAL_HUMAN`: a working-tree file
51
+ * that could set identity would put the human-only gate one `echo >>` away from
52
+ * being nobody's gate). That rule is not weakened here. The operator runs `eval
53
+ * "$(approval env)"` in the terminal they start the daemon from — the same act
54
+ * `approval setup sampling` already documents, resolving the keychain item or the
55
+ * env file's declared source — and this reads what that established.
56
+ *
57
+ * That act is also the feature's opt-in, which is why there is no flag to turn
58
+ * it on. Serving draws changes VERDICTS: a class an operator declared
59
+ * `supervised-live` at 0.1 goes from gating 100% to gating 10%, and a default
60
+ * that moved that under someone on an upgrade would be the surprise this project
61
+ * exists to prevent. But it moves only for an operator who has exported the
62
+ * secret into the daemon's shell on purpose, having already amended their policy
63
+ * to declare the class live. Both halves are deliberate acts by the human. What
64
+ * exists instead is a way OUT — `approval daemon run --no-draw` — because the
65
+ * one thing an operator must always be able to do is take a control back.
66
+ */
67
+ export declare function drawServerFor(options: Omit<DrawServerOptions, "secret"> & {
68
+ env?: NodeJS.ProcessEnv;
69
+ }): {
70
+ ok: true;
71
+ server: DrawServer;
72
+ } | {
73
+ ok: false;
74
+ reason: DrawUnavailableReason;
75
+ message: string;
76
+ };
77
+ /**
78
+ * Why {@link drawServerFor} built no server.
79
+ *
80
+ * The four inherited from `core/sampler.ts` are all worth telling an operator
81
+ * about: each one means a class they declared live is gating at 100%.
82
+ * `no-live-class` is the fifth and the only SILENT one, because it means the
83
+ * operator never asked for any of this — the reason a caller must branch on it
84
+ * rather than print every refusal alike.
85
+ */
86
+ export type DrawUnavailableReason = LiveSelectorUnavailableReason | "no-live-class";
87
+ /** Why the server declined to listen. Reported, never fatal to the daemon. */
88
+ export type DrawServeRefusal = "path-too-long" | "directory-unusable" | "listen-failed";
89
+ export interface DrawServerOptions {
90
+ logPath: string;
91
+ policy: {
92
+ dir?: string;
93
+ file?: string;
94
+ };
95
+ schemaDir?: string;
96
+ /** The operator's sampling secret, resolved by the caller from its own env. */
97
+ secret: string;
98
+ /** The clock for `answered_at`, which is outside the MAC and never logged. */
99
+ now?: () => string;
100
+ }
101
+ export type DrawServeResult = {
102
+ ok: true;
103
+ path: string;
104
+ } | {
105
+ ok: false;
106
+ reason: DrawServeRefusal;
107
+ detail: string;
108
+ };
109
+ /**
110
+ * A server that answers live draws for one log, over one owner-only socket.
111
+ *
112
+ * Started and stopped by the daemon loop. Every failure is a refusal to serve,
113
+ * never a throw into the loop: a daemon that could not open a socket must keep
114
+ * doing its job, and every asker fails closed to a human decision, which is
115
+ * exactly what happens today with no daemon at all.
116
+ */
117
+ export declare class DrawServer {
118
+ private readonly options;
119
+ private server;
120
+ private answered;
121
+ private refused;
122
+ constructor(options: DrawServerOptions);
123
+ /** How many questions were answered and refused, for the daemon's report. */
124
+ stats(): {
125
+ answered: number;
126
+ refused: number;
127
+ };
128
+ start(): DrawServeResult;
129
+ close(): void;
130
+ private serve;
131
+ /**
132
+ * Answer one question, or refuse it.
133
+ *
134
+ * Returns the answer body; refusals go through `refuse` and this returns
135
+ * `undefined`, which `answer` above ignores because `done` is already set.
136
+ */
137
+ /** Where this daemon reads its policy from. One spelling, two readers. */
138
+ private policyWhere;
139
+ /**
140
+ * This daemon's report on its OWN sampler (APRV-271).
141
+ *
142
+ * Read through `core/sampler.ts` from this process's environment, which is
143
+ * the whole point: the operator exported the secret HERE, and every other
144
+ * process on the machine has had `APPROVAL_*` stripped from it. It carries
145
+ * the variable's NAME and the rate, both of which the policy file already
146
+ * states in the open, and never the value.
147
+ *
148
+ * No verified read, no attestation check and no MAC: this answer decides
149
+ * nothing. `core/live-draw.ts`'s header sets out what a caller may and may
150
+ * not conclude from it.
151
+ */
152
+ private samplingReport;
153
+ private decide;
154
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Per-event git commits — SPEC.md §8's optional hardening (APRV-42).
3
+ *
4
+ * > "Optionally, the log directory is a git repo and the daemon commits per
5
+ * > event with its own identity, giving signed, distributed tamper evidence for
6
+ * > free."
7
+ *
8
+ * The chain in `events.jsonl` already detects mutation and truncation on its
9
+ * own. What a git repository adds is a *second, independent* record of the same
10
+ * bytes: a mutation that is plausible against one layer has to be plausible
11
+ * against the other at the same time, and the second layer is one an operator
12
+ * can clone, mirror, and diff from somewhere the tamperer does not control.
13
+ * Neither layer is trusted to police the other, which is the point of having
14
+ * two: `approval log verify` never consults git, and nothing here ever reads a
15
+ * verdict out of a commit.
16
+ *
17
+ * ## Opt-in, and only for a standalone log deployment
18
+ *
19
+ * This is off unless the operator asks for it, and refuses to turn on unless the
20
+ * log's own directory is a repository *root*. The two layouts do not mix:
21
+ *
22
+ * - **Standalone log deployment.** The log home (`.approval/`, or whatever
23
+ * directory holds the log when it is not under a `log/` folder) is its own git
24
+ * repository, containing the log, the payload store, and nothing else. Enable
25
+ * the opt-in here.
26
+ * - **Nested project layout** — this repository's own dogfood arrangement, where
27
+ * `.approval/` is committed as part of a larger project repo. Perfectly valid,
28
+ * and the opt-in is REFUSED for it.
29
+ *
30
+ * Why refuse the nested case rather than make it work? Because a hash chain does
31
+ * not survive a merge. Two branches that each append independently produce a
32
+ * chain that is corrupt by construction, and no merge strategy repairs the
33
+ * semantics. An outer repository's ordinary history operations — rebase, amend,
34
+ * squash, force-push, `filter-branch` — rewrite the very bytes the evidence is
35
+ * made of, and a daemon committing into someone else's branch would be a second
36
+ * writer to a history the project's humans also write. Evidence that the subject
37
+ * of the investigation can rewrite is not evidence. So the runtime does not try
38
+ * to be clever about it: own-root repository, or no git evidence at all.
39
+ *
40
+ * ## What it does, precisely
41
+ *
42
+ * After every tick in which the log moved, the daemon calls {@link
43
+ * GitEvidenceRecorder.commit} with the verified head it just read. The recorder
44
+ * stages the log file and the payload store, and commits with a message naming
45
+ * the head's `(seq, hash)`. Batching is **one commit per tick**, not literally
46
+ * one per event: ticks are the only moment the daemon has a verified head in
47
+ * hand, and a commit is only meaningful against a head that verified. A tick
48
+ * that observed three appends produces one commit naming the new head and the
49
+ * number of records it covers — the intermediate states are still fully
50
+ * recoverable from the log itself, which is the artifact being witnessed.
51
+ *
52
+ * ## What it deliberately does not do
53
+ *
54
+ * It never pushes, never fetches, never creates or moves a branch by name, never
55
+ * touches a remote, and never writes to any git config outside the single
56
+ * `git commit` invocation (identity is passed with `-c`, per command, so the
57
+ * operator's `user.name` is neither read into the commit nor overwritten on
58
+ * disk). Commits are local, to the log's own repository, authored by the daemon
59
+ * as itself. A failure to commit is a warning, never a stop: git evidence is
60
+ * hardening on top of the chain, and a daemon that halted approvals because a
61
+ * disk was full of git objects would have converted a redundancy into a
62
+ * dependency.
63
+ */
64
+ import type { LogHead } from "../core/log.js";
65
+ /**
66
+ * The daemon's own git identity. Never the operator's: a commit that says a
67
+ * human made it is a false statement about who wrote the evidence, and the whole
68
+ * value of the second layer is that its authorship is unambiguous.
69
+ *
70
+ * The version is pinned to `package.json` by a test rather than read at runtime,
71
+ * because a module that resolves its own package root differs between the source
72
+ * tree and the build output for no benefit at all.
73
+ */
74
+ export declare const APPROVALD_VERSION = "0.2.0";
75
+ /** `user.name` on every commit this module makes. */
76
+ export declare const GIT_EVIDENCE_AUTHOR_NAME = "approvald 0.2.0";
77
+ /** `user.email` on every commit: fixed, and deliberately undeliverable. */
78
+ export declare const GIT_EVIDENCE_AUTHOR_EMAIL = "approvald@noreply.approval.md";
79
+ /**
80
+ * Why enabling git evidence was refused. **Frozen union**, additive-only, in the
81
+ * same sense as every other refusal vocabulary in this codebase (SPEC.md §11.1
82
+ * invariant 6: refusals are machine-readable and distinct). An operator's
83
+ * supervisor branches on these to tell "install git" apart from "your layout is
84
+ * wrong", and the repair differs for every one of them.
85
+ */
86
+ export declare const GIT_EVIDENCE_REFUSAL_CODES: readonly [
87
+ /** No usable `git` on PATH. The daemon runs fine without the opt-in. */
88
+ "git-unavailable",
89
+ /** The directory that would hold the evidence repository does not exist. */
90
+ "log-dir-missing",
91
+ /** It exists and is not a git repository. The repair is `git init` there. */
92
+ "log-dir-not-repo",
93
+ /**
94
+ * It is inside a working tree it does not own — either some outer repository
95
+ * tracks it, or it is a repository whose root is somewhere above. Hash chains
96
+ * do not survive merges and an outer history rewrites evidence.
97
+ */
98
+ "log-dir-nested"];
99
+ export type GitEvidenceRefusalCode = (typeof GIT_EVIDENCE_REFUSAL_CODES)[number];
100
+ export interface GitEvidenceRefusal {
101
+ ok: false;
102
+ code: GitEvidenceRefusalCode;
103
+ message: string;
104
+ }
105
+ /**
106
+ * One line of git-evidence output. Its own frozen shape, reported through the
107
+ * callback the CLI supplies, so that `daemon/daemon.ts`'s event union stays
108
+ * exactly as it was: the hardening layer speaks for itself and the loop's
109
+ * contract is untouched.
110
+ */
111
+ export type GitEvidenceEvent = {
112
+ event: "git_evidence";
113
+ /** The abbreviated commit this tick produced. */
114
+ commit: string;
115
+ /** The verified head the commit witnesses. */
116
+ seq: number;
117
+ hash: string;
118
+ /**
119
+ * Log lines added since the commit this one builds on — the batch size.
120
+ * The log is append-only, so a line is a record. `null` only when git
121
+ * declined to say, which is a reporting gap and never a correctness one.
122
+ */
123
+ records: number | null;
124
+ } | {
125
+ event: "git_evidence_failed";
126
+ /** The git invocation that failed, as a bare subcommand name. */
127
+ step: string;
128
+ message: string;
129
+ };
130
+ /** Where git-evidence output goes. Injected, so this module writes to nothing. */
131
+ export type GitEvidenceSink = (event: GitEvidenceEvent) => void;
132
+ /**
133
+ * The hook the daemon calls. One method, taking the verified head of the log it
134
+ * just read, so that the loop's edit is a single line and no scheduling
135
+ * knowledge leaks into this file.
136
+ */
137
+ export interface GitEvidenceRecorder {
138
+ commit(head: LogHead | null): void;
139
+ }
140
+ /**
141
+ * The directory that must be the evidence repository's root, for a given log.
142
+ *
143
+ * The same rule `payloadStoreDirFor` uses, for the same reason: SPEC.md §9 fixes
144
+ * the log at `<home>/log/events.jsonl` and the payload store at
145
+ * `<home>/payloads/`, so the only directory that contains *both* is `<home>`.
146
+ * Rooting the repository at the log's immediate directory would leave every
147
+ * payload file outside the evidence, which is precisely where a tamperer would
148
+ * then work. When a caller points `--log` somewhere flatter, the log's own
149
+ * directory is the home and the rule still holds.
150
+ */
151
+ export declare function evidenceRootFor(logPath: string): string;
152
+ export type EnableGitEvidenceResult = {
153
+ ok: true;
154
+ root: string;
155
+ recorder: GitEvidenceRecorder;
156
+ } | GitEvidenceRefusal;
157
+ /**
158
+ * Check every precondition and, when they all hold, build the recorder.
159
+ *
160
+ * Fail closed and fail *loudly*: each refusal is distinct, names the directory
161
+ * it judged, and states the repair. An operator who mistyped `--log` and an
162
+ * operator whose log lives inside a project repository need different sentences,
163
+ * and a single "git evidence unavailable" would have sent both of them looking
164
+ * in the wrong place.
165
+ */
166
+ export declare function enableGitEvidence(logPath: string, sink: GitEvidenceSink): EnableGitEvidenceResult;
167
+ /**
168
+ * The commit message: the head's `(seq, hash)` on the subject line, so a reader
169
+ * of `git log --oneline` can check a commit against the chain without opening
170
+ * anything, and the batch size in the body so the per-tick batching is visible
171
+ * rather than inferred.
172
+ */
173
+ export declare function message(head: LogHead, records: number | null): string;
@@ -75,7 +75,7 @@ import { PAYLOAD_STORE_DIRNAME } from "../core/payload-store.js";
75
75
  * because a module that resolves its own package root differs between the source
76
76
  * tree and the build output for no benefit at all.
77
77
  */
78
- export const APPROVALD_VERSION = "0.1.0";
78
+ export const APPROVALD_VERSION = "0.2.0";
79
79
  /** `user.name` on every commit this module makes. */
80
80
  export const GIT_EVIDENCE_AUTHOR_NAME = `approvald ${APPROVALD_VERSION}`;
81
81
  /** `user.email` on every commit: fixed, and deliberately undeliverable. */
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The daemon's pure projections (SPEC.md §6.3, §10.2).
3
+ *
4
+ * Everything the daemon *decides* lives here, and everything it *does* lives in
5
+ * `daemon.ts`. The split is the same one `core/loop.ts` draws for loop safety:
6
+ * a projection over verified records is a pure function of (records, instant,
7
+ * TTL), so it can be replayed, unit-tested against an injected clock, and never
8
+ * disagrees with itself between two callers.
9
+ *
10
+ * Nothing here reads the clock, the filesystem, or the network, and nothing
11
+ * here appends. Three questions are answered:
12
+ *
13
+ * 1. **What does the log say a task's envelope `state:` should be?**
14
+ * {@link taskEnvelopeState}. §6.3: "`state` is a projection of log events;
15
+ * the file is updated by the daemon after the event is appended, never the
16
+ * reverse." The daemon compares the file's claim against this answer, and a
17
+ * file that contradicts it is `envelope.drift`.
18
+ * 2. **Which live requests have lapsed with no `approval.expired` on record?**
19
+ * {@link lapsedRequests}. The gate already judges TTL lazily at decision
20
+ * time (a late grant is refused whether or not an expiry event exists), so
21
+ * the sweep changes no verdict; it makes the verdict *visible* in the log
22
+ * and in every projection built from it.
23
+ * 3. **Has this exact drift already been recorded?** {@link driftAlreadyLogged}.
24
+ * A watcher fires on every save and the periodic tick re-scans regardless,
25
+ * so without a dedupe rule one unfixed file would append an unbounded run of
26
+ * identical events. The rule is stated on that function.
27
+ *
28
+ * The state derivation itself is NOT reimplemented here: every action's state
29
+ * comes from `core/state.ts`'s `requestState`, the same derivation the gate,
30
+ * the token module, and the executor read. This module only rolls action states
31
+ * up to the task level, and that rollup is the one new rule it owns.
32
+ */
33
+ import type { EventRecord } from "../core/log.js";
34
+ import { type RequestDerivation } from "../core/state.js";
35
+ /**
36
+ * The envelope's `state:` vocabulary (`envelope.schema.json`, SPEC.md §6.3).
37
+ *
38
+ * Spelled here rather than imported from the schema because the schema is data
39
+ * read at runtime and this is a type; `tests/daemon-projection.test.ts` pins the
40
+ * two against each other so they cannot drift apart.
41
+ */
42
+ export declare const ENVELOPE_STATES: readonly ["proposed", "awaiting", "approved", "executed", "rejected", "expired", "revoked"];
43
+ export type EnvelopeState = (typeof ENVELOPE_STATES)[number];
44
+ /** Is this the envelope's `state:` value? Used to read an untrusted file. */
45
+ export declare function isEnvelopeState(value: unknown): value is EnvelopeState;
46
+ /** One declared action of a task, as the log records it, with its derived state. */
47
+ export interface ActionProjection {
48
+ actionKey: string;
49
+ derivation: RequestDerivation;
50
+ }
51
+ /** What the log says about one task. */
52
+ export interface TaskProjection {
53
+ task: string;
54
+ /** A `task.registered` record exists for this task id. */
55
+ registered: boolean;
56
+ /** The envelope `state:` the log implies (SPEC.md §6.3). */
57
+ state: EnvelopeState;
58
+ /** Every action key the latest registration declared, with its derivation. */
59
+ actions: ActionProjection[];
60
+ }
61
+ /** Every action key the latest `task.registered` for `task` declared, in order. */
62
+ export declare function registeredActionKeys(records: EventRecord[], task: string): string[];
63
+ /**
64
+ * The envelope `state:` the log implies for `task`, at instant `ts`.
65
+ *
66
+ * ## The rollup rule, stated once
67
+ *
68
+ * §6.3's lifecycle is drawn for one action; a task may declare several, so the
69
+ * projection needs a rule for a task whose actions disagree. It is this, in
70
+ * order, and the order is the point:
71
+ *
72
+ * 1. any action with a live request → **awaiting**. A human owes an answer, and
73
+ * that fact outranks every other, because it is the only state that asks
74
+ * something of a person.
75
+ * 2. else any granted action that has not executed → **approved**.
76
+ * 3. else any action with an `execution.completed` → **executed**.
77
+ * 4. else **revoked**, then **rejected**, then **expired** — human decisions
78
+ * outrank a runtime lapse, so a task with one revoked and one expired action
79
+ * reads as revoked, naming the decision a person actually made.
80
+ * 5. else **proposed**: registered (or not) and nothing has happened.
81
+ *
82
+ * A task with exactly one action — the common case — collapses to §6.3's
83
+ * lifecycle exactly, which is the property the rule was chosen for.
84
+ *
85
+ * An unregistered task projects to `proposed`: the log knows of no declaration,
86
+ * so nothing has been proposed *to the gate* yet. A file claiming `approved`
87
+ * for a task the log never registered therefore contradicts the log, which is
88
+ * exactly the reading §6.3 wants.
89
+ */
90
+ export declare function taskEnvelopeState(records: EventRecord[], task: string, ts: string, ttlMs: number | null): TaskProjection;
91
+ /** One request the TTL sweep would materialise an `approval.expired` for. */
92
+ export interface LapsedRequest {
93
+ actionKey: string;
94
+ task: string | null;
95
+ /** The `ts` of the `approval.requested` that lapsed. */
96
+ requestedTs: string | null;
97
+ }
98
+ /**
99
+ * Live requests whose TTL has lapsed and which carry no `approval.expired`.
100
+ *
101
+ * Idempotence is structural rather than remembered: the answer is re-derived
102
+ * from the verified log every sweep, and an action whose expiry has been
103
+ * appended no longer satisfies the predicate. Two sweeps over the same log
104
+ * therefore produce the same list, and a sweep that follows a successful one
105
+ * produces an empty list — with no state carried between them, and no way for a
106
+ * restarted daemon to expire something twice.
107
+ *
108
+ * `ttlMs === null` (a policy that declares no `defaults.approval_ttl`) yields no
109
+ * candidates at all: nothing lapses when nothing was bounded, and inventing a
110
+ * deadline is not the daemon's to invent.
111
+ *
112
+ * Returned in first-request order, so a sweep's appends land in a deterministic
113
+ * sequence.
114
+ */
115
+ export declare function lapsedRequests(records: EventRecord[], ts: string, ttlMs: number | null): LapsedRequest[];
116
+ /**
117
+ * Why an `envelope.drift` record was written (APRV-63).
118
+ *
119
+ * `state-mismatch` is the original reading of SPEC.md §6.3: the file makes a
120
+ * claim about `state:` and the log implies another one. `envelope-missing` is
121
+ * the loss case observed live in APRV-60: the log holds a `task.registered` for
122
+ * the task, and the file that declared it now carries no `approval:` key at all.
123
+ * They are separated because they call for different human actions — one is an
124
+ * edit to reconcile, the other is a *deletion to restore* — and a reader who
125
+ * could not tell them apart would treat a lost envelope as a stale one.
126
+ *
127
+ * `reason` is absent from records written before this vocabulary existed;
128
+ * readers (including {@link driftAlreadyLogged}) treat absence as
129
+ * `state-mismatch`, which is what every such record was.
130
+ */
131
+ export declare const DRIFT_REASONS: readonly ["state-mismatch", "envelope-missing"];
132
+ export type DriftReason = (typeof DRIFT_REASONS)[number];
133
+ export { latestRegistration } from "../core/registration.js";
134
+ /** The facts one `envelope.drift` record carries, and the dedupe key. */
135
+ export interface DriftFacts {
136
+ /** The `state:` the file claims, or `null` when it declares none. */
137
+ declaredState: string | null;
138
+ /** The state the log implies (SPEC.md §6.3). */
139
+ derivedState: EnvelopeState;
140
+ /**
141
+ * SHA-256 over the RFC 8785 form of the file's whole `approval:` envelope, or
142
+ * `null` when the envelope could not be canonicalized. Part of the dedupe key
143
+ * so that editing a file from one contradiction into a different one is a new
144
+ * drift and not a suppressed one.
145
+ */
146
+ envelopeDigest: string | null;
147
+ /**
148
+ * Which kind of drift this is (APRV-63). Part of the dedupe key: a file that
149
+ * contradicts the log and a file that lost its envelope are different facts
150
+ * about the same task, and neither may suppress the other. Absent means
151
+ * `state-mismatch`, matching every record written before the vocabulary
152
+ * existed.
153
+ */
154
+ reason?: DriftReason;
155
+ }
156
+ /**
157
+ * Has this exact drift already been recorded for `task`?
158
+ *
159
+ * The rule: compare against the **latest** `envelope.drift` for the task. Equal
160
+ * `(reason, declared_state, derived_state, envelope_sha256)` means the situation
161
+ * the log already describes is the situation now, so nothing is appended. Any
162
+ * difference
163
+ * — the human edited the file again, or the log moved and the derived state
164
+ * changed — is a new fact and is recorded.
165
+ *
166
+ * Comparing against the latest record rather than against any record is
167
+ * deliberate: a task that drifts, is repaired, and drifts the same way again has
168
+ * genuinely drifted twice, and an audit that collapsed those into one would be
169
+ * hiding a repetition from the person whose attention this system spends.
170
+ *
171
+ * For `envelope-missing` (APRV-63) the same rule reads as: one record per
172
+ * episode of loss, re-derived every tick from the file and the log rather than
173
+ * remembered, and a new record when the derived state moves underneath a file
174
+ * that is still stripped. Its limit is stated where it is felt: a file whose
175
+ * envelope is restored by hand *in agreement with the log* leaves no record of
176
+ * the restoration, so a second loss at the same derived state reads as the same
177
+ * episode and is not appended twice. Recording the restoration would need an
178
+ * event nobody has specified; detection does not invent one.
179
+ */
180
+ export declare function driftAlreadyLogged(records: EventRecord[], task: string, facts: DriftFacts): boolean;