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,138 @@
1
+ /**
2
+ * Write-boundary JSON Schema validation.
3
+ *
4
+ * SPEC.md §8: every event and envelope validates against its JSON Schema
5
+ * before it is written. Validation at the write boundary is itself a control,
6
+ * so this module fails **closed**: a missing schema directory, an unreadable
7
+ * or unparseable schema file, an unknown schema id, or an Ajv compile failure
8
+ * all produce `{ ok: false, errors: [...] }`. Nothing here can return `ok`
9
+ * for a document it did not actually validate, and nothing throws out to the
10
+ * caller (a thrown error would be an unhandled write-boundary bypass).
11
+ *
12
+ * Determinism: the result is a pure function of (schema files on disk,
13
+ * document). No network, no clock, no randomness. Every call re-reads every
14
+ * schema file, so a run never depends on the order or history of previous
15
+ * calls: edit a schema mid-run and the next call validates against the edit.
16
+ * Two reuses sit under that rule and neither bends it. {@link prepareValidator}
17
+ * lets a caller validating many documents against one schema in one pass
18
+ * compile once and hold the snapshot itself. And since APRV-206 the Ajv
19
+ * *compile* is reused across calls when the schema bytes just read hash to the
20
+ * digest the compiled validator was built from — the schema files are still
21
+ * read every time, so nothing is answered for bytes that were not re-proved;
22
+ * see {@link compiledValidators} for the argument in full.
23
+ *
24
+ * Dialect / import notes (AC #6 — documented, never silently downgraded):
25
+ *
26
+ * - Dialect is JSON Schema draft 2020-12 via Ajv's dedicated `Ajv2020` class.
27
+ * Ajv 8 ships no `exports` map, so under NodeNext the working ESM import is
28
+ * the deep path to the built CJS file:
29
+ * import { Ajv2020 } from "ajv/dist/2020.js";
30
+ * The named export is used rather than the default because Ajv 8's CJS
31
+ * interop default (`module.exports = Ajv2020`) is not typed as a default
32
+ * export under `verbatimModuleSyntax` + `esModuleInterop: false`. The named
33
+ * binding resolves and type-checks cleanly under NodeNext.
34
+ * - `ajv-formats` is likewise a deep CJS import: its plugin is exported as
35
+ * `module.exports = formatsPlugin`, so the callable value comes from the
36
+ * `.default` property of the namespace import under NodeNext.
37
+ * - Ajv is configured `strict: true`. No strict flag is relaxed. Formats are
38
+ * enforced, not annotation-only: `validateFormats: true` plus `ajv-formats`
39
+ * registered, so an invalid `date-time` string is a validation error.
40
+ */
41
+ /** A single validation failure, flattened for logging and CLI output. */
42
+ export interface ValidationError {
43
+ /** JSON Pointer to the offending location ("" for the document root). */
44
+ path: string;
45
+ /** Ajv keyword, or a harness pseudo-keyword such as "schemaLoad". */
46
+ keyword: string;
47
+ /** Human-readable reason. */
48
+ message: string;
49
+ }
50
+ /** Result of a write-boundary validation. Failure always carries a reason. */
51
+ export type ValidationResult = {
52
+ ok: true;
53
+ } | {
54
+ ok: false;
55
+ errors: ValidationError[];
56
+ };
57
+ /** Suffix identifying schema files inside the schema directory. */
58
+ export declare const SCHEMA_FILE_SUFFIX = ".schema.json";
59
+ /** Repo `schema/` directory: default source of truth for all schemas. */
60
+ export declare const DEFAULT_SCHEMA_DIR: string;
61
+ /**
62
+ * Which boundary a validation is speaking for (APRV-121).
63
+ *
64
+ * - `write` — the default, and what SPEC.md §8 means by "validate at the write
65
+ * boundary". The schemas as written on disk: a monetary amount is a canonical
66
+ * decimal string and nothing else.
67
+ * - `historical` — the read boundary. The log is append-only, so a verifier
68
+ * walking records written before APRV-121 meets JSON-number amounts that were
69
+ * valid when they were appended and must stay valid forever. This mode makes
70
+ * exactly one substitution, described in {@link WIDENED_DEFS}, and no other.
71
+ *
72
+ * The asymmetry is the point: a document only ever gets *more* permissive by a
73
+ * caller explicitly naming the read boundary, and the callers that do are
74
+ * pinned: `core/verify.ts` (the log walk), the daemon's envelope scan, and the
75
+ * `set-state` task-file rewrite — paths that read or preserve claims an earlier
76
+ * write boundary accepted, never ones that author new claims (APRV-121,
77
+ * APRV-148).
78
+ */
79
+ export type ValidationMode = "write" | "historical";
80
+ /** Options accepted by {@link validate}. */
81
+ export interface ValidateOptions {
82
+ /** Directory to load `*.schema.json` from. Injectable for tests. */
83
+ schemaDir?: string;
84
+ /** Which boundary this validation speaks for. Defaults to `"write"`. */
85
+ mode?: ValidationMode;
86
+ }
87
+ /**
88
+ * The `$defs` a `historical` validation replaces, as `name -> replacement name`.
89
+ *
90
+ * Pinned as a list rather than derived from a naming convention, so widening
91
+ * the read boundary is always a reviewable diff in this file and never a side
92
+ * effect of adding a definition to a schema. `usd_amount` is the only entry:
93
+ * the pre-APRV-121 write boundary typed monetary fields as `{"type": "number",
94
+ * "minimum": 0}`, and `usd_amount_historical` is exactly that union with the
95
+ * decimal string.
96
+ */
97
+ export declare const WIDENED_DEFS: Readonly<Record<string, string>>;
98
+ /**
99
+ * List schema names (file basename minus `.schema.json`) in a directory.
100
+ * Sorted, so discovery order is stable across platforms and runs.
101
+ */
102
+ export declare function listSchemaNames(schemaDir?: string): string[];
103
+ /**
104
+ * Validate `document` against the schema identified by `schemaId` (the schema
105
+ * filename minus `.schema.json`, or the schema's `$id`).
106
+ *
107
+ * Fails closed: every load, parse, and compile problem is reported as a
108
+ * validation failure rather than thrown.
109
+ */
110
+ export declare function validate(schemaId: string, document: unknown, options?: ValidateOptions): ValidationResult;
111
+ /**
112
+ * A validator compiled once and reusable for many documents.
113
+ *
114
+ * `check` is {@link validate} with the schema load and Ajv compile already
115
+ * paid: same results, same error shapes, on every document. This exists for
116
+ * the one caller that validates thousands of documents against one schema in
117
+ * one pass — the log chain walk — where a per-document recompile turns a
118
+ * subsecond verification into minutes of CPU (APRV-186).
119
+ *
120
+ * The determinism stance in the module header is unchanged: nothing is cached
121
+ * across calls to {@link prepareValidator} itself. A prepared validator is a
122
+ * snapshot of the schema files as they stood when it was prepared; a caller
123
+ * that wants fresh schemas prepares again.
124
+ */
125
+ export interface PreparedValidator {
126
+ ok: true;
127
+ check(document: unknown): ValidationResult;
128
+ }
129
+ /**
130
+ * Load and compile `schemaId` once, for reuse across many documents.
131
+ *
132
+ * Fails closed exactly as {@link validate} does: every load, parse, and
133
+ * compile problem is reported as `{ ok: false, errors }`, never thrown.
134
+ */
135
+ export declare function prepareValidator(schemaId: string, options?: ValidateOptions): PreparedValidator | {
136
+ ok: false;
137
+ errors: ValidationError[];
138
+ };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The values block of `APPROVAL.md` (SPEC.md §5.3), read.
3
+ *
4
+ * Everything else in this directory is control: what an agent may do, who
5
+ * decides, what is sampled. This module reads the other half of the file — the
6
+ * optional ` ```yaml approval-values ` block in which the operator says what
7
+ * they value in the work, what they want from the agent, and how they read and
8
+ * answer. It is the mirror of the journal (SPEC.md §10.1): the journal is the
9
+ * agent's outlet the gate does not stand in front of, and the values block is
10
+ * the human's, with the same rule running in both directions, that nothing said
11
+ * there moves a verdict.
12
+ *
13
+ * Three decisions are worth stating here rather than leaving to be inferred.
14
+ *
15
+ * **Absence is a declaration, never a default.** A file with no values block is
16
+ * an operator who has declared no values, which is information. It is not a
17
+ * missing thing to be repaired, and it is not an invitation to invent a neutral
18
+ * middle. So the result has three states rather than two: present, absent, and
19
+ * unreadable, and `ok: true, present: false` is the absent one. Surfaces render
20
+ * it in the words SPEC.md §5.3 fixes, so a session can tell "nothing was
21
+ * declared" from "I did not look".
22
+ *
23
+ * **Two blocks fail.** One file, at most one values block, exactly as the policy
24
+ * block is exactly one. Two blocks are two answers to one question, and a reader
25
+ * that silently took the first would be choosing between two things a human
26
+ * wrote, on the strength of document order.
27
+ *
28
+ * **A values failure never fails the policy.** Enforcement fails closed on the
29
+ * policy block because an unparseable permission document is one whose author
30
+ * believes constraints are in force that are not. Nothing about that argument
31
+ * carries here, because this block is not enforcement: no routing, class match,
32
+ * sampling draw, budget, token or execution decision reads it (SPEC.md §11.1
33
+ * invariant 10, pinned by `tests/values-inert.test.ts`). Failing closed on a
34
+ * malformed values block would convert a typo into an all-manual repository, and
35
+ * would buy no safety at all in exchange. The two blocks are therefore parsed
36
+ * and judged independently, on paths that share only the fence splitter, and
37
+ * this module never calls the policy loader's parse path.
38
+ *
39
+ * ## Determinism
40
+ *
41
+ * {@link loadValues} is a pure function of the file bytes on disk plus the
42
+ * schema directory. No clock, no network, no randomness, no cross-call caching,
43
+ * and no throw: every failure is a result.
44
+ */
45
+ import { type ValidationError } from "./validate.js";
46
+ /** Info string that marks the OPTIONAL values block (SPEC.md §5.3). */
47
+ export declare const VALUES_INFO_STRING = "yaml approval-values";
48
+ /**
49
+ * The parsed values block, as `values.schema.json` admits it.
50
+ *
51
+ * The key set is closed and small on purpose, and the schema is the authority
52
+ * on why each key is here and which were rejected. This interface restates the
53
+ * shape for TypeScript and adds nothing.
54
+ */
55
+ export interface Values {
56
+ /** Format version. The only required key; the integer `1` and nothing else. */
57
+ version: 1;
58
+ /** What the operator loves, in their own words. */
59
+ love?: string[];
60
+ /** What the operator likes. */
61
+ like?: string[];
62
+ /** What the operator dislikes. NOT a prohibition: a prohibition is policy. */
63
+ dislike?: string[];
64
+ /** What the operator wants FROM the agent, as behaviour rather than taste. */
65
+ wants?: string[];
66
+ /** How the operator reads and answers. One sentence or two. */
67
+ responds?: string;
68
+ }
69
+ /**
70
+ * Why a values block could not be read.
71
+ *
72
+ * Deliberately NOT {@link import("./policy-load.js").PolicyLoadErrorCode}: the
73
+ * two are different questions with different consequences, and a shared union
74
+ * would invite a caller to handle one with the other's rules. There is no
75
+ * `no-block` here, because a file with no values block is not a failure at all.
76
+ */
77
+ export type ValuesLoadFailureCode = "file-missing" | "multiple-blocks" | "unterminated-fence" | "yaml-error" | "schema-invalid";
78
+ /** Where the values block was read from. */
79
+ export interface ValuesSource {
80
+ /** Absolute or caller-relative path actually read. */
81
+ path: string;
82
+ /** Basename of that path, e.g. `APPROVAL.md`. */
83
+ filename: string;
84
+ }
85
+ /**
86
+ * Result of {@link loadValues}: present, absent, or unreadable.
87
+ *
88
+ * A not-ok result places no obligation on any enforcement path, because no
89
+ * enforcement path may be looking. It obligates exactly one thing, of the
90
+ * surfaces that print it: say the block is there and could not be read, and say
91
+ * that it grants nothing either way.
92
+ */
93
+ export type ValuesLoadResult = {
94
+ ok: true;
95
+ present: true;
96
+ values: Values;
97
+ source: ValuesSource;
98
+ } | {
99
+ ok: true;
100
+ present: false;
101
+ source: ValuesSource;
102
+ } | {
103
+ ok: false;
104
+ code: ValuesLoadFailureCode;
105
+ message: string;
106
+ errors?: ValidationError[];
107
+ source?: ValuesSource;
108
+ };
109
+ /** Options accepted by {@link loadValues}. */
110
+ export interface LoadValuesOptions {
111
+ /** Directory to search for `APPROVAL.md` / `APPROVALS.md`. Default: cwd. */
112
+ dir?: string;
113
+ /** Explicit policy file path. Overrides discovery entirely. */
114
+ file?: string;
115
+ /** Schema directory passed through to {@link validate}. Injectable for tests. */
116
+ schemaDir?: string;
117
+ }
118
+ /**
119
+ * Extract, parse, and validate the values block of an already-read file.
120
+ *
121
+ * The bytes-in form of {@link loadValues}, for the caller that has the file in
122
+ * hand (a doctor run reading it once for several questions) and for the tests
123
+ * that build a file variant without touching a disk. Nothing here touches the
124
+ * filesystem, nothing throws, and `path` is used only for messages and for
125
+ * {@link ValuesSource}.
126
+ */
127
+ export declare function loadValuesText(path: string, text: string, options?: {
128
+ schemaDir?: string;
129
+ }): ValuesLoadResult;
130
+ /**
131
+ * Find `APPROVAL.md`, and read its values block if it has one.
132
+ *
133
+ * Discovery plus {@link loadValuesText}. A file that cannot be read at all is
134
+ * `file-missing`, which is the only failure code here that says nothing about
135
+ * the block: there was no file to hold one.
136
+ */
137
+ export declare function loadValues(options?: LoadValuesOptions): ValuesLoadResult;
@@ -0,0 +1,291 @@
1
+ /**
2
+ * The reference credential vault (SPEC.md §10.4, §11; APRV-68).
3
+ *
4
+ * SPEC.md §10.4 states the hard boundary in one sentence: adapters "hold the
5
+ * actual credentials in an encrypted vault and MUST require a valid, unexpired,
6
+ * single-use execution token bound to the action's `idempotency_key` … an agent
7
+ * that bypasses the CLI still cannot send, spend, or delete, because the
8
+ * credentials only answer to tokens." APRV-67 built the token half of that
9
+ * sentence (`adapters/contract.ts`: a credential provider that is live only
10
+ * inside the verified-token window). This module is the other half: the place
11
+ * the values actually sit when nobody is executing.
12
+ *
13
+ * ## What it is
14
+ *
15
+ * One file, `.approval/vault.enc`, beside the log's home in the same way
16
+ * `.approval/payloads/` is. It holds a JSON map from credential NAME to
17
+ * credential string, encrypted with AES-256-GCM under a key derived by scrypt
18
+ * from an operator passphrase. The passphrase is read from an environment
19
+ * variable whose NAME the policy declares (`vault.passphrase_env`, default
20
+ * {@link DEFAULT_PASSPHRASE_ENV}), which is the convention SPEC.md §5.1 already
21
+ * uses for `channels.telegram.token_env` and §5.2 for
22
+ * `audit.sampling_secret_env`: the policy file an agent may read carries a
23
+ * variable name, never a value.
24
+ *
25
+ * ## Threat model, stated plainly (SPEC.md §11)
26
+ *
27
+ * **What the vault defends.** Credentials at rest: a repository, a backup, or a
28
+ * synced folder that ends up somewhere it should not be carries ciphertext and
29
+ * a KDF header, not an SMTP password. And casual reads by an agent with file
30
+ * access: an agent that can `cat` every file in the working tree learns the
31
+ * NAMES of nothing and the values of nothing, because the names live inside the
32
+ * ciphertext too.
33
+ *
34
+ * **What it does not defend.** A compromised host. An agent that can read the
35
+ * passphrase environment variable — such an agent can decrypt the file at
36
+ * leisure, and no arrangement of this module changes that, which is why the
37
+ * passphrase belongs in an operator-held environment (a keychain-populated
38
+ * shell, a systemd credential) and outside every agent-readable path. Nor does
39
+ * it defend against an operator who exports the passphrase into the same
40
+ * process an agent drives. This is the same boundary SPEC.md §11 already draws
41
+ * for the sampling secret and for human identity: the trust boundary is the
42
+ * local machine, and anyone who can set that configuration is inside it. The
43
+ * vault raises the cost of a credential leak from "read a file" to "own the
44
+ * session"; it is not a claim of secrecy against the session's owner.
45
+ *
46
+ * ## Invariant 3 is the whole module
47
+ *
48
+ * SPEC.md §11.1 invariant 3 — raw secrets never appear in the log — is extended
49
+ * here to every surface this module touches. No credential VALUE appears in a
50
+ * return value except {@link getCredential}'s, in no message, no error, no
51
+ * refusal, and nothing here writes to the log at all. {@link listCredentials}
52
+ * returns names and a count. `set` and `remove` return counts. That asymmetry is
53
+ * deliberate and is pinned by a test: the module has exactly one function that
54
+ * can hand back a credential, and the adapter contract is what decides when it
55
+ * may be called.
56
+ *
57
+ * ## Determinism, and the one place it stops
58
+ *
59
+ * Reads are pure functions of the file bytes and the passphrase. Writes are not:
60
+ * every write draws a fresh 96-bit nonce and (on creation) a fresh 128-bit salt,
61
+ * so two writes of the same map produce different files. That is required, not
62
+ * incidental — GCM is catastrophically broken by nonce reuse under one key, and
63
+ * a deterministic file would also leak "nothing changed" to an observer who only
64
+ * sees the ciphertext.
65
+ *
66
+ * Nothing here throws. Every failure is a `{ ok: false, code, message }` from
67
+ * the frozen union {@link VAULT_REFUSAL_CODES}.
68
+ */
69
+ import type { PolicyLoadResult } from "./policy-load.js";
70
+ /** The vault's filename, beside the log's home: `.approval/vault.enc`. */
71
+ export declare const VAULT_FILENAME = "vault.enc";
72
+ /**
73
+ * The environment variable the passphrase is read from when the policy declares
74
+ * no `vault.passphrase_env`.
75
+ *
76
+ * A default rather than a hard requirement, because a runtime with no policy at
77
+ * all (or with an unparseable one) must still be able to open a vault an
78
+ * operator created: the variable name is not a permission, and treating it as
79
+ * one would mean an unrelated policy typo locked the credentials.
80
+ */
81
+ export declare const DEFAULT_PASSPHRASE_ENV = "APPROVAL_VAULT_PASSPHRASE";
82
+ /**
83
+ * The vault file for a given log path — the convention every caller uses.
84
+ *
85
+ * Derived exactly as `payloadStoreDirFor` derives the payload store, so the
86
+ * vault, the store, and the log stay together under one home: SPEC.md §9 fixes
87
+ * the log at `<home>/log/events.jsonl`, so the vault is `<home>/vault.enc`, a
88
+ * sibling of the log DIRECTORY and never inside it. Pointing `--log` at some
89
+ * other layout puts the vault beside that file instead.
90
+ */
91
+ export declare function vaultPathFor(logPath: string): string;
92
+ /**
93
+ * The NAME of the environment variable this policy says the passphrase lives in.
94
+ *
95
+ * The name only, in both directions: a policy that carried a passphrase would be
96
+ * a passphrase in a file agents may read, which is the thing the vault exists to
97
+ * avoid. A policy that failed to load names nothing, so the default applies.
98
+ */
99
+ export declare function passphraseEnvFor(load: PolicyLoadResult): string;
100
+ /** Is there a vault file at this path? Says nothing about whether it opens. */
101
+ export declare function vaultExists(vaultPath: string): boolean;
102
+ /**
103
+ * Everything this module can refuse. Frozen public API, per SPEC.md §11.1(6).
104
+ *
105
+ * Each code names a different repair, which is the test of whether a code earns
106
+ * its place. Note the one deliberate *conflation*: {@link "vault-unreadable"}
107
+ * covers both a wrong passphrase and an altered file, and the message says so
108
+ * rather than choosing. Distinguishing them would publish an oracle — a caller
109
+ * who could tell "your passphrase is wrong" from "these bytes were tampered
110
+ * with" could confirm a guessed passphrase against a file they had modified, and
111
+ * GCM's authentication tag cannot tell the two apart anyway without first
112
+ * trusting one of them.
113
+ */
114
+ export declare const VAULT_REFUSAL_CODES: readonly [
115
+ /** No vault file exists. The repair is `approval vault set <name>`. */
116
+ "vault-absent",
117
+ /** The named environment variable is unset or empty in this process. */
118
+ "passphrase-unset",
119
+ /** The file could not be read or written. A filesystem fact, not a secret. */
120
+ "vault-io",
121
+ /** The file is not the JSON envelope this module writes, or its header lies. */
122
+ "vault-malformed",
123
+ /** The file declares a format version this build does not implement. */
124
+ "vault-version-unsupported",
125
+ /**
126
+ * The ciphertext did not authenticate: the passphrase is wrong OR the file
127
+ * was altered. Deliberately not distinguished — see the union's own doc.
128
+ */
129
+ "vault-unreadable",
130
+ /** The vault opened and holds no credential under that name. */
131
+ "credential-absent",
132
+ /** The credential name is empty or not a usable name. */
133
+ "invalid-name",
134
+ /** The credential value is empty. An empty secret is a configuration error. */
135
+ "empty-value",
136
+ /** The re-encrypted file could not be put in place. Nothing was changed. */
137
+ "vault-write-failed"];
138
+ export type VaultRefusalCode = (typeof VAULT_REFUSAL_CODES)[number];
139
+ /** Every failure of this module. Nothing here throws. */
140
+ export interface VaultRefusal {
141
+ ok: false;
142
+ code: VaultRefusalCode;
143
+ message: string;
144
+ /** The file the refusal is about, so a caller need not reconstruct it. */
145
+ path: string;
146
+ }
147
+ /**
148
+ * The only format version this build writes or reads.
149
+ *
150
+ * Versioned from the first byte so that a future scheme (a different AEAD, an
151
+ * Argon2 KDF, a hardware-backed key) is a migration with two readers rather than
152
+ * a silent reinterpretation of old bytes. A file declaring anything else is
153
+ * refused {@link "vault-version-unsupported"} and left untouched: guessing at an
154
+ * unknown layout is how a decryption bug becomes a corrupted vault.
155
+ */
156
+ export declare const VAULT_FORMAT_VERSION = 1;
157
+ /**
158
+ * scrypt cost parameters, written into every file and read back from it.
159
+ *
160
+ * `N = 16384, r = 8, p = 1` is the classic interactive tuning: about 16 MiB of
161
+ * memory (128·N·r) and something on the order of 100 ms on a laptop, which is a
162
+ * meaningful brute-force cost against a human-typed passphrase while staying
163
+ * fast enough for a CLI verb a human runs interactively. `keylen` is 32 bytes,
164
+ * because AES-256-GCM takes a 256-bit key.
165
+ *
166
+ * The parameters live in the FILE rather than only in this constant so that a
167
+ * vault written under one tuning still opens after the tuning changes. They are
168
+ * bounded on read ({@link readKdf}) rather than trusted: a header claiming
169
+ * `N = 2^40` would otherwise be a denial-of-service delivered as a config file.
170
+ */
171
+ export declare const SCRYPT_PARAMS: {
172
+ readonly N: 16384;
173
+ readonly r: 8;
174
+ readonly p: 1;
175
+ readonly keylen: 32;
176
+ };
177
+ /** What every mutating operation reports. Counts and names, never values. */
178
+ export interface VaultWriteResult {
179
+ ok: true;
180
+ path: string;
181
+ name: string;
182
+ /** How many credentials the vault holds after the operation. */
183
+ count: number;
184
+ /** True when this call added a name the vault did not already hold. */
185
+ created: boolean;
186
+ }
187
+ /**
188
+ * Store `value` under `name`, creating the vault when it does not exist.
189
+ *
190
+ * The whole map is decrypted, amended, and re-encrypted under a fresh nonce:
191
+ * there is no partial update, because an AEAD over the whole document is what
192
+ * makes a partial edit detectable in the first place.
193
+ *
194
+ * `value` is a parameter and nothing else — never logged, never echoed, never
195
+ * placed in a message, including in the refusals this can return.
196
+ */
197
+ export declare function setCredential(vaultPath: string, passphrase: string, name: string, value: string): VaultWriteResult | VaultRefusal;
198
+ /**
199
+ * Delete `name`. A name the vault does not hold refuses `credential-absent`
200
+ * rather than reporting success: an operator removing a credential wants to know
201
+ * whether they removed the one they meant.
202
+ */
203
+ export declare function removeCredential(vaultPath: string, passphrase: string, name: string): VaultWriteResult | VaultRefusal;
204
+ /** What the vault holds, by name. There is no shape here that carries a value. */
205
+ export interface VaultListing {
206
+ ok: true;
207
+ path: string;
208
+ /** Sorted credential names. */
209
+ names: string[];
210
+ count: number;
211
+ }
212
+ /**
213
+ * The names in the vault, sorted, and how many there are.
214
+ *
215
+ * Names and nothing else, on every path including the error ones. Sorted so two
216
+ * listings of the same vault are byte-identical, which is what lets a test and
217
+ * an operator compare them.
218
+ */
219
+ export declare function listCredentials(vaultPath: string, passphrase: string): VaultListing | VaultRefusal;
220
+ /** The one shape in this module that carries a credential value. */
221
+ export interface VaultCredential {
222
+ ok: true;
223
+ path: string;
224
+ name: string;
225
+ value: string;
226
+ }
227
+ /**
228
+ * The value stored under `name`.
229
+ *
230
+ * **The only function here that returns a credential**, which is the module's
231
+ * structural rule and is pinned by `tests/vault.test.ts`. There is no
232
+ * `approval vault get`, because a verb that printed a credential would put it
233
+ * in a terminal, a scrollback buffer, a CI log, and a shell history, and would
234
+ * do so on a machine where the whole point is that the value only ever travels
235
+ * from this file into the use it was stored for.
236
+ *
237
+ * **Two sanctioned callers** (flagged by APRV-220, decided by APRV-257):
238
+ *
239
+ * 1. `adapters/vault-provider.ts`, whose provider `executeThroughAdapter`
240
+ * scopes to the verified-token window.
241
+ * 2. `cli/checkpoint-tap.ts`, which reads `approval.checkpoint.key` and hands
242
+ * it to `core/checkpoint.ts`'s signer — the one custody decision for every
243
+ * surface that can take a checkpoint.
244
+ *
245
+ * The rule this list keeps is not "one caller". It is that a credential's value
246
+ * goes from this file into a USE and never onto a SURFACE, and both callers
247
+ * obey it: the second takes a private key into an Ed25519 signature and hands
248
+ * back a signature and a fingerprint of the PUBLIC half.
249
+ *
250
+ * The alternative — move the checkpoint key somewhere else and keep this list
251
+ * at one name — was considered and rejected, because it would have made the key
252
+ * weaker rather than the module cleaner. The OS keystore has no equivalent of
253
+ * the passphrase variable `core/child-env.ts` strips from every child
254
+ * (APRV-205), and a file beside the log has no encryption at all. A checkpoint
255
+ * key is the one secret whose entire value is that a process an agent launched
256
+ * cannot reach it, so it belongs in the strictest store this runtime has. What
257
+ * that costs is a second name in this comment.
258
+ */
259
+ export declare function getCredential(vaultPath: string, passphrase: string, name: string): VaultCredential | VaultRefusal;
260
+ /**
261
+ * Does this passphrase open the vault, and how many credentials does it hold?
262
+ *
263
+ * The diagnostic form, for `approval doctor`: a yes/no plus a count, with no
264
+ * name and no value in either the success or the failure. Distinct from
265
+ * {@link listCredentials} because a health check has no business learning which
266
+ * credentials an operator keeps.
267
+ */
268
+ export declare function checkVault(vaultPath: string, passphrase: string): {
269
+ ok: true;
270
+ path: string;
271
+ count: number;
272
+ } | VaultRefusal;
273
+ /**
274
+ * Read the passphrase from the environment variable named by `envName`.
275
+ *
276
+ * Returns the value, or `null` when it is unset or empty. A separate function
277
+ * so that every caller reads the passphrase the same way and so that no caller
278
+ * is tempted to accept one as a flag: a passphrase on a command line is a
279
+ * passphrase in the shell history, in `ps`, and in the parent process's
280
+ * environment.
281
+ */
282
+ export declare function passphraseFrom(envName: string, env?: NodeJS.ProcessEnv): string | null;
283
+ /**
284
+ * Constant-time equality for two secrets, exported for callers that must
285
+ * compare one without leaking its length-prefix through timing.
286
+ *
287
+ * Not used by the vault's own paths (nothing here compares credentials), but the
288
+ * alternative — a caller writing `a === b` over a secret — is the kind of thing
289
+ * that gets written once and copied five times.
290
+ */
291
+ export declare function secretsEqual(left: string, right: string): boolean;