approval-md 0.3.0 → 0.4.1

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 (270) hide show
  1. package/README.md +83 -914
  2. package/SPEC.md +101 -13
  3. package/cli.js +134 -7
  4. package/dist/src/adapters/smtp.d.ts +18 -0
  5. package/dist/src/adapters/smtp.js +24 -3
  6. package/dist/src/adapters/smtp.js.map +1 -1
  7. package/dist/src/channels/contract.d.ts +72 -0
  8. package/dist/src/channels/contract.js +161 -1
  9. package/dist/src/channels/contract.js.map +1 -1
  10. package/dist/src/channels/relay-server.d.ts +90 -0
  11. package/dist/src/channels/relay-server.js +295 -0
  12. package/dist/src/channels/relay-server.js.map +1 -0
  13. package/dist/src/channels/relay.d.ts +273 -0
  14. package/dist/src/channels/relay.js +564 -0
  15. package/dist/src/channels/relay.js.map +1 -0
  16. package/dist/src/channels/tagging.d.ts +25 -0
  17. package/dist/src/channels/tagging.js +63 -6
  18. package/dist/src/channels/tagging.js.map +1 -1
  19. package/dist/src/channels/telegram-minimal.d.ts +251 -0
  20. package/dist/src/channels/telegram-minimal.js +618 -0
  21. package/dist/src/channels/telegram-minimal.js.map +1 -0
  22. package/dist/src/channels/telegram-webhook.d.ts +273 -0
  23. package/dist/src/channels/telegram-webhook.js +536 -0
  24. package/dist/src/channels/telegram-webhook.js.map +1 -0
  25. package/dist/src/channels/telegram.d.ts +725 -20
  26. package/dist/src/channels/telegram.js +1332 -123
  27. package/dist/src/channels/telegram.js.map +1 -1
  28. package/dist/src/cli/amend.js +358 -5
  29. package/dist/src/cli/amend.js.map +1 -1
  30. package/dist/src/cli/attest.js +43 -6
  31. package/dist/src/cli/attest.js.map +1 -1
  32. package/dist/src/cli/audit-card.d.ts +1 -1
  33. package/dist/src/cli/audit-card.js +40 -1
  34. package/dist/src/cli/audit-card.js.map +1 -1
  35. package/dist/src/cli/audit.d.ts +7 -5
  36. package/dist/src/cli/audit.js +22 -10
  37. package/dist/src/cli/audit.js.map +1 -1
  38. package/dist/src/cli/channel-relay.d.ts +108 -0
  39. package/dist/src/cli/channel-relay.js +334 -0
  40. package/dist/src/cli/channel-relay.js.map +1 -0
  41. package/dist/src/cli/channel-telegram-webhook.d.ts +275 -0
  42. package/dist/src/cli/channel-telegram-webhook.js +716 -0
  43. package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
  44. package/dist/src/cli/channel-telegram.d.ts +347 -28
  45. package/dist/src/cli/channel-telegram.js +648 -56
  46. package/dist/src/cli/channel-telegram.js.map +1 -1
  47. package/dist/src/cli/channel.d.ts +1 -1
  48. package/dist/src/cli/channel.js +5 -1
  49. package/dist/src/cli/channel.js.map +1 -1
  50. package/dist/src/cli/daemon.js +37 -5
  51. package/dist/src/cli/daemon.js.map +1 -1
  52. package/dist/src/cli/doctor.d.ts +63 -0
  53. package/dist/src/cli/doctor.js +856 -111
  54. package/dist/src/cli/doctor.js.map +1 -1
  55. package/dist/src/cli/execute.d.ts +29 -1
  56. package/dist/src/cli/execute.js +387 -21
  57. package/dist/src/cli/execute.js.map +1 -1
  58. package/dist/src/cli/exit-codes.d.ts +12 -0
  59. package/dist/src/cli/exit-codes.js +13 -0
  60. package/dist/src/cli/exit-codes.js.map +1 -1
  61. package/dist/src/cli/gate.d.ts +2 -0
  62. package/dist/src/cli/gate.js +233 -2
  63. package/dist/src/cli/gate.js.map +1 -1
  64. package/dist/src/cli/help.d.ts +10 -1
  65. package/dist/src/cli/help.js +283 -54
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook.d.ts +485 -6
  68. package/dist/src/cli/hook.js +2370 -70
  69. package/dist/src/cli/hook.js.map +1 -1
  70. package/dist/src/cli/init.js +16 -7
  71. package/dist/src/cli/init.js.map +1 -1
  72. package/dist/src/cli/instructions.js +24 -3
  73. package/dist/src/cli/instructions.js.map +1 -1
  74. package/dist/src/cli/log-anchor.d.ts +55 -0
  75. package/dist/src/cli/log-anchor.js +199 -60
  76. package/dist/src/cli/log-anchor.js.map +1 -1
  77. package/dist/src/cli/log-unlock.d.ts +23 -0
  78. package/dist/src/cli/log-unlock.js +109 -0
  79. package/dist/src/cli/log-unlock.js.map +1 -0
  80. package/dist/src/cli/main.d.ts +0 -12
  81. package/dist/src/cli/main.js +72 -4
  82. package/dist/src/cli/main.js.map +1 -1
  83. package/dist/src/cli/muse.d.ts +2 -0
  84. package/dist/src/cli/muse.js +62 -0
  85. package/dist/src/cli/muse.js.map +1 -0
  86. package/dist/src/cli/payload.js +79 -1
  87. package/dist/src/cli/payload.js.map +1 -1
  88. package/dist/src/cli/scaffold.js +6 -0
  89. package/dist/src/cli/scaffold.js.map +1 -1
  90. package/dist/src/cli/serve.d.ts +53 -0
  91. package/dist/src/cli/serve.js +270 -0
  92. package/dist/src/cli/serve.js.map +1 -0
  93. package/dist/src/cli/up.js +26 -2
  94. package/dist/src/cli/up.js.map +1 -1
  95. package/dist/src/cli/verb-registry.d.ts +2 -0
  96. package/dist/src/cli/verb-registry.js +338 -39
  97. package/dist/src/cli/verb-registry.js.map +1 -1
  98. package/dist/src/cli/wordmark.d.ts +2 -2
  99. package/dist/src/cli/wordmark.js +2 -2
  100. package/dist/src/core/attest-bootstrap.d.ts +95 -0
  101. package/dist/src/core/attest-bootstrap.js +135 -0
  102. package/dist/src/core/attest-bootstrap.js.map +1 -0
  103. package/dist/src/core/attest.d.ts +20 -3
  104. package/dist/src/core/attest.js +25 -0
  105. package/dist/src/core/attest.js.map +1 -1
  106. package/dist/src/core/audit.d.ts +153 -12
  107. package/dist/src/core/audit.js +415 -20
  108. package/dist/src/core/audit.js.map +1 -1
  109. package/dist/src/core/channel-lease.d.ts +322 -0
  110. package/dist/src/core/channel-lease.js +663 -0
  111. package/dist/src/core/channel-lease.js.map +1 -0
  112. package/dist/src/core/command-class.d.ts +51 -0
  113. package/dist/src/core/command-class.js +766 -8
  114. package/dist/src/core/command-class.js.map +1 -1
  115. package/dist/src/core/daemon-host.d.ts +178 -0
  116. package/dist/src/core/daemon-host.js +228 -0
  117. package/dist/src/core/daemon-host.js.map +1 -0
  118. package/dist/src/core/daemon-identity.d.ts +238 -0
  119. package/dist/src/core/daemon-identity.js +252 -0
  120. package/dist/src/core/daemon-identity.js.map +1 -0
  121. package/dist/src/core/gate.d.ts +364 -6
  122. package/dist/src/core/gate.js +802 -27
  123. package/dist/src/core/gate.js.map +1 -1
  124. package/dist/src/core/harness-version.d.ts +128 -4
  125. package/dist/src/core/harness-version.js +179 -12
  126. package/dist/src/core/harness-version.js.map +1 -1
  127. package/dist/src/core/harness-wait.d.ts +82 -3
  128. package/dist/src/core/harness-wait.js +89 -3
  129. package/dist/src/core/harness-wait.js.map +1 -1
  130. package/dist/src/core/log-lock.d.ts +435 -0
  131. package/dist/src/core/log-lock.js +1093 -0
  132. package/dist/src/core/log-lock.js.map +1 -0
  133. package/dist/src/core/log-subscribe.d.ts +21 -0
  134. package/dist/src/core/log-subscribe.js +16 -6
  135. package/dist/src/core/log-subscribe.js.map +1 -1
  136. package/dist/src/core/log-write-layer.d.ts +55 -0
  137. package/dist/src/core/log-write-layer.js +91 -0
  138. package/dist/src/core/log-write-layer.js.map +1 -0
  139. package/dist/src/core/log.d.ts +130 -22
  140. package/dist/src/core/log.js +473 -50
  141. package/dist/src/core/log.js.map +1 -1
  142. package/dist/src/core/payload-census.d.ts +51 -0
  143. package/dist/src/core/payload-census.js +125 -2
  144. package/dist/src/core/payload-census.js.map +1 -1
  145. package/dist/src/core/payload-store.d.ts +31 -0
  146. package/dist/src/core/payload-store.js +113 -8
  147. package/dist/src/core/payload-store.js.map +1 -1
  148. package/dist/src/core/payload.d.ts +18 -3
  149. package/dist/src/core/payload.js +20 -4
  150. package/dist/src/core/payload.js.map +1 -1
  151. package/dist/src/core/policy-diff.d.ts +10 -0
  152. package/dist/src/core/policy-diff.js +10 -2
  153. package/dist/src/core/policy-diff.js.map +1 -1
  154. package/dist/src/core/policy-explain.d.ts +7 -1
  155. package/dist/src/core/policy-explain.js +16 -1
  156. package/dist/src/core/policy-explain.js.map +1 -1
  157. package/dist/src/core/policy-load.d.ts +24 -0
  158. package/dist/src/core/policy-load.js.map +1 -1
  159. package/dist/src/core/policy-match.d.ts +38 -0
  160. package/dist/src/core/policy-match.js +35 -0
  161. package/dist/src/core/policy-match.js.map +1 -1
  162. package/dist/src/core/policy-proposal.d.ts +40 -0
  163. package/dist/src/core/policy-proposal.js +37 -16
  164. package/dist/src/core/policy-proposal.js.map +1 -1
  165. package/dist/src/core/prompt-layout.d.ts +186 -1
  166. package/dist/src/core/prompt-layout.js +345 -3
  167. package/dist/src/core/prompt-layout.js.map +1 -1
  168. package/dist/src/core/read-scope.d.ts +10 -0
  169. package/dist/src/core/read-scope.js +4 -0
  170. package/dist/src/core/read-scope.js.map +1 -1
  171. package/dist/src/core/run-payload.d.ts +160 -0
  172. package/dist/src/core/run-payload.js +304 -0
  173. package/dist/src/core/run-payload.js.map +1 -0
  174. package/dist/src/core/sender-identity.d.ts +59 -11
  175. package/dist/src/core/sender-identity.js +79 -11
  176. package/dist/src/core/sender-identity.js.map +1 -1
  177. package/dist/src/core/state.d.ts +52 -1
  178. package/dist/src/core/state.js +52 -6
  179. package/dist/src/core/state.js.map +1 -1
  180. package/dist/src/core/telegram-config.d.ts +79 -0
  181. package/dist/src/core/telegram-config.js +108 -0
  182. package/dist/src/core/telegram-config.js.map +1 -1
  183. package/dist/src/core/validate.d.ts +20 -6
  184. package/dist/src/core/validate.js +0 -0
  185. package/dist/src/core/validate.js.map +1 -1
  186. package/dist/src/core/verify.d.ts +23 -1
  187. package/dist/src/core/verify.js +20 -2
  188. package/dist/src/core/verify.js.map +1 -1
  189. package/dist/src/daemon/audit.d.ts +80 -5
  190. package/dist/src/daemon/audit.js +126 -6
  191. package/dist/src/daemon/audit.js.map +1 -1
  192. package/dist/src/daemon/daemon.d.ts +174 -2
  193. package/dist/src/daemon/daemon.js +298 -7
  194. package/dist/src/daemon/daemon.js.map +1 -1
  195. package/dist/src/daemon/git-evidence.d.ts +2 -2
  196. package/dist/src/daemon/git-evidence.js +1 -1
  197. package/dist/src/daemon/projection.d.ts +13 -2
  198. package/dist/src/daemon/projection.js +39 -6
  199. package/dist/src/daemon/projection.js.map +1 -1
  200. package/dist/src/mcp/server.d.ts +39 -3
  201. package/dist/src/mcp/server.js +22 -3
  202. package/dist/src/mcp/server.js.map +1 -1
  203. package/dist/src/muse/server.d.ts +16 -0
  204. package/dist/src/muse/server.js +238 -0
  205. package/dist/src/muse/server.js.map +1 -0
  206. package/dist/src/serve/archive.d.ts +166 -0
  207. package/dist/src/serve/archive.js +363 -0
  208. package/dist/src/serve/archive.js.map +1 -0
  209. package/dist/src/serve/arguments.d.ts +93 -0
  210. package/dist/src/serve/arguments.js +228 -0
  211. package/dist/src/serve/arguments.js.map +1 -0
  212. package/dist/src/serve/credentials.d.ts +91 -0
  213. package/dist/src/serve/credentials.js +143 -0
  214. package/dist/src/serve/credentials.js.map +1 -0
  215. package/dist/src/serve/follow.d.ts +86 -0
  216. package/dist/src/serve/follow.js +101 -0
  217. package/dist/src/serve/follow.js.map +1 -0
  218. package/dist/src/serve/hook-thread.d.ts +134 -0
  219. package/dist/src/serve/hook-thread.js +333 -0
  220. package/dist/src/serve/hook-thread.js.map +1 -0
  221. package/dist/src/serve/hook-worker.d.ts +134 -0
  222. package/dist/src/serve/hook-worker.js +173 -0
  223. package/dist/src/serve/hook-worker.js.map +1 -0
  224. package/dist/src/serve/server.d.ts +412 -0
  225. package/dist/src/serve/server.js +1251 -0
  226. package/dist/src/serve/server.js.map +1 -0
  227. package/docs/README-extended.md +768 -0
  228. package/docs/cli-reference.md +2139 -67
  229. package/package.json +4 -2
  230. package/schema/event.schema.json +413 -30
  231. package/schema/fixtures/event/invalid/audit-lock-reclaimed-age-without-created.json +19 -0
  232. package/schema/fixtures/event/invalid/audit-lock-reclaimed-agent-actor.json +14 -0
  233. package/schema/fixtures/event/invalid/audit-lock-reclaimed-lockfile-path.json +14 -0
  234. package/schema/fixtures/event/invalid/audit-lock-reclaimed-operator-cleared-system-actor.json +14 -0
  235. package/schema/fixtures/event/invalid/audit-lock-reclaimed-unknown-reason.json +14 -0
  236. package/schema/fixtures/event/invalid/audit-lock-reclaimed-without-age.json +13 -0
  237. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +6 -1
  238. package/schema/fixtures/event/invalid/audit-reviewed-bad-payload-hash.json +21 -0
  239. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +5 -0
  240. package/schema/fixtures/event/invalid/audit-reviewed-no-subject-hash.json +19 -0
  241. package/schema/fixtures/event/invalid/audit-reviewed-no-subject-seq.json +19 -0
  242. package/schema/fixtures/event/invalid/audit-reviewed-no-verdict-source.json +19 -0
  243. package/schema/fixtures/event/invalid/audit-reviewed-no-verdict.json +19 -0
  244. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +6 -1
  245. package/schema/fixtures/event/invalid/audit-sampled-no-policy-hash.json +14 -0
  246. package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
  247. package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
  248. package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
  249. package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
  250. package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
  251. package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
  252. package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
  253. package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
  254. package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
  255. package/schema/fixtures/event/valid/audit-lock-reclaimed-implausible-created.json +18 -0
  256. package/schema/fixtures/event/valid/audit-lock-reclaimed-legacy.json +14 -0
  257. package/schema/fixtures/event/valid/audit-lock-reclaimed-unlock.json +19 -0
  258. package/schema/fixtures/event/valid/audit-lock-reclaimed.json +19 -0
  259. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +5 -0
  260. package/schema/fixtures/event/valid/audit-reviewed.json +6 -1
  261. package/schema/fixtures/event/valid/audit-sampled.json +2 -1
  262. package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
  263. package/schema/fixtures/event/valid/daemon-identity.json +16 -0
  264. package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
  265. package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
  266. package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
  267. package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
  268. package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
  269. package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
  270. package/schema/policy.schema.json +102 -6
