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,500 @@
1
+ /**
2
+ * Human-signed log checkpoints (APRV-220).
3
+ *
4
+ * ## The hole this fills
5
+ *
6
+ * The chain in `.approval/log/events.jsonl` is unkeyed, and
7
+ * `docs/proposals/incremental-prefix-proof.md` §3 states the consequence: a
8
+ * process with write access to that file can truncate it and recompute a chain
9
+ * that is self-consistent from genesis. Nothing INSIDE the file contradicts the
10
+ * forgery, so no walk of it ever will. The conformance suite says the same from
11
+ * the other side — `chain-verification/truncation-unanchored` is a boundary
12
+ * vector, and an implementation that claims to catch an unanchored truncation
13
+ * is claiming more than a hash chain can give.
14
+ *
15
+ * §12 of that proposal named three ways out: external anchoring, a keyed chain,
16
+ * and human-signed checkpoints. APRV-219 built the first. This module builds
17
+ * the third.
18
+ *
19
+ * ## How the two witnesses relate
20
+ *
21
+ * They are independent, and neither weakens the other.
22
+ *
23
+ * The ANCHOR (`cli/log-anchor.ts`) asks "does somebody else hold a copy of
24
+ * these bytes?" and answers from git. It is exactly as fresh as the last push,
25
+ * and on a machine with no remote and no records branch it says nothing at all
26
+ * (a skip, never a pass).
27
+ *
28
+ * A CHECKPOINT asks "did a key that no agent process holds sign this head?" and
29
+ * answers from the log itself plus the policy. It works offline, it covers the
30
+ * window since the last push, and it survives being copied to another machine.
31
+ *
32
+ * Against the forger of §3 they fail in different directions, which is the
33
+ * point of having both: the anchor catches a truncation whose records somebody
34
+ * else already holds, and a checkpoint catches a truncation inside the window
35
+ * nobody has pushed yet — because every checkpoint in the rewritten range names
36
+ * a `(seq, hash)` the rewritten chain does not carry, and the forger cannot
37
+ * produce a signature over the hashes they DID recompute.
38
+ *
39
+ * `approval log verify` runs the two independently and reports both. Neither
40
+ * check may be weakened to make the other pass.
41
+ *
42
+ * ## The key
43
+ *
44
+ * A DEDICATED Ed25519 keypair, not the attestation identity. Attestation
45
+ * (`core/attest.ts`) has no keypair to reuse: human identity at v0.1 is
46
+ * config-declared (`--as human:<id>`), and its whole documented claim is that
47
+ * *someone with local control* signed off, not who. A checkpoint has to claim
48
+ * more than that or it claims nothing, because the party it defends against is
49
+ * a process with local control.
50
+ *
51
+ * - The PRIVATE half never appears in the log, in a policy, or in any file an
52
+ * agent may read: it lives in the credential vault (`core/vault.ts`),
53
+ * encrypted at rest under a passphrase `core/child-env.ts` strips from every
54
+ * spawned child, behind a file whose reading classifies `account.credential`
55
+ * (human-only). This module never reads it from anywhere — the caller passes
56
+ * the bytes, so the custody decision lives in one place, the CLI verb.
57
+ * - The PUBLIC half is listed in the policy, `audit.checkpoint_keys`. The
58
+ * policy is the human's own committed, attested artifact: editing it is a
59
+ * visible diff AND de-attests the policy, so gate operations refuse until a
60
+ * human re-attests. That is a materially harder thing to do quietly than
61
+ * rewriting a log line.
62
+ *
63
+ * A list rather than a scalar, so rotation can RETAIN a retired key. A
64
+ * checkpoint signed by a key the policy no longer lists is a refusal here, on
65
+ * purpose (see {@link CHECKPOINT_REFUSAL_CODES}), which makes dropping a key
66
+ * that signed anything a de-verification rather than a cleanup.
67
+ *
68
+ * ## What is signed, and why not more
69
+ *
70
+ * `"approval.md/log-checkpoint/v1\n" + JCS({alg, hash, seq})`. The prefix is
71
+ * domain separation: a signature made here cannot be lifted into any other use
72
+ * of the same key, and a signature made elsewhere cannot be presented as a
73
+ * checkpoint. The head's `hash` is a 256-bit chain digest, so the message is
74
+ * already specific to one chain at one position and needs no further binding.
75
+ *
76
+ * The signature deliberately does NOT cover the rest of the record. It could
77
+ * not: the record's own `hash` covers its payload, which covers the signature.
78
+ * What a checkpoint asserts is exactly "a key holder saw this head" — every
79
+ * other field of the record is covered by the chain, and by the anchor, and by
80
+ * this module refusing a checkpoint whose signed head is not the head the log
81
+ * actually carries.
82
+ *
83
+ * ## Fail closed on a bad signature, fail open on an absent one
84
+ *
85
+ * An invalid signature, an unknown key, or a signed hash the log contradicts is
86
+ * a refusal. A log with no checkpoints at all is not: a human who has been away
87
+ * is not a forger, and a runtime that refused a log for want of a tap would
88
+ * teach its operator to turn the check off. A configured cadence that has
89
+ * lapsed is a WARNING, at every layer, and there is no path in this module from
90
+ * "due" to "refused".
91
+ *
92
+ * A missing PUBLIC KEY is a skip naming why, never a pass — the same rule the
93
+ * anchor check follows for a missing anchor. Nothing has been verified, and
94
+ * reporting silence as a pass is how a check stops being one.
95
+ */
96
+ import { type ClockOptions } from "./clock.js";
97
+ import { type AppendError, type AppendOptions, type EventRecord, type LogHead } from "./log.js";
98
+ /** The one signature scheme at v0.1. Recorded on every checkpoint. */
99
+ export declare const CHECKPOINT_ALG = "ed25519";
100
+ /** The event type a checkpoint is written as. */
101
+ export declare const CHECKPOINT_EVENT = "log.checkpoint";
102
+ /**
103
+ * Domain separation for the signed message. A constant, and a versioned one:
104
+ * if the signed shape ever changes, the prefix changes with it, so a signature
105
+ * over the old shape can never be read as one over the new.
106
+ */
107
+ export declare const CHECKPOINT_DOMAIN = "approval.md/log-checkpoint/v1";
108
+ /** The credential name the CLI reads the private half from. */
109
+ export declare const CHECKPOINT_KEY_CREDENTIAL = "approval.checkpoint.key";
110
+ /**
111
+ * Every way a checkpoint can refuse a range. A closed union per SPEC.md §11.1
112
+ * invariant 6, frozen the way the others are: callers branch on the string.
113
+ *
114
+ * Five codes, because they are five different facts about how a checkpoint
115
+ * failed and they have five different repairs. Collapsing them would leave the
116
+ * one message a person needs — which record, which key, which seq — inside a
117
+ * free-text blob nothing can branch on.
118
+ *
119
+ * `checkpoint-key-unknown` is a refusal rather than a warning, and that is the
120
+ * load-bearing choice in this union. Softening it would hand a forger the
121
+ * escape hatch: rewrite each checkpoint's `key_sha256` to name a key nobody
122
+ * lists, and every refusal in the range becomes a shrug. The cost is that
123
+ * removing a retired key from `audit.checkpoint_keys` stops the checkpoints it
124
+ * signed from verifying, which is why the policy field is a LIST and why
125
+ * APRV-257's rotation verb refuses to drop a key that signed anything.
126
+ */
127
+ export declare const CHECKPOINT_REFUSAL_CODES: readonly [
128
+ /**
129
+ * The record names a `key_sha256` that no configured public key hashes to.
130
+ * Either a key was retired out of the policy, or somebody wrote a checkpoint
131
+ * with a key of their own — and this runtime cannot tell which, so it refuses.
132
+ */
133
+ "checkpoint-key-unknown",
134
+ /**
135
+ * The signature does not verify under the key the record names. The bytes
136
+ * signed are not the bytes presented; nothing further is inferred, because
137
+ * distinguishing "wrong key" from "tampered payload" would be an oracle and
138
+ * the repair is identical.
139
+ */
140
+ "checkpoint-signature-invalid",
141
+ /**
142
+ * The signature is good and the log disagrees with it: the record at the
143
+ * signed `seq` carries a hash the signature does not name (or the log carries
144
+ * no record at that seq at all). THIS is the forged-chain catch — a chain
145
+ * recomputed after a checkpoint cannot reproduce a signature over the hashes
146
+ * it replaced.
147
+ */
148
+ "checkpoint-hash-mismatch",
149
+ /**
150
+ * The signed `seq` is not below the checkpoint record's own. A checkpoint
151
+ * signs the past; one naming itself or the future is not a checkpoint, and a
152
+ * runtime that accepted one would accept a record vouching for a head that
153
+ * did not exist when it was written.
154
+ */
155
+ "checkpoint-out-of-order",
156
+ /**
157
+ * The payload is not a checkpoint payload: a missing field, a hash that is
158
+ * not 64 hex, an `alg` this build does not implement. The write boundary
159
+ * refuses these, so reaching one means the record came from elsewhere — and a
160
+ * `log.checkpoint` nobody can read is not a checkpoint that passes.
161
+ */
162
+ "checkpoint-malformed"];
163
+ export type CheckpointRefusalCode = (typeof CHECKPOINT_REFUSAL_CODES)[number];
164
+ /** A checkpoint keypair. The public half travels; the private half never does. */
165
+ export interface CheckpointKeypair {
166
+ /** Base64 DER SPKI. What goes in `audit.checkpoint_keys`. */
167
+ publicKey: string;
168
+ /** Base64 DER PKCS#8. What goes in the vault, and nowhere else. */
169
+ privateKey: string;
170
+ /** SHA-256 of the DER SPKI bytes, hex. What the record names. */
171
+ fingerprint: string;
172
+ }
173
+ /** Mint a checkpoint keypair. Ed25519 from `node:crypto`; no dependency added. */
174
+ export declare function mintCheckpointKeypair(): CheckpointKeypair;
175
+ /**
176
+ * SHA-256 of a public key's DER SPKI **bytes**, hex.
177
+ *
178
+ * Over the bytes rather than over their base64 spelling, so a key that was
179
+ * re-wrapped, re-encoded, or copied through a text editor still fingerprints to
180
+ * the same value. Returns `null` for anything that is not a public key this
181
+ * build can parse — an unreadable key is not a key, and callers turn that into
182
+ * a skip or a refusal rather than into a match against nothing.
183
+ */
184
+ export declare function checkpointKeyFingerprint(publicKey: string): string | null;
185
+ /**
186
+ * The configured public keys, indexed by fingerprint.
187
+ *
188
+ * Keys that do not parse are DROPPED rather than throwing, and the count of
189
+ * what survived is what a caller reports: a policy listing one good key and one
190
+ * typo must still verify the checkpoints the good key signed, and it must not
191
+ * be able to claim the typo verified anything.
192
+ */
193
+ export declare function checkpointKeyIndex(publicKeys: readonly string[]): {
194
+ keys: Map<string, string>;
195
+ unreadable: number;
196
+ };
197
+ /** The head a checkpoint signs, exactly as the record records it. */
198
+ export interface CheckpointHead {
199
+ seq: number;
200
+ hash: string;
201
+ alg?: string;
202
+ }
203
+ /**
204
+ * The bytes a checkpoint signature covers.
205
+ *
206
+ * JCS (RFC 8785) over `{alg, hash, seq}`, behind {@link CHECKPOINT_DOMAIN} and
207
+ * a newline. Canonical, so two runtimes that agree on the head agree on the
208
+ * bytes; domain-separated, so the signature means one thing.
209
+ */
210
+ export declare function checkpointMessage(head: CheckpointHead): Buffer;
211
+ /** Sign a head with a base64 PKCS#8 private key. `null` when the key is unusable. */
212
+ export declare function signCheckpoint(head: CheckpointHead, privateKey: string): string | null;
213
+ /** Does `signature` verify over `head` under `publicKey`? Never throws. */
214
+ export declare function verifyCheckpointSignature(head: CheckpointHead, signature: string, publicKey: string): boolean;
215
+ /** A `log.checkpoint` payload, as this runtime reads one back. */
216
+ export interface CheckpointPayload {
217
+ seq: number;
218
+ hash: string;
219
+ alg: string;
220
+ keySha256: string;
221
+ signature: string;
222
+ }
223
+ /** Is this record a `log.checkpoint`? Type only; the payload is read separately. */
224
+ export declare function isCheckpointRecord(record: EventRecord): boolean;
225
+ /**
226
+ * Read a checkpoint record's payload, or `null` when it is not one.
227
+ *
228
+ * Strict, and deliberately so: the write boundary already refuses a malformed
229
+ * checkpoint, so a payload that fails here arrived from somewhere else, and the
230
+ * caller's answer to that is {@link CHECKPOINT_REFUSAL_CODES}'s
231
+ * `checkpoint-malformed` rather than a pass.
232
+ */
233
+ export declare function readCheckpointPayload(record: EventRecord): CheckpointPayload | null;
234
+ /**
235
+ * Which keys have signed checkpoints in this log, and at which seqs (APRV-257).
236
+ *
237
+ * The question rotation has to ask before it retires anything. Removing a key
238
+ * from `audit.checkpoint_keys` turns every checkpoint it signed into
239
+ * `checkpoint-key-unknown` — a REFUSAL, by the deliberate choice recorded in
240
+ * {@link CHECKPOINT_REFUSAL_CODES} — so a verb that let an operator drop a key
241
+ * casually would be a verb that broke a log's verification to tidy a list.
242
+ *
243
+ * Keyed by the record's self-reported `key_sha256`, which is the only thing
244
+ * `checkLogCheckpoints` looks a key up by, so the answer is exactly the set of
245
+ * names whose removal would change a verdict. Records with an unreadable
246
+ * payload are skipped: they refuse for a different reason already, and no
247
+ * removal makes that better or worse.
248
+ */
249
+ export declare function checkpointSignersIn(records: readonly EventRecord[]): Map<string, number[]>;
250
+ /** Why a checkpoint was not appended. Nothing was written in any of these. */
251
+ export declare const CHECKPOINT_APPEND_REFUSAL_CODES: readonly [
252
+ /** The actor is not `human:`-prefixed. Refused here and in the schema. */
253
+ "actor-not-human",
254
+ /** The private key will not parse, or would not sign. */
255
+ "checkpoint-key-unusable",
256
+ /** The log has no records yet: an empty chain has no head to sign. */
257
+ "log-empty",
258
+ /**
259
+ * A caller named a head this log does not carry (APRV-257).
260
+ *
261
+ * Only {@link appendCheckpointAt} can reach it, and only from the tap: the
262
+ * human is shown a `(seq, hash)`, the head moves while the phone is in a
263
+ * pocket, and the record they sign still names the head they SAW. That is
264
+ * allowed — a checkpoint signs any seq below its own — but only while the
265
+ * log actually carries that hash at that seq. When it does not, the thing in
266
+ * front of the human was derived from a different chain than the one being
267
+ * written to, and signing it would mint a checkpoint that
268
+ * `checkpoint-hash-mismatch` refuses forever after.
269
+ */
270
+ "checkpoint-head-unknown",
271
+ /** The log could not be opened. */
272
+ "log-unreadable",
273
+ /** The log's last line is truncated. Nothing may chain onto it. */
274
+ "log-torn-tail",
275
+ /** The chain does not verify, so no head derived from it may be signed. */
276
+ "log-corrupt",
277
+ /** The append itself was refused; `append` carries the writer's own code. */
278
+ "append-failed"];
279
+ export type CheckpointAppendRefusalCode = (typeof CHECKPOINT_APPEND_REFUSAL_CODES)[number];
280
+ export interface CheckpointAppendRefusal {
281
+ ok: false;
282
+ code: CheckpointAppendRefusalCode;
283
+ message: string;
284
+ /** The writer's own refusal, present when `code` is `append-failed`. */
285
+ append?: AppendError;
286
+ }
287
+ export interface CheckpointAppendResult {
288
+ ok: true;
289
+ record: EventRecord;
290
+ /** The head this checkpoint signed. */
291
+ head: LogHead;
292
+ /** The fingerprint of the key that signed it. */
293
+ fingerprint: string;
294
+ }
295
+ export interface CheckpointAppendOptions extends ClockOptions {
296
+ schemaDir?: string;
297
+ append?: AppendOptions;
298
+ /**
299
+ * The channel the record names (APRV-257). `cli` by default, which is what
300
+ * the terminal verb is; the tap passes the channel the human tapped on.
301
+ *
302
+ * Descriptive and never authoritative: the schema constrains the ACTOR of a
303
+ * `log.checkpoint` and the signature constrains the head, and neither of
304
+ * those reads this field. It is here so a reader of the log can tell a
305
+ * checkpoint taken at a terminal from one taken on a phone.
306
+ */
307
+ channel?: string;
308
+ /**
309
+ * How many whole read-sign-append cycles a moved head may cost (APRV-257).
310
+ *
311
+ * The tap needs it and the terminal verb inherits it: a listener signing on a
312
+ * busy log races the daemon's own appends, and handing `head-moved` back to
313
+ * someone who has just tapped a button on their phone is precisely the party
314
+ * `core/head-retry.ts` exists to stop handing it to. Each attempt re-reads
315
+ * and re-signs; nothing crosses an attempt.
316
+ */
317
+ attempts?: number;
318
+ }
319
+ /**
320
+ * Append one `log.checkpoint` signing the log's current head.
321
+ *
322
+ * The head is read, then signed, then written with that head as
323
+ * `expectedHead`, so the record cannot land on a chain that moved underneath it
324
+ * (SPEC.md §11.1: every check-then-append passes through compare-and-append).
325
+ * A concurrent append is `head-moved`, and the repair is to run it again — the
326
+ * signature would otherwise vouch for a head that is no longer this record's
327
+ * predecessor, which is a checkpoint that verifies and means less than it looks
328
+ * like it means.
329
+ *
330
+ * `ts` is stamped from the clock at the write boundary and is never a
331
+ * parameter: `log.checkpoint` is gate-typed under amended SPEC.md §8 (A2), and
332
+ * a caller who could choose the moment could backdate the one record whose
333
+ * whole content is a claim about a moment.
334
+ *
335
+ * The private key arrives as a value. This module never reads it from a vault,
336
+ * a file, or an environment variable, so there is exactly one place in the
337
+ * codebase that decides where a checkpoint key may come from, and it is the CLI
338
+ * verb a human runs.
339
+ */
340
+ export declare function appendCheckpoint(logPath: string, privateKey: string, actor: string, options?: CheckpointAppendOptions): CheckpointAppendResult | CheckpointAppendRefusal;
341
+ /**
342
+ * Append one `log.checkpoint` signing a head the CALLER names (APRV-257).
343
+ *
344
+ * The tap's entry point, and the reason APRV-220's verify rule asks only that a
345
+ * checkpoint signs a seq BELOW its own rather than its immediate predecessor.
346
+ * A human is shown `(seq, hash)` on a phone; by the time they tap, the daemon
347
+ * has appended three records. The honest thing to sign is the head they SAW,
348
+ * because that is what they looked at, and a runtime that quietly re-read the
349
+ * head and signed something else would be putting a human's key over bytes
350
+ * nobody inspected.
351
+ *
352
+ * The named head is checked against the log before anything is signed: it must
353
+ * be a `(seq, hash)` this chain actually carries ({@link
354
+ * CHECKPOINT_APPEND_REFUSAL_CODES}'s `checkpoint-head-unknown`). So a stale
355
+ * prompt from a chain that has since been rewritten cannot be turned into a
356
+ * signature, and the checkpoint that lands is one `checkLogCheckpoints` will
357
+ * accept rather than one it will refuse forever.
358
+ */
359
+ export declare function appendCheckpointAt(logPath: string, privateKey: string, actor: string, head: CheckpointHead, options?: CheckpointAppendOptions): CheckpointAppendResult | CheckpointAppendRefusal;
360
+ /** The fingerprint of the public half of a private key, or `null`. */
361
+ export declare function privateKeyFingerprint(privateKey: string): string | null;
362
+ /** One checkpoint that validated, as a caller reports it. */
363
+ export interface VerifiedCheckpoint {
364
+ /** The seq of the `log.checkpoint` record itself. */
365
+ at: number;
366
+ /** The head it signed. */
367
+ seq: number;
368
+ hash: string;
369
+ /** When it was signed, from the record's runtime-stamped `ts`. */
370
+ ts: string;
371
+ actor: string;
372
+ keySha256: string;
373
+ }
374
+ /** How the walked range stands against the checkpoints inside it. */
375
+ export type CheckpointCheck = {
376
+ status: "pass";
377
+ /** Every checkpoint that validated, in log order. */
378
+ checkpoints: VerifiedCheckpoint[];
379
+ /** Checkpoints whose signed seq falls below the walked range. */
380
+ unchecked: number;
381
+ /** Configured keys this build could parse. */
382
+ keys: number;
383
+ /** The cadence warning, when one is due. Never a refusal. */
384
+ warning: string | null;
385
+ detail: string;
386
+ } | {
387
+ status: "skip";
388
+ reason: string;
389
+ checkpoints: 0;
390
+ } | {
391
+ status: "refused";
392
+ code: CheckpointRefusalCode;
393
+ /** The seq of the offending `log.checkpoint` record. */
394
+ at: number;
395
+ message: string;
396
+ /** Checkpoints that validated before this one. */
397
+ checkpoints: VerifiedCheckpoint[];
398
+ };
399
+ /** What {@link checkLogCheckpoints} is asked. `records` are already VERIFIED. */
400
+ export interface CheckpointCheckOptions {
401
+ /**
402
+ * The log's records, already verified by the caller.
403
+ *
404
+ * Required rather than re-derived, for SPEC.md §11.1 invariant 1: this check
405
+ * reads only verified records, and a check that walked the chain itself would
406
+ * be answering a question its caller has already answered, differently.
407
+ */
408
+ records: readonly EventRecord[];
409
+ /** The configured public keys, base64 DER SPKI. From `audit.checkpoint_keys`. */
410
+ publicKeys: readonly string[];
411
+ /**
412
+ * Why there are no keys, when the caller already knows: an unloadable policy,
413
+ * a missing file. Folded into the skip reason so the sentence names the cause
414
+ * rather than only the symptom.
415
+ */
416
+ keysUnavailable?: string | null;
417
+ /** `audit.checkpoint_every` in milliseconds, or `null` when the cadence is off. */
418
+ checkpointEveryMs?: number | null;
419
+ /** Now, for the cadence warning only. No verdict reads it. */
420
+ now?: number;
421
+ }
422
+ /**
423
+ * Demand every checkpoint inside the walked range.
424
+ *
425
+ * Every `log.checkpoint` record in `records` must carry a readable payload,
426
+ * name a configured key, verify under it, and name the hash the log actually
427
+ * carries at the seq it signed. The FIRST failure refuses, carrying the seq of
428
+ * the offending record and the checkpoints that validated ahead of it: a person
429
+ * reading a divergence needs to know how far the log was still good.
430
+ *
431
+ * Three things are deliberately not refusals.
432
+ *
433
+ * - **No configured key** is a skip naming why, and naming how many checkpoint
434
+ * records went unchecked. Nothing was verified, and a check that reported
435
+ * that as a pass would have stopped being a check.
436
+ * - **A signed seq below the walked range** is counted and named, not refused.
437
+ * A full walk starts at genesis so this cannot arise there; a caller walking
438
+ * a suffix gets an honest count of what its range could not speak to.
439
+ * - **A lapsed cadence** is a warning. A human who has been away is not a
440
+ * forger, and a runtime that refused a log for want of a tap is a runtime
441
+ * whose operator turns the check off.
442
+ */
443
+ export declare function checkLogCheckpoints(options: CheckpointCheckOptions): CheckpointCheck;
444
+ /**
445
+ * A checkpoint the runtime would like a human to sign, and the head to show
446
+ * them (APRV-257).
447
+ *
448
+ * `head` is the log's CURRENT head at the moment the offer was made, and it is
449
+ * carried through the prompt into {@link appendCheckpointAt} unchanged. What
450
+ * the human is shown is what gets signed, however long the phone stays in the
451
+ * pocket.
452
+ */
453
+ export interface CheckpointOffer {
454
+ /** The head a prompt asks the human to sign. */
455
+ head: {
456
+ seq: number;
457
+ hash: string;
458
+ };
459
+ /** The seq of the newest checkpoint RECORD, or `null` when there is none. */
460
+ since: number | null;
461
+ /** How long since that checkpoint (or since the log's oldest record), in ms. */
462
+ ageMs: number;
463
+ /** `audit.checkpoint_every`, in ms. */
464
+ everyMs: number;
465
+ /** The sentence every reporting surface prints. Never a refusal. */
466
+ warning: string;
467
+ }
468
+ /**
469
+ * Is a checkpoint due, and over which head? `null` when it is not.
470
+ *
471
+ * The whole question in one call, over already-verified records: it runs
472
+ * {@link checkLogCheckpoints} and offers only from a PASS. A refused range is
473
+ * not a range to ask for another signature over — the thing to do with a
474
+ * checkpoint that does not verify is look at it, not sign a new one on top —
475
+ * and a skipped one has no key configured, so there is nothing to sign with and
476
+ * nobody to ask.
477
+ */
478
+ export declare function checkpointDue(options: CheckpointCheckOptions): CheckpointOffer | null;
479
+ /** What the policy says about checkpoints, and why it says nothing when it does. */
480
+ export interface CheckpointPolicy {
481
+ /** `audit.checkpoint_keys`, or empty. */
482
+ publicKeys: string[];
483
+ /** `audit.checkpoint_every` in milliseconds, or `null`. */
484
+ checkpointEveryMs: number | null;
485
+ /** Present when the policy could not be loaded at all. */
486
+ unloadable: string | null;
487
+ }
488
+ /**
489
+ * The checkpoint half of a policy, read the way `skewToleranceMsOf` reads its
490
+ * key — except that this one fails to a SKIP rather than to a default.
491
+ *
492
+ * A policy that cannot be loaded configures no keys, and a caller with no keys
493
+ * skips with a reason. There is no safe default here: falling back to "no keys"
494
+ * and calling it a pass would report an unreadable policy as a verified log,
495
+ * and falling back to a built-in key would be a key nobody chose.
496
+ */
497
+ export declare function checkpointPolicyOf(policy: {
498
+ dir?: string;
499
+ file?: string;
500
+ }, schemaDir?: string): CheckpointPolicy;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The environment a spawned child receives (APRV-205).
3
+ *
4
+ * `approval run` used to call `spawnSync` with no `env` option, so Node handed
5
+ * the child a copy of the whole session environment: the Telegram bot token,
6
+ * whatever `vault.passphrase_env` names, every other credential the session was
7
+ * launched with. APRV-194 closed the direct route by classifying the shell
8
+ * commands that READ credential material, and it could not close this one,
9
+ * because the classifier reads `npm test` and the reading happens inside
10
+ * whatever `npm test` runs. A gate that holds the token while handing it to
11
+ * every child it launches is custody theatre.
12
+ *
13
+ * This module is the scrub. It is the minimal, pre-launch slice of APRV-193's
14
+ * "spawn starved": it removes credential-bearing variables and does nothing
15
+ * else. It is not a sandbox — the child keeps the network, the filesystem, and
16
+ * every other ambient capability of the session, and APRV-193 is where those
17
+ * are taken away.
18
+ *
19
+ * Three rules, in this order:
20
+ *
21
+ * 1. A name the granted action's adapter declared in `requiredCredentials`
22
+ * (APRV-169) is PASSED. That declaration is static, made by the adapter's own
23
+ * code, and reaches this function through nothing a caller typed: a flag that
24
+ * could name a variable to keep would be a flag that hands an agent the
25
+ * token back.
26
+ * 2. A name under the credential-bearing prefixes (`APPROVAL_`, `TELEGRAM_`,
27
+ * `VAULT_`), less the APRV-194 allowlist of runtime names that hold no
28
+ * secret, is REMOVED. The list is imported from `command-class.ts` rather
29
+ * than restated, so the classifier and the scrub cannot drift apart.
30
+ * 3. The name the policy's `vault.passphrase_env` gives is REMOVED, whatever it
31
+ * is. The default (`APPROVAL_VAULT_PASSPHRASE`) is already caught by rule 2;
32
+ * a deployment that renamed it to something outside the prefixes is the
33
+ * reason this rule is separate.
34
+ *
35
+ * Everything else passes through untouched. `PATH`, `HOME`, `TMPDIR`, `LANG`,
36
+ * `NODE_OPTIONS` and the rest of a working environment are not this task's
37
+ * business, and a scrub that broke `PATH` would be reverted within the day.
38
+ * That is an allowlist inverted, and the design says so plainly: APRV-193's
39
+ * §3.4 wants a real allowlist at the SESSION boundary, where the operator
40
+ * launches the harness; a per-child allowlist here would break every command an
41
+ * agent legitimately runs.
42
+ *
43
+ * What comes back beside the environment is a COUNT. The log records how many
44
+ * variables were withheld and never which ones, because a name is the half of a
45
+ * credential this repository can print, and SPEC.md §11.1's raw-secrets
46
+ * invariant is not satisfied by leaking the other half slowly. The count is
47
+ * informational: nothing in the gate reads it back, and no decision anywhere
48
+ * turns on it.
49
+ */
50
+ import { NON_SECRET_ENV_NAMES, SECRET_ENV_PREFIXES, isSecretEnvName } from "./command-class.js";
51
+ export { NON_SECRET_ENV_NAMES, SECRET_ENV_PREFIXES, isSecretEnvName };
52
+ export interface ChildEnvironmentOptions {
53
+ /** The environment to start from. Defaults to this process's own. */
54
+ readonly source?: NodeJS.ProcessEnv;
55
+ /**
56
+ * The name the policy's `vault.passphrase_env` gives, when a policy was
57
+ * loaded. `null` or omitted removes nothing beyond the prefixed family.
58
+ */
59
+ readonly passphraseEnv?: string | null;
60
+ /**
61
+ * The credential names the granted action's adapter declared in
62
+ * `requiredCredentials` (APRV-169). Passed through even when they fall under
63
+ * the credential-bearing prefixes: the adapter said it cannot act without
64
+ * them, and this is the injection point the design names.
65
+ */
66
+ readonly declaredCredentials?: readonly string[];
67
+ }
68
+ export interface ChildEnvironment {
69
+ /** What to hand `spawnSync`. Never a reference to the source. */
70
+ readonly env: Record<string, string>;
71
+ /** How many variables were withheld. Names are deliberately not reported. */
72
+ readonly stripped: number;
73
+ /**
74
+ * The declared names that were present in the source and survived, as a
75
+ * count, for the same reason: a caller may want to know that injection
76
+ * happened without learning what was injected.
77
+ */
78
+ readonly passed: number;
79
+ }
80
+ /**
81
+ * Build the environment a granted child gets.
82
+ *
83
+ * Total and deterministic: no reads, no throws, and the same source produces
84
+ * the same answer every time. A variable whose value is `undefined` (Node's
85
+ * spelling for "unset") is neither copied nor counted, because there was
86
+ * nothing there to withhold.
87
+ */
88
+ export declare function childEnvironment(options?: ChildEnvironmentOptions): ChildEnvironment;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The write-boundary clock (amended SPEC.md §8, APRV-20 pass two / amendment A2).
3
+ *
4
+ * ## Why this module exists
5
+ *
6
+ * "Events written through the gate (`approval.*`, `execution.*`, `budget.*`,
7
+ * `audit.*`, `policy.updated`) have `ts` assigned by the runtime at the write
8
+ * boundary. Caller-supplied timestamps on these types MUST be refused. Because
9
+ * TTL judgment and budget windows read `ts`, a party subject to those controls
10
+ * must never author the clock they are judged by."
11
+ *
12
+ * Before this amendment every gate-typed append took `ts` as a positional
13
+ * parameter, so an agent calling the core (or the CLI's own argv, had a flag
14
+ * ever been added) could hand the runtime the moment it wished to be judged at:
15
+ * a timestamp inside a lapsed TTL, or one that placed an authorization outside
16
+ * the rolling budget window. The refusal the spec asks for is expressed here
17
+ * **structurally rather than as a check**: the parameter no longer exists on any
18
+ * public gate/token/execute/attest function, so there is nothing to refuse and
19
+ * nothing to forget to refuse.
20
+ *
21
+ * Determinism is preserved by injection rather than by parameters. Every such
22
+ * function takes an optional `clock` in its options; the CLI never passes one
23
+ * (so the real clock is read once, at the write boundary, inside core), and
24
+ * tests pass a fixed clock so TTL lapse and budget windows stay exercised
25
+ * without sleeps. A replay still reproduces exactly, because the clock is an
26
+ * input to the run rather than a read of ambient state inside the hashing path.
27
+ *
28
+ * ## The carve-out
29
+ *
30
+ * `core/log.ts`'s `appendEvent` still accepts `ts`, deliberately. SPEC.md §8
31
+ * leaves direct log writers outside the gate free to supply their own
32
+ * timestamps — an importer replaying a historical log is the obvious case, and
33
+ * a writer that could not state when something happened could not import
34
+ * anything. The rule binds the *gate*, which is where a subject of oversight
35
+ * would benefit from lying.
36
+ */
37
+ /** A source of RFC 3339 instants. Injected, never read from ambient state. */
38
+ export type Clock = () => string;
39
+ /** The default: the real clock, read at the write boundary and nowhere else. */
40
+ export declare const systemClock: Clock;
41
+ /** Options carrying an injectable clock. Shared by every gate-typed writer. */
42
+ export interface ClockOptions {
43
+ /**
44
+ * The clock the runtime stamps this write with. Defaults to
45
+ * {@link systemClock}. Tests inject a fixed clock; production does not pass
46
+ * one at all, so the timestamp of a gate event is authored by the runtime and
47
+ * never by the party being judged (amended SPEC.md §8).
48
+ */
49
+ clock?: Clock;
50
+ }
51
+ /** Read the injected clock, or the real one. One line, one place. */
52
+ export declare function tick(options?: ClockOptions): string;