@@ -0,0 +1,363 @@
1
+ /**
2
+ * The store as an archive (APRV-421): a POSIX ustar tar, gzipped, written here
3
+ * rather than by a library.
4
+ *
5
+ * ## Why a writer and not a dependency
6
+ *
7
+ * The repository rule is minimal dependencies, justified per addition, and the
8
+ * justification for a tar library would have to survive one question: what does
9
+ * it do that ninety lines here do not? A ustar header is a fixed 512-byte
10
+ * record with octal fields and a checksum, `node:zlib` already ships the
11
+ * compression, and the archive this verb produces contains a handful of files
12
+ * whose paths are short and known. A dependency would add a supply-chain
13
+ * surface to a process that hands a tenant their own evidence, in exchange for
14
+ * code this file can state completely.
15
+ *
16
+ * ## The contents are a POSITIVE allowlist
17
+ *
18
+ * {@link EXPORTED_PATHS} names what goes in. Everything else is out by not
19
+ * being named, which is the fail-closed direction SPEC.md §11 asks for
20
+ * everywhere else: `.approval/keys`, `.approval/env`, `.approval/daemon` and
21
+ * any `vault.enc` are excluded because nothing admits them, not because a
22
+ * denylist remembered them. A file added to the store next month is absent
23
+ * from this archive until somebody decides it belongs, and that decision is a
24
+ * diff here.
25
+ *
26
+ * `.approval/payloads/` IS on the list, and it is the entry the reasoning
27
+ * turns on. The log records a `payload_hash` for every action a human was
28
+ * shown, and the bytes those hashes name live in that directory. An archive
29
+ * carrying the hashes without the bytes would hand a tenant a chain of
30
+ * references to evidence they no longer hold: a receipt for an exit rather
31
+ * than an exit. Payload bytes are the tenant's own and are not credential
32
+ * material — the vault, the keys and the environment source map are, and
33
+ * those are the things excluded below.
34
+ *
35
+ * ## What the reader is for
36
+ *
37
+ * {@link readTarEntries} exists so that a test, and a conformance reader after
38
+ * it, can assert what an archive CONTAINS rather than trusting the writer that
39
+ * produced it. A test that checked the writer against itself would pass on the
40
+ * day the writer put the vault in.
41
+ */
42
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync } from "node:fs";
43
+ import { join, posix, relative, sep } from "node:path";
44
+ import { gzipSync } from "node:zlib";
45
+ /** One tar block. The format's unit, and every offset below is inside one. */
46
+ const BLOCK = 512;
47
+ /**
48
+ * What an export carries, relative to the store root, in archive order.
49
+ *
50
+ * A trailing `/` means "this directory, recursively". Everything else is one
51
+ * file, included when it exists and silently absent when it does not: a store
52
+ * whose index has never been built is an ordinary store, not a broken one.
53
+ */
54
+ export const EXPORTED_PATHS = [
55
+ "APPROVAL.md",
56
+ ".approval/log/",
57
+ ".approval/payloads/",
58
+ ".approval/QUEUE.md",
59
+ ".approval/index.sqlite",
60
+ ];
61
+ /**
62
+ * Paths an export MUST NOT carry, checked as well as excluded.
63
+ *
64
+ * The allowlist above is what decides; this list is what a test asserts
65
+ * against, so the property "no credential material left the store" is written
66
+ * down as a property and not merely implied by the absence of an entry.
67
+ */
68
+ export const EXCLUDED_PREFIXES = [
69
+ ".approval/keys",
70
+ ".approval/env",
71
+ ".approval/daemon",
72
+ ];
73
+ /** Any file with this name is credential material, wherever it sits. */
74
+ export const EXCLUDED_BASENAMES = ["vault.enc"];
75
+ function octal(value, width) {
76
+ // `width - 1` digits and a NUL, which is what every reader accepts and what
77
+ // GNU tar itself writes for these fields.
78
+ return value.toString(8).padStart(width - 1, "0").slice(-(width - 1)) + "\0";
79
+ }
80
+ function writeString(block, offset, width, value) {
81
+ block.write(value.slice(0, width), offset, width, "utf8");
82
+ }
83
+ /**
84
+ * Split a path into ustar's `prefix` and `name` fields.
85
+ *
86
+ * Refused rather than truncated when it does not fit: a truncated path in an
87
+ * archive is a file restored to the wrong place, which is worse than an export
88
+ * that says plainly it cannot represent this store.
89
+ */
90
+ export function splitTarName(path) {
91
+ if (Buffer.byteLength(path) <= 100)
92
+ return { name: path, prefix: "" };
93
+ const cut = path.lastIndexOf("/", path.length - 1);
94
+ for (let index = cut; index > 0; index -= 1) {
95
+ if (path[index] !== "/")
96
+ continue;
97
+ const prefix = path.slice(0, index);
98
+ const name = path.slice(index + 1);
99
+ if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100) {
100
+ return { name, prefix };
101
+ }
102
+ }
103
+ throw new Error(`${path} cannot be written into a ustar archive: the name exceeds the format's 100/155 byte fields`);
104
+ }
105
+ /** One 512-byte ustar header, checksum included. */
106
+ function header(entry) {
107
+ const block = Buffer.alloc(BLOCK, 0);
108
+ const { name, prefix } = splitTarName(entry.path);
109
+ writeString(block, 0, 100, name);
110
+ writeString(block, 100, 8, octal(0o644, 8));
111
+ writeString(block, 108, 8, octal(0, 8));
112
+ writeString(block, 116, 8, octal(0, 8));
113
+ writeString(block, 124, 12, octal(entry.data.length, 12));
114
+ writeString(block, 136, 12, octal(Math.floor(entry.mtime), 12));
115
+ // The checksum field is computed as though it held eight spaces.
116
+ block.write(" ", 148, 8, "utf8");
117
+ block.write("0", 156, 1, "utf8"); // typeflag: a regular file
118
+ block.write("ustar\0", 257, 6, "utf8");
119
+ block.write("00", 263, 2, "utf8");
120
+ writeString(block, 345, 155, prefix);
121
+ let sum = 0;
122
+ for (const byte of block)
123
+ sum += byte;
124
+ // Six octal digits, a NUL and a space: the spelling every reader accepts.
125
+ block.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "utf8");
126
+ return block;
127
+ }
128
+ function padding(size) {
129
+ const remainder = size % BLOCK;
130
+ return remainder === 0 ? Buffer.alloc(0) : Buffer.alloc(BLOCK - remainder, 0);
131
+ }
132
+ /** Write `entries` as an uncompressed ustar archive. */
133
+ export function writeTar(entries) {
134
+ const parts = [];
135
+ for (const entry of entries) {
136
+ parts.push(header(entry), entry.data, padding(entry.data.length));
137
+ }
138
+ // Two zero blocks end the archive; a reader that stops at the first one is
139
+ // still correct, and one that requires both is satisfied.
140
+ parts.push(Buffer.alloc(BLOCK * 2, 0));
141
+ return Buffer.concat(parts);
142
+ }
143
+ /**
144
+ * Read an uncompressed ustar archive back.
145
+ *
146
+ * Deliberately strict about the checksum: the point of this function is to be
147
+ * an independent reader of what {@link writeTar} produced, and a reader that
148
+ * ignored the field the writer computes would not be independent of it.
149
+ */
150
+ export function readTarEntries(archive) {
151
+ const entries = [];
152
+ let offset = 0;
153
+ while (offset + BLOCK <= archive.length) {
154
+ const block = archive.subarray(offset, offset + BLOCK);
155
+ if (block.every((byte) => byte === 0))
156
+ break;
157
+ const declared = Number.parseInt(block.subarray(148, 156).toString("utf8").trim(), 8);
158
+ let sum = 0;
159
+ for (const [index, byte] of block.entries()) {
160
+ sum += index >= 148 && index < 156 ? 0x20 : byte;
161
+ }
162
+ if (sum !== declared) {
163
+ throw new Error(`tar header checksum mismatch at offset ${String(offset)}`);
164
+ }
165
+ const trim = (buffer) => {
166
+ const end = buffer.indexOf(0);
167
+ return buffer.subarray(0, end === -1 ? buffer.length : end).toString("utf8");
168
+ };
169
+ const name = trim(block.subarray(0, 100));
170
+ const prefix = trim(block.subarray(345, 500));
171
+ const size = Number.parseInt(trim(block.subarray(124, 136)) || "0", 8);
172
+ const mtime = Number.parseInt(trim(block.subarray(136, 148)) || "0", 8);
173
+ offset += BLOCK;
174
+ entries.push({
175
+ path: prefix.length === 0 ? name : `${prefix}/${name}`,
176
+ data: Buffer.from(archive.subarray(offset, offset + size)),
177
+ mtime,
178
+ });
179
+ offset += size + padding(size).length;
180
+ }
181
+ return entries;
182
+ }
183
+ /**
184
+ * Is this store-relative path one an export may never carry?
185
+ *
186
+ * `excludedLock` is the ONE lockfile this server itself creates, passed in as
187
+ * an exact path rather than matched as a class. An earlier pass excluded
188
+ * `*.lock` by suffix, which quietly deleted a tenant's own
189
+ * `.approval/payloads/x.lock` from their archive: the tenant's bytes are the
190
+ * tenant's, and a name this runtime happens to use for its own bookkeeping is
191
+ * not a reason to drop somebody else's file. Only the path derived from the
192
+ * log is dropped, and only because the export holds that lock while it walks,
193
+ * so the file exists for exactly the span of the copy and means nothing but
194
+ * "somebody was reading when this was made".
195
+ *
196
+ * The same holds for the names the lock's reclaim derives from that exact path
197
+ * (APRV-479), every one `<lockfile>.<something>`: the claim on a dead holder's
198
+ * lockfile (`.stale.<pid>.<ms>`) and a taker's own lockfile before its rename
199
+ * (`.take.<pid>.<nonce>`), which a writer killed part way leaves behind. They
200
+ * are the lock's bookkeeping, they carry the writing machine's hostname and
201
+ * boot id (which the log itself deliberately does not), and nothing in them is
202
+ * the tenant's.
203
+ */
204
+ export function isExcludedPath(path, excludedLock = null) {
205
+ const base = path.split("/").at(-1) ?? path;
206
+ if (EXCLUDED_BASENAMES.includes(base))
207
+ return true;
208
+ if (excludedLock !== null && (path === excludedLock || path.startsWith(`${excludedLock}.`)))
209
+ return true;
210
+ return EXCLUDED_PREFIXES.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
211
+ }
212
+ /**
213
+ * A symlink the export walk reached.
214
+ *
215
+ * Thrown rather than returned because it must abort the WHOLE archive from
216
+ * wherever it is found, and an error is the one control flow that cannot be
217
+ * accidentally ignored by a caller that forgot to check a return value.
218
+ */
219
+ export class ExportSymlinkError extends Error {
220
+ /** The store-relative path of the link itself, never its target. */
221
+ path;
222
+ constructor(path) {
223
+ super(`${path} is a symbolic link. An export copies the tenant's own store and never what a link points at: a link under the store can name credential material inside it (\`.approval/keys\`, \`.approval/env\`, the vault) or any file on the host, and a copy that followed one would put those bytes in the tenant's archive. The whole export is refused rather than the link skipped, because an archive silently missing a file is an archive nobody can tell from a complete one. Replace the link with the file, or move it outside the store.`);
224
+ this.name = "ExportSymlinkError";
225
+ this.path = path;
226
+ }
227
+ }
228
+ /**
229
+ * A HARD link, which a name-based exclusion cannot see at all (APRV-421,
230
+ * second review pass).
231
+ *
232
+ * `ln .approval/keys/sender.key .approval/log/note.jsonl` produces a second
233
+ * name for the same inode. There is no link to refuse to follow and no target
234
+ * to notice: the excluded path and the admitted path ARE one file, and the
235
+ * admitted name is a perfectly ordinary regular file by every test a walker
236
+ * can apply to it except one — its link count.
237
+ *
238
+ * So `nlink > 1` on a regular file under the store is refused, fail-closed and
239
+ * whole, exactly as a symlink is. The false positive is a tenant who
240
+ * deliberately hard-linked something into their own store, which is rare, is
241
+ * visible in the refusal, and is repaired by copying the file.
242
+ */
243
+ export class ExportHardLinkError extends Error {
244
+ /** The store-relative path of the link, which is also the file. */
245
+ path;
246
+ /** How many names this inode has. */
247
+ links;
248
+ constructor(path, links) {
249
+ super(`${path} has ${String(links)} names (it is a hard link). A second name for one inode cannot be told from an ordinary file by its path, so an excluded file — a key under \`.approval/keys\`, the environment source map, the vault — can be given an admitted name and copied into the archive with nothing to notice. The whole export is refused rather than the file skipped, for the reason a symbolic link refuses it: an archive silently missing a file is one nobody can tell from a complete one. Replace the link with a copy.`);
250
+ this.name = "ExportHardLinkError";
251
+ this.path = path;
252
+ this.links = links;
253
+ }
254
+ }
255
+ function walk(root, relativeDir, into, lock) {
256
+ let names;
257
+ try {
258
+ names = readdirSync(join(root, relativeDir)).sort();
259
+ }
260
+ catch {
261
+ return;
262
+ }
263
+ for (const name of names) {
264
+ const relativePath = posix.join(relativeDir, name);
265
+ collect(root, relativePath, into, lock);
266
+ }
267
+ }
268
+ function collect(root, relativePath, into, lock) {
269
+ // Checked on every path rather than only on the roots: the allowlist decides
270
+ // what is offered and this decides what is accepted, and a nested `vault.enc`
271
+ // is refused by the second even when the first admitted the directory above
272
+ // it.
273
+ if (isExcludedPath(relativePath, lock))
274
+ return;
275
+ const absolute = join(root, relativePath);
276
+ let stats;
277
+ try {
278
+ // LSTAT, never stat (APRV-421). `stat` follows the link, so the exclusion
279
+ // check above would be reading one name while the copy below read another
280
+ // file entirely: `ln -s .approval/keys/sender.key .approval/log/note.jsonl`
281
+ // passes every name-based check ever written and hands over the key.
282
+ stats = lstatSync(absolute);
283
+ }
284
+ catch {
285
+ return;
286
+ }
287
+ if (stats.isSymbolicLink())
288
+ throw new ExportSymlinkError(relativePath);
289
+ if (stats.isDirectory()) {
290
+ walk(root, relativePath, into, lock);
291
+ return;
292
+ }
293
+ // Not a regular file: a fifo, a socket or a device. Skipped rather than
294
+ // refused, because none of them is a way to name another file's bytes, and
295
+ // a store that happens to hold one is not a store that is lying about what
296
+ // it contains.
297
+ if (!stats.isFile())
298
+ return;
299
+ // OPEN, then ask the DESCRIPTOR what it is (APRV-421, second review pass).
300
+ //
301
+ // `lstat` above and a `readFileSync(path)` below would be two resolutions of
302
+ // one name with a window between them, and the classic move is to replace a
303
+ // checked regular file with a symlink to somebody's key inside that window.
304
+ // `O_NOFOLLOW` refuses to open a symlink at all, and `fstat` describes THE
305
+ // FILE THAT WAS OPENED rather than whatever the name points at now — so the
306
+ // regular-file and link-count checks below are about the bytes actually read.
307
+ const fd = openSync(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
308
+ try {
309
+ const opened = fstatSync(fd);
310
+ if (!opened.isFile())
311
+ return;
312
+ if (opened.nlink > 1)
313
+ throw new ExportHardLinkError(relativePath, opened.nlink);
314
+ into.push({
315
+ path: relativePath,
316
+ data: readFileSync(fd),
317
+ mtime: Math.floor(opened.mtimeMs / 1000),
318
+ });
319
+ }
320
+ finally {
321
+ try {
322
+ closeSync(fd);
323
+ }
324
+ catch {
325
+ // `readFileSync(fd)` closes it on some paths; a double close is not an
326
+ // error worth propagating out of an export.
327
+ }
328
+ }
329
+ }
330
+ /**
331
+ * Build the export archive for the store rooted at `root`.
332
+ *
333
+ * Reads only; writes nothing anywhere, appends no record, and touches no
334
+ * credential. A path outside the allowlist cannot be reached from here, because
335
+ * the walk starts at the allowlist and nowhere else, and no path is followed
336
+ * through a link.
337
+ *
338
+ * Throws {@link ExportSymlinkError} when the walk meets a symbolic link. The
339
+ * caller turns that into a refusal naming the link's own relative path.
340
+ */
341
+ export function buildStoreArchive(root, logPath) {
342
+ // The one file this server's own append lock creates, as an exact path (and,
343
+ // by `isExcludedPath`, the reclaim's files named from it).
344
+ const lock = storeRelative(root, `${logPath}.lock`);
345
+ const entries = [];
346
+ for (const candidate of EXPORTED_PATHS) {
347
+ const relativePath = candidate.endsWith("/") ? candidate.slice(0, -1) : candidate;
348
+ collect(root, relativePath, entries, lock);
349
+ }
350
+ // Sorted, so two exports of one unchanged store are the same archive: a
351
+ // tenant comparing two downloads is comparing the store and not the order a
352
+ // directory happened to be read in.
353
+ entries.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
354
+ return {
355
+ bytes: gzipSync(writeTar(entries), { level: 6 }),
356
+ paths: entries.map((entry) => entry.path),
357
+ };
358
+ }
359
+ /** `relative()` in POSIX spelling, for callers that hold absolute paths. */
360
+ export function storeRelative(root, path) {
361
+ return relative(root, path).split(sep).join("/");
362
+ }
363
+ //# sourceMappingURL=archive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.js","sourceRoot":"","sources":["../../../src/serve/archive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * What a caller on this transport may say: nothing about the filesystem
3
+ * outside the store, and nothing shaped like a flag where a value belongs
4
+ * (APRV-421, review findings 3 and 1).
5
+ *
6
+ * ## The two holes this closes
7
+ *
8
+ * **A published path flag is a filesystem oracle.** `approval mcp serve`
9
+ * publishes `--log`, `--dir` and `--policy` on the verbs that accept them and
10
+ * injects the operator's pins only where the operator passed any. On a pipe
11
+ * the operator handed the server that is harmless: the party at the other end
12
+ * is the party who started it. Over HTTP the party at the other end is a
13
+ * sandboxed harness on another machine, and the same flag answers questions
14
+ * about files it cannot see.
15
+ *
16
+ * **A positional is a flag if it starts with a dash.** `buildArgv` emits
17
+ * positionals FIRST and verbatim, and `parseFlags` reads any token beginning
18
+ * with `-` as a flag. So a caller refused `{"flags":{"--payload":"…"}}` could
19
+ * write `{"positionals":["--payload","/tmp/secret","t1"]}` and reach the same
20
+ * flag through the front door — including the `--payload=/tmp/secret`
21
+ * spelling, which `parseFlags` splits on `=`. Positionals are VALUES. One that
22
+ * looks like a flag is refused rather than escaped, because escaping is a
23
+ * thing to get subtly wrong once and lose.
24
+ *
25
+ * `trailing` needs no such rule and deliberately does not get one. `buildArgv`
26
+ * always emits `--` before it, and `parseFlags` STOPS at `--`, pushing the
27
+ * remainder into positionals without reading a single token as a flag. A
28
+ * blanket refusal of dash-leading trailing elements would instead break the
29
+ * one agent verb that needs them: `hook classify -- git push -f` classifies a
30
+ * command line, and command lines have flags. The invariant that makes this
31
+ * safe is pinned by a test rather than assumed here.
32
+ *
33
+ * ## The rule, by scope
34
+ *
35
+ * **Both scopes.** No positional may begin with `-`. `--dir`, `--log` and
36
+ * `--policy` are appended to every call from the launch configuration and are
37
+ * refused from a caller even when the value is correct: one process serves one
38
+ * store, and a caller restating it is a caller that believes it can choose.
39
+ *
40
+ * **Agent scope.** Every path-typed flag is refused OUTRIGHT rather than
41
+ * confined: a sandboxed harness has no files on the daemon's machine, so a
42
+ * path it names is either useless or somebody else's. Payload bytes reach the
43
+ * gate for such a harness through the hook route, which builds the envelope
44
+ * from the tool call itself. On top of that, only the flags named in
45
+ * {@link AGENT_FLAGS} are accepted at all, per verb, so a flag added to one of
46
+ * the seven agent verbs tomorrow is refused until somebody decides otherwise.
47
+ *
48
+ * **Tenant scope.** Every path-typed flag is CONFINED to the store, and which
49
+ * flags those are comes from the registry ({@link pathFlagsOf}) rather than
50
+ * from a list kept here, so a flag declared `"path"` tomorrow is confined on
51
+ * the day it appears. Positionals that name files are confined the same way.
52
+ */
53
+ import { type VerbSpec } from "../cli/verb-registry.js";
54
+ import type { ServeScope } from "./credentials.js";
55
+ /**
56
+ * The three the server pins itself, refused from any caller in any scope.
57
+ *
58
+ * They ARE the store. Everything else this module decides is about values
59
+ * inside it.
60
+ */
61
+ export declare const PINNED_FLAGS: readonly string[];
62
+ /**
63
+ * What each AGENT verb may be given, flag by flag.
64
+ *
65
+ * A positive list per verb, and short on purpose. The agent surface is seven
66
+ * verbs; between them they need an action key, a decision deadline, a note,
67
+ * a proposal's class, key, summary and inline payload, and `--json`. Everything else on those verbs is either the store (pinned), the
68
+ * identity (`--as`, refused by the argv builder both transports share), a path
69
+ * (refused below), or something nobody has decided a sandboxed harness should
70
+ * have.
71
+ *
72
+ * `--help` is absent deliberately: a help page over a transport whose client
73
+ * is a program is an odd thing to serve, and `instructions` is the verb for
74
+ * that.
75
+ */
76
+ export declare const AGENT_FLAGS: ReadonlyMap<string, ReadonlySet<string>>;
77
+ export type ArgumentCheck = {
78
+ ok: true;
79
+ } | {
80
+ ok: false;
81
+ code: string;
82
+ message: string;
83
+ };
84
+ /**
85
+ * Check one verb call's arguments before an argv is built from them.
86
+ *
87
+ * Runs for EVERY scope. The tenant credential is the operator's and is trusted
88
+ * with far more than the agent's, and that is still not a reason to let one
89
+ * process serve two stores: the pins are the launch configuration's, and a
90
+ * second store reached through an argument would be a second gate with no
91
+ * attestation, no daemon id, and no record of which one answered.
92
+ */
93
+ export declare function checkVerbArguments(spec: VerbSpec, rawArgs: unknown, storeRoot: string, cwd: string, scope: ServeScope): ArgumentCheck;