approval-md 0.2.0 → 0.4.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 (378) hide show
  1. package/README.md +85 -877
  2. package/SPEC.md +150 -19
  3. package/cli.js +66 -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 +53 -1
  8. package/dist/src/channels/contract.js +304 -7
  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.js +6 -1
  17. package/dist/src/channels/tagging.js.map +1 -1
  18. package/dist/src/channels/telegram-webhook.d.ts +273 -0
  19. package/dist/src/channels/telegram-webhook.js +536 -0
  20. package/dist/src/channels/telegram-webhook.js.map +1 -0
  21. package/dist/src/channels/telegram.d.ts +394 -12
  22. package/dist/src/channels/telegram.js +621 -68
  23. package/dist/src/channels/telegram.js.map +1 -1
  24. package/dist/src/channels/web.d.ts +9 -0
  25. package/dist/src/channels/web.js +17 -0
  26. package/dist/src/channels/web.js.map +1 -1
  27. package/dist/src/cli/amend.js +564 -34
  28. package/dist/src/cli/amend.js.map +1 -1
  29. package/dist/src/cli/attest.d.ts +9 -0
  30. package/dist/src/cli/attest.js +177 -13
  31. package/dist/src/cli/attest.js.map +1 -1
  32. package/dist/src/cli/channel-relay.d.ts +108 -0
  33. package/dist/src/cli/channel-relay.js +334 -0
  34. package/dist/src/cli/channel-relay.js.map +1 -0
  35. package/dist/src/cli/channel-telegram-webhook.d.ts +273 -0
  36. package/dist/src/cli/channel-telegram-webhook.js +709 -0
  37. package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
  38. package/dist/src/cli/channel-telegram.d.ts +347 -28
  39. package/dist/src/cli/channel-telegram.js +763 -28
  40. package/dist/src/cli/channel-telegram.js.map +1 -1
  41. package/dist/src/cli/channel.d.ts +10 -1
  42. package/dist/src/cli/channel.js +14 -1
  43. package/dist/src/cli/channel.js.map +1 -1
  44. package/dist/src/cli/codex-bridge.d.ts +819 -0
  45. package/dist/src/cli/codex-bridge.js +1607 -0
  46. package/dist/src/cli/codex-bridge.js.map +1 -0
  47. package/dist/src/cli/codex.d.ts +1 -1
  48. package/dist/src/cli/codex.js +304 -7
  49. package/dist/src/cli/codex.js.map +1 -1
  50. package/dist/src/cli/daemon.js +41 -6
  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 +1322 -122
  54. package/dist/src/cli/doctor.js.map +1 -1
  55. package/dist/src/cli/execute.d.ts +17 -1
  56. package/dist/src/cli/execute.js +366 -22
  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 +14 -2
  65. package/dist/src/cli/help.js +395 -82
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook-codex.d.ts +49 -1
  68. package/dist/src/cli/hook-codex.js +60 -1
  69. package/dist/src/cli/hook-codex.js.map +1 -1
  70. package/dist/src/cli/hook.d.ts +828 -4
  71. package/dist/src/cli/hook.js +2972 -142
  72. package/dist/src/cli/hook.js.map +1 -1
  73. package/dist/src/cli/import.js +1 -1
  74. package/dist/src/cli/import.js.map +1 -1
  75. package/dist/src/cli/init.js +16 -7
  76. package/dist/src/cli/init.js.map +1 -1
  77. package/dist/src/cli/instructions.js +24 -3
  78. package/dist/src/cli/instructions.js.map +1 -1
  79. package/dist/src/cli/log-anchor.d.ts +55 -0
  80. package/dist/src/cli/log-anchor.js +199 -60
  81. package/dist/src/cli/log-anchor.js.map +1 -1
  82. package/dist/src/cli/main.d.ts +0 -12
  83. package/dist/src/cli/main.js +56 -3
  84. package/dist/src/cli/main.js.map +1 -1
  85. package/dist/src/cli/muse.d.ts +2 -0
  86. package/dist/src/cli/muse.js +62 -0
  87. package/dist/src/cli/muse.js.map +1 -0
  88. package/dist/src/cli/payload.js +79 -1
  89. package/dist/src/cli/payload.js.map +1 -1
  90. package/dist/src/cli/policy-apply.d.ts +195 -0
  91. package/dist/src/cli/policy-apply.js +573 -0
  92. package/dist/src/cli/policy-apply.js.map +1 -0
  93. package/dist/src/cli/policy.js +14 -1
  94. package/dist/src/cli/policy.js.map +1 -1
  95. package/dist/src/cli/preflight.d.ts +151 -13
  96. package/dist/src/cli/preflight.js +398 -41
  97. package/dist/src/cli/preflight.js.map +1 -1
  98. package/dist/src/cli/sandbox.js +17 -1
  99. package/dist/src/cli/sandbox.js.map +1 -1
  100. package/dist/src/cli/scaffold.d.ts +1 -1
  101. package/dist/src/cli/scaffold.js +7 -1
  102. package/dist/src/cli/scaffold.js.map +1 -1
  103. package/dist/src/cli/serve.d.ts +53 -0
  104. package/dist/src/cli/serve.js +270 -0
  105. package/dist/src/cli/serve.js.map +1 -0
  106. package/dist/src/cli/setup-channel.d.ts +9 -0
  107. package/dist/src/cli/setup-channel.js +28 -1
  108. package/dist/src/cli/setup-channel.js.map +1 -1
  109. package/dist/src/cli/setup-common.d.ts +3 -1
  110. package/dist/src/cli/setup-common.js +3 -2
  111. package/dist/src/cli/setup-common.js.map +1 -1
  112. package/dist/src/cli/setup.d.ts +2 -0
  113. package/dist/src/cli/setup.js +94 -2
  114. package/dist/src/cli/setup.js.map +1 -1
  115. package/dist/src/cli/up.js +134 -51
  116. package/dist/src/cli/up.js.map +1 -1
  117. package/dist/src/cli/values.js +3 -4
  118. package/dist/src/cli/values.js.map +1 -1
  119. package/dist/src/cli/verb-registry.d.ts +2 -0
  120. package/dist/src/cli/verb-registry.js +510 -47
  121. package/dist/src/cli/verb-registry.js.map +1 -1
  122. package/dist/src/cli/wordmark.d.ts +2 -2
  123. package/dist/src/cli/wordmark.js +2 -2
  124. package/dist/src/codex/broker.d.ts +229 -0
  125. package/dist/src/codex/broker.js +548 -0
  126. package/dist/src/codex/broker.js.map +1 -0
  127. package/dist/src/codex/runner.d.ts +178 -0
  128. package/dist/src/codex/runner.js +231 -0
  129. package/dist/src/codex/runner.js.map +1 -0
  130. package/dist/src/codex/serve.d.ts +56 -0
  131. package/dist/src/codex/serve.js +98 -0
  132. package/dist/src/codex/serve.js.map +1 -0
  133. package/dist/src/codex/workspace-commit.d.ts +219 -0
  134. package/dist/src/codex/workspace-commit.js +549 -0
  135. package/dist/src/codex/workspace-commit.js.map +1 -0
  136. package/dist/src/core/advance-cycle.d.ts +51 -0
  137. package/dist/src/core/advance-cycle.js +66 -2
  138. package/dist/src/core/advance-cycle.js.map +1 -1
  139. package/dist/src/core/agents-md.d.ts +20 -18
  140. package/dist/src/core/agents-md.js +33 -31
  141. package/dist/src/core/agents-md.js.map +1 -1
  142. package/dist/src/core/attest-bootstrap.d.ts +95 -0
  143. package/dist/src/core/attest-bootstrap.js +135 -0
  144. package/dist/src/core/attest-bootstrap.js.map +1 -0
  145. package/dist/src/core/attest.d.ts +234 -2
  146. package/dist/src/core/attest.js +342 -7
  147. package/dist/src/core/attest.js.map +1 -1
  148. package/dist/src/core/audit.d.ts +73 -3
  149. package/dist/src/core/audit.js +65 -5
  150. package/dist/src/core/audit.js.map +1 -1
  151. package/dist/src/core/channel-lease.d.ts +322 -0
  152. package/dist/src/core/channel-lease.js +663 -0
  153. package/dist/src/core/channel-lease.js.map +1 -0
  154. package/dist/src/core/channel-owner.d.ts +213 -0
  155. package/dist/src/core/channel-owner.js +358 -0
  156. package/dist/src/core/channel-owner.js.map +1 -0
  157. package/dist/src/core/command-class.d.ts +205 -0
  158. package/dist/src/core/command-class.js +1433 -28
  159. package/dist/src/core/command-class.js.map +1 -1
  160. package/dist/src/core/commit-guard.d.ts +272 -0
  161. package/dist/src/core/commit-guard.js +424 -0
  162. package/dist/src/core/commit-guard.js.map +1 -0
  163. package/dist/src/core/daemon-actor.d.ts +45 -0
  164. package/dist/src/core/daemon-actor.js +54 -0
  165. package/dist/src/core/daemon-actor.js.map +1 -0
  166. package/dist/src/core/daemon-host.d.ts +178 -0
  167. package/dist/src/core/daemon-host.js +228 -0
  168. package/dist/src/core/daemon-host.js.map +1 -0
  169. package/dist/src/core/daemon-identity.d.ts +238 -0
  170. package/dist/src/core/daemon-identity.js +252 -0
  171. package/dist/src/core/daemon-identity.js.map +1 -0
  172. package/dist/src/core/dark-session.d.ts +109 -8
  173. package/dist/src/core/dark-session.js +266 -82
  174. package/dist/src/core/dark-session.js.map +1 -1
  175. package/dist/src/core/decision-refusal.d.ts +23 -2
  176. package/dist/src/core/decision-refusal.js +24 -2
  177. package/dist/src/core/decision-refusal.js.map +1 -1
  178. package/dist/src/core/env-file.d.ts +5 -0
  179. package/dist/src/core/env-file.js +60 -1
  180. package/dist/src/core/env-file.js.map +1 -1
  181. package/dist/src/core/execute.d.ts +15 -2
  182. package/dist/src/core/execute.js +15 -2
  183. package/dist/src/core/execute.js.map +1 -1
  184. package/dist/src/core/gate.d.ts +392 -6
  185. package/dist/src/core/gate.js +852 -24
  186. package/dist/src/core/gate.js.map +1 -1
  187. package/dist/src/core/gesture-refusal.d.ts +166 -0
  188. package/dist/src/core/gesture-refusal.js +188 -0
  189. package/dist/src/core/gesture-refusal.js.map +1 -0
  190. package/dist/src/core/harness-version.d.ts +128 -4
  191. package/dist/src/core/harness-version.js +181 -12
  192. package/dist/src/core/harness-version.js.map +1 -1
  193. package/dist/src/core/harness-wait.d.ts +82 -3
  194. package/dist/src/core/harness-wait.js +89 -3
  195. package/dist/src/core/harness-wait.js.map +1 -1
  196. package/dist/src/core/instance.d.ts +59 -2
  197. package/dist/src/core/instance.js +113 -0
  198. package/dist/src/core/instance.js.map +1 -1
  199. package/dist/src/core/log-subscribe.d.ts +21 -0
  200. package/dist/src/core/log-subscribe.js +16 -6
  201. package/dist/src/core/log-subscribe.js.map +1 -1
  202. package/dist/src/core/log-write-layer.d.ts +55 -0
  203. package/dist/src/core/log-write-layer.js +91 -0
  204. package/dist/src/core/log-write-layer.js.map +1 -0
  205. package/dist/src/core/log.d.ts +89 -22
  206. package/dist/src/core/log.js +197 -19
  207. package/dist/src/core/log.js.map +1 -1
  208. package/dist/src/core/payload-census.d.ts +51 -0
  209. package/dist/src/core/payload-census.js +125 -2
  210. package/dist/src/core/payload-census.js.map +1 -1
  211. package/dist/src/core/payload-store.d.ts +31 -0
  212. package/dist/src/core/payload-store.js +113 -8
  213. package/dist/src/core/payload-store.js.map +1 -1
  214. package/dist/src/core/payload.d.ts +18 -3
  215. package/dist/src/core/payload.js +20 -4
  216. package/dist/src/core/payload.js.map +1 -1
  217. package/dist/src/core/policy-diff.d.ts +10 -0
  218. package/dist/src/core/policy-diff.js +10 -2
  219. package/dist/src/core/policy-diff.js.map +1 -1
  220. package/dist/src/core/policy-explain.d.ts +17 -1
  221. package/dist/src/core/policy-explain.js +48 -1
  222. package/dist/src/core/policy-explain.js.map +1 -1
  223. package/dist/src/core/policy-load.d.ts +65 -1
  224. package/dist/src/core/policy-load.js +21 -3
  225. package/dist/src/core/policy-load.js.map +1 -1
  226. package/dist/src/core/policy-match.d.ts +81 -0
  227. package/dist/src/core/policy-match.js +87 -0
  228. package/dist/src/core/policy-match.js.map +1 -1
  229. package/dist/src/core/policy-proposal.d.ts +75 -0
  230. package/dist/src/core/policy-proposal.js +110 -4
  231. package/dist/src/core/policy-proposal.js.map +1 -1
  232. package/dist/src/core/protected-path-guard.d.ts +117 -4
  233. package/dist/src/core/protected-path-guard.js +362 -48
  234. package/dist/src/core/protected-path-guard.js.map +1 -1
  235. package/dist/src/core/question-preempted.d.ts +141 -0
  236. package/dist/src/core/question-preempted.js +152 -0
  237. package/dist/src/core/question-preempted.js.map +1 -0
  238. package/dist/src/core/read-scope.d.ts +182 -0
  239. package/dist/src/core/read-scope.js +256 -0
  240. package/dist/src/core/read-scope.js.map +1 -0
  241. package/dist/src/core/run-payload.d.ts +160 -0
  242. package/dist/src/core/run-payload.js +304 -0
  243. package/dist/src/core/run-payload.js.map +1 -0
  244. package/dist/src/core/sandbox.d.ts +81 -0
  245. package/dist/src/core/sandbox.js +190 -1
  246. package/dist/src/core/sandbox.js.map +1 -1
  247. package/dist/src/core/sender-identity.d.ts +501 -0
  248. package/dist/src/core/sender-identity.js +609 -0
  249. package/dist/src/core/sender-identity.js.map +1 -0
  250. package/dist/src/core/shlex.d.ts +102 -0
  251. package/dist/src/core/shlex.js +159 -0
  252. package/dist/src/core/shlex.js.map +1 -0
  253. package/dist/src/core/state.d.ts +52 -1
  254. package/dist/src/core/state.js +52 -6
  255. package/dist/src/core/state.js.map +1 -1
  256. package/dist/src/core/telegram-config.d.ts +79 -0
  257. package/dist/src/core/telegram-config.js +108 -0
  258. package/dist/src/core/telegram-config.js.map +1 -1
  259. package/dist/src/core/values.d.ts +18 -8
  260. package/dist/src/core/values.js +36 -1
  261. package/dist/src/core/values.js.map +1 -1
  262. package/dist/src/core/verify.d.ts +23 -1
  263. package/dist/src/core/verify.js +20 -2
  264. package/dist/src/core/verify.js.map +1 -1
  265. package/dist/src/daemon/advance.d.ts +10 -0
  266. package/dist/src/daemon/advance.js +25 -4
  267. package/dist/src/daemon/advance.js.map +1 -1
  268. package/dist/src/daemon/audit.d.ts +80 -5
  269. package/dist/src/daemon/audit.js +126 -6
  270. package/dist/src/daemon/audit.js.map +1 -1
  271. package/dist/src/daemon/daemon.d.ts +174 -2
  272. package/dist/src/daemon/daemon.js +307 -7
  273. package/dist/src/daemon/daemon.js.map +1 -1
  274. package/dist/src/daemon/git-evidence.d.ts +2 -2
  275. package/dist/src/daemon/git-evidence.js +1 -1
  276. package/dist/src/daemon/projection.d.ts +13 -2
  277. package/dist/src/daemon/projection.js +39 -6
  278. package/dist/src/daemon/projection.js.map +1 -1
  279. package/dist/src/mcp/server.d.ts +39 -3
  280. package/dist/src/mcp/server.js +30 -3
  281. package/dist/src/mcp/server.js.map +1 -1
  282. package/dist/src/muse/server.d.ts +16 -0
  283. package/dist/src/muse/server.js +238 -0
  284. package/dist/src/muse/server.js.map +1 -0
  285. package/dist/src/serve/archive.d.ts +158 -0
  286. package/dist/src/serve/archive.js +354 -0
  287. package/dist/src/serve/archive.js.map +1 -0
  288. package/dist/src/serve/arguments.d.ts +93 -0
  289. package/dist/src/serve/arguments.js +228 -0
  290. package/dist/src/serve/arguments.js.map +1 -0
  291. package/dist/src/serve/credentials.d.ts +91 -0
  292. package/dist/src/serve/credentials.js +143 -0
  293. package/dist/src/serve/credentials.js.map +1 -0
  294. package/dist/src/serve/follow.d.ts +86 -0
  295. package/dist/src/serve/follow.js +101 -0
  296. package/dist/src/serve/follow.js.map +1 -0
  297. package/dist/src/serve/hook-thread.d.ts +134 -0
  298. package/dist/src/serve/hook-thread.js +333 -0
  299. package/dist/src/serve/hook-thread.js.map +1 -0
  300. package/dist/src/serve/hook-worker.d.ts +134 -0
  301. package/dist/src/serve/hook-worker.js +173 -0
  302. package/dist/src/serve/hook-worker.js.map +1 -0
  303. package/dist/src/serve/server.d.ts +412 -0
  304. package/dist/src/serve/server.js +1251 -0
  305. package/dist/src/serve/server.js.map +1 -0
  306. package/docs/README-extended.md +766 -0
  307. package/docs/cli-reference.md +2563 -73
  308. package/docs/codex-enforced-session.md +75 -2
  309. package/docs/codex-workspace-broker.md +118 -0
  310. package/package.json +6 -2
  311. package/schema/event.schema.json +595 -9
  312. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
  313. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
  314. package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
  315. package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
  316. package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
  317. package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
  318. package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
  319. package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
  320. package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
  321. package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
  322. package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
  323. package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
  324. package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
  325. package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
  326. package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
  327. package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
  328. package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
  329. package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
  330. package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
  331. package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
  332. package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
  333. package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
  334. package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
  335. package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
  336. package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
  337. package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
  338. package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
  339. package/schema/fixtures/event/valid/daemon-identity.json +15 -0
  340. package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
  341. package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
  342. package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
  343. package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
  344. package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
  345. package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
  346. package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
  347. package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
  348. package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
  349. package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
  350. package/schema/fixtures/policy/valid/canonical.json +1 -1
  351. package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
  352. package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
  353. package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
  354. package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
  355. package/schema/fixtures/policy-md/valid/canonical.md +1 -1
  356. package/schema/fixtures/policy-md/valid/with-values.md +5 -7
  357. package/schema/fixtures/values/invalid/class-shaped.json +1 -1
  358. package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
  359. package/schema/fixtures/values/invalid/non-string-item.json +1 -1
  360. package/schema/fixtures/values/invalid/over-cap.json +1 -1
  361. package/schema/fixtures/values/invalid/unknown-key.json +1 -1
  362. package/schema/fixtures/values/invalid/version-float.json +1 -0
  363. package/schema/fixtures/values/invalid/version-integer.json +1 -0
  364. package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
  365. package/schema/fixtures/values/valid/empty-lists.json +2 -3
  366. package/schema/fixtures/values/valid/full.json +5 -7
  367. package/schema/fixtures/values/valid/minimal.json +1 -1
  368. package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
  369. package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
  370. package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
  371. package/schema/fixtures/values-md/invalid/version-1.md +69 -0
  372. package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
  373. package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
  374. package/schema/fixtures/values-md/valid/absent.md +1 -1
  375. package/schema/fixtures/values-md/valid/with-values.md +5 -7
  376. package/schema/policy.schema.json +100 -4
  377. package/schema/values.schema.json +7 -11
  378. package/schema/fixtures/values/invalid/version-string.json +0 -1
@@ -0,0 +1,304 @@
1
+ /**
2
+ * The `approval run` payload: the argv, the cwd, and the bytes of the script
3
+ * the argv names (SPEC.md §6.2, §10.4; APRV-401).
4
+ *
5
+ * ## The hazard this closes
6
+ *
7
+ * §6.2 defined this payload as "the argv array and cwd", and `approval run`
8
+ * recomputes it from the argv and cwd it is about to spawn. For a command that
9
+ * IS its own bytes (`npm update @types/node`) that is the whole action. For a
10
+ * command that names a script it is not: the payload binds a NAME, and the
11
+ * bytes behind that name are whatever the path holds at execution time. The
12
+ * requester controls the file between the request and the grant, so a human
13
+ * approved a path and the runtime executed a file. Found on a live grant during
14
+ * APRV-398, exploited by nobody, filed as APRV-401.
15
+ *
16
+ * The payload hash is supposed to BE the binding (§10.4: "A grant therefore
17
+ * approves specific bytes"), so the repair is to put the bytes inside it. A
18
+ * script edited between the declaration and the execution hashes differently,
19
+ * and the difference refuses `payload-mismatch` before the child is spawned and
20
+ * before anything is appended, exactly as a changed argv already did.
21
+ *
22
+ * ## The shape, as the operator ruled it (APRV-401 notes, 2026-09-21)
23
+ *
24
+ * > the script digest is PART OF THE BINDING, never advisory. Shape: when argv
25
+ * > names an interpreter (bash, sh, zsh, node, python3 and kin) followed by a
26
+ * > path that resolves to a regular file at request time, the runtime reads and
27
+ * > hashes the file into the bound payload, the card shows the digest, byte
28
+ * > count and path, and approval run re-reads the file and refuses
29
+ * > payload-mismatch if the bytes changed. No interpretation of the script and
30
+ * > no second pass for files it sources; the limit is documented. Out of scope:
31
+ * > piped installers (already opaque), sourced files, interpreters the
32
+ * > classifier does not name.
33
+ *
34
+ * So this is an INTERPRETER-KEYED rule and not "every argv word that happens to
35
+ * name a file". One script per invocation: the first word after the interpreter
36
+ * that resolves to a readable regular file, which is the interpreter's operand
37
+ * whether or not a subcommand comes first (`deno run job.ts`).
38
+ * {@link SCRIPT_INTERPRETERS} is the list, and every member of it is a name
39
+ * `core/command-class.ts` already knows: the six shells of its unwrappable set,
40
+ * `node` from its own branch, and `python`, `python3`, `perl`, `ruby` and
41
+ * `deno` from its inline-source table. A launcher neither file names binds
42
+ * nothing, which is the documented limit rather than an oversight.
43
+ *
44
+ * The one thing here that the ruling does not spell out is the DEGENERATE case:
45
+ * `./install.sh` with no interpreter word at all, where the kernel reads the
46
+ * shebang and the script is `argv[0]` itself. It is bound, because it is the
47
+ * same fact with the interpreter implied rather than typed, and leaving it out
48
+ * would be a hole in the rule shaped exactly like the rule.
49
+ *
50
+ * ## What is NOT bound, stated plainly
51
+ *
52
+ * - **An inline program.** `bash -c '…'`, `node -e '…'`, `python3 -c '…'`: the
53
+ * program is a word of the argv, so it was always bound, and there is no file
54
+ * to read. The inline flags per interpreter are in {@link SCRIPT_INTERPRETERS}.
55
+ * - **Nothing resolved through PATH.** The interpreter itself is not hashed. Its
56
+ * bytes are the host's rather than the requester's, and resolving a program
57
+ * name through `PATH` would make the payload a function of the environment the
58
+ * process happens to hold, which §11 says is never read implicitly.
59
+ * - **Nothing transitive.** A bound script that sources another file, curls a
60
+ * second one, or execs a third binds only its own bytes. The digest is a
61
+ * binding, not a sandbox.
62
+ * - **A piped installer.** `curl … | sh` has no file operand and is opaque to
63
+ * the classifier for the same reason it is unbindable here.
64
+ * - **The instant between hashing and spawning.** The digest is taken, then
65
+ * `execution.started` is appended, then the child is spawned. A file rewritten
66
+ * inside that window runs unbound. Closing it needs the executor to hold the
67
+ * bytes rather than the path, which is the adapter contract of §10.4.
68
+ *
69
+ * ## Absence is not a claim
70
+ *
71
+ * `script` is omitted when nothing is bound, and that omission is deliberate: an
72
+ * invocation naming no readable script hashes to exactly the value it hashed
73
+ * before this module existed, so every record already in the log, and every
74
+ * declaration already written into a task file, verifies unchanged. What the
75
+ * absence does NOT say is "this runtime checked and there was nothing" — it is
76
+ * the ordinary shape of a payload with no script to bind, and a reader wanting
77
+ * to know whether a digest was possible reads the argv, which is right there.
78
+ *
79
+ * Every skip is safe for one reason, and it is the same reason in each case: the
80
+ * value is recomputed at execution by this same rule. A file that was
81
+ * unreadable at declaration time and readable at execution time gains an entry
82
+ * and refuses; one that existed and was deleted loses one and refuses. The rule
83
+ * does not have to decide correctly what a command will do, because both ends
84
+ * apply it to the same tree and any disagreement is a refusal.
85
+ *
86
+ * Reads the filesystem, by design and by necessity: at most one file per call,
87
+ * and only the one the argv names. Writes nothing, appends nothing, reads no
88
+ * log, no policy, no environment and no clock.
89
+ */
90
+ import { createHash } from "node:crypto";
91
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
92
+ import { isAbsolute, resolve as resolvePath } from "node:path";
93
+ /** The payload key carrying the bound script. Omitted when there is none. */
94
+ export const BOUND_SCRIPT_KEY = "script";
95
+ /**
96
+ * How much of a file is read at once while digesting it.
97
+ *
98
+ * Chunked rather than `readFileSync`, because a script may be of any size and
99
+ * the executor must not be the thing that runs out of memory: a refusal to
100
+ * compute the binding is a refusal to execute.
101
+ */
102
+ export const DIGEST_CHUNK_BYTES = 64 * 1024;
103
+ /**
104
+ * The interpreters whose script operand is bound, and the flags that mean
105
+ * "the program is inline, there is no file".
106
+ *
107
+ * Every name here is one `core/command-class.ts` already knows, which is the
108
+ * ruling's "interpreters the classifier does not name" read as a positive list:
109
+ * the six shells of its unwrappable set, `node` from its own classification
110
+ * branch, and `python`, `python3`, `perl`, `ruby`, `deno` from its
111
+ * inline-source table. The list is duplicated rather than imported because
112
+ * those tables are private to that module and a public export of them would be
113
+ * a second consumer of a decision that belongs to classification; the comment
114
+ * is the seam, and a name added there without being added here binds nothing,
115
+ * which fails toward the old behaviour rather than toward a wrong digest.
116
+ *
117
+ * The shell inline flag is `-c` with any run of `l` and `i` before it, which is
118
+ * `INLINE_SCRIPT_FLAG` in that module, spelled here as the same regular
119
+ * expression for the same reason.
120
+ */
121
+ export const SCRIPT_INTERPRETERS = new Map([
122
+ ["bash", [/^-[li]*c$/u]],
123
+ ["sh", [/^-[li]*c$/u]],
124
+ ["zsh", [/^-[li]*c$/u]],
125
+ ["dash", [/^-[li]*c$/u]],
126
+ ["ksh", [/^-[li]*c$/u]],
127
+ ["fish", [/^-[li]*c$/u, "--command"]],
128
+ ["node", ["-e", "--eval", "-p", "--print", "--input-type=module"]],
129
+ ["nodejs", ["-e", "--eval", "-p", "--print"]],
130
+ ["python", ["-c"]],
131
+ ["python3", ["-c"]],
132
+ ["perl", ["-e", "-E"]],
133
+ ["ruby", ["-e"]],
134
+ ["deno", ["eval"]],
135
+ ]);
136
+ /**
137
+ * The digest and size of one file, read in bounded chunks, or `null` when it
138
+ * cannot be read at all.
139
+ *
140
+ * `null` for every failure — gone, a directory, a device, permission denied, a
141
+ * disappearing file, a read error partway — and the reason is not recorded
142
+ * anywhere. Recording it would put a local errno inside hashed material, so two
143
+ * machines that agree about the bytes could disagree about the payload; and the
144
+ * only thing a reader could do with it is treat "unreadable" as a state, which
145
+ * the recomputation already handles better than any field could.
146
+ */
147
+ function fileDigest(path) {
148
+ let fd;
149
+ try {
150
+ const stats = statSync(path);
151
+ if (!stats.isFile())
152
+ return null;
153
+ fd = openSync(path, "r");
154
+ }
155
+ catch {
156
+ return null;
157
+ }
158
+ const hash = createHash("sha256");
159
+ const buffer = Buffer.allocUnsafe(DIGEST_CHUNK_BYTES);
160
+ let bytes = 0;
161
+ try {
162
+ let read = readSync(fd, buffer, 0, DIGEST_CHUNK_BYTES, null);
163
+ while (read > 0) {
164
+ hash.update(buffer.subarray(0, read));
165
+ bytes += read;
166
+ read = readSync(fd, buffer, 0, DIGEST_CHUNK_BYTES, null);
167
+ }
168
+ }
169
+ catch {
170
+ return null;
171
+ }
172
+ finally {
173
+ try {
174
+ closeSync(fd);
175
+ }
176
+ catch {
177
+ // A close that fails has nothing to do with the bytes already hashed.
178
+ }
179
+ }
180
+ return { bytes, sha256: hash.digest("hex") };
181
+ }
182
+ /** The last path segment of an argv word: `/usr/bin/bash` reads as `bash`. */
183
+ function basenameOf(word) {
184
+ const segments = word.split("/");
185
+ return segments[segments.length - 1] ?? word;
186
+ }
187
+ /** Does `word` mean "the program is inline" for this interpreter? */
188
+ function isInlineProgramFlag(word, markers) {
189
+ return markers.some((marker) => typeof marker === "string" ? word === marker : marker.test(word));
190
+ }
191
+ /**
192
+ * The words of an interpreter invocation that could be its script, in order,
193
+ * or `null` when the program is inline and there is no file at all.
194
+ *
195
+ * An inline-program marker anywhere in the options answers `null` rather than
196
+ * "keep looking": the program is a word of the argv and already bound, and a
197
+ * file that happens to share its spelling is a file the command never opens.
198
+ *
199
+ * Otherwise every non-option word is a candidate, in argv order, and the caller
200
+ * binds the first that resolves to a readable regular file. Not just the first
201
+ * non-option word, because interpreters take SUBCOMMANDS: `deno run job.ts`
202
+ * would otherwise offer `run` and stop, and `python3 -m pkg job.py` would offer
203
+ * `pkg`. Walking on cannot bind anything the argv does not name, and the worst
204
+ * case is the harmless direction — a command whose script is missing and whose
205
+ * later argument happens to be a file binds that file, which is one more thing
206
+ * bound rather than one fewer.
207
+ *
208
+ * An option that takes a separate path value is the one place this can pick the
209
+ * wrong file (`node --require ./a.js job.js` binds `./a.js`), and both are
210
+ * files that invocation executes.
211
+ */
212
+ function scriptCandidateIndexes(argv, markers) {
213
+ const candidates = [];
214
+ for (let index = 1; index < argv.length; index += 1) {
215
+ const word = argv[index];
216
+ if (isInlineProgramFlag(word, markers))
217
+ return null;
218
+ if (word === "--")
219
+ continue;
220
+ if (word.startsWith("-") && word.length > 1)
221
+ continue;
222
+ candidates.push(index);
223
+ }
224
+ return candidates;
225
+ }
226
+ /**
227
+ * Where an argv word points, resolved against the payload's own `cwd`.
228
+ *
229
+ * The cwd is the payload's, never `process.cwd()`: the payload already carries
230
+ * the directory the child will run in, and resolving against anything else
231
+ * would make the binding a function of where the hashing process happened to
232
+ * stand.
233
+ */
234
+ function resolveArgvPath(word, cwd) {
235
+ if (word === "" || word === "-" || word === "--")
236
+ return null;
237
+ if (word.includes("\0"))
238
+ return null;
239
+ return isAbsolute(word) ? word : resolvePath(cwd, word);
240
+ }
241
+ /**
242
+ * The script this argv runs, bound by its bytes, or `null` (APRV-401).
243
+ *
244
+ * Two shapes, and only two: an interpreter this module names followed by a path
245
+ * operand, and a path at `argv[0]` that the kernel will read a shebang from.
246
+ * Anything else binds nothing and the payload is what it always was.
247
+ */
248
+ export function boundScript(argv, cwd) {
249
+ const program = argv[0];
250
+ if (program === undefined || program === "")
251
+ return null;
252
+ const markers = SCRIPT_INTERPRETERS.get(basenameOf(program));
253
+ if (markers !== undefined) {
254
+ const candidates = scriptCandidateIndexes(argv, markers);
255
+ if (candidates === null)
256
+ return null;
257
+ for (const index of candidates) {
258
+ const path = resolveArgvPath(argv[index], cwd);
259
+ if (path === null)
260
+ continue;
261
+ const digest = fileDigest(path);
262
+ if (digest !== null)
263
+ return { argv_index: index, path, ...digest };
264
+ }
265
+ return null;
266
+ }
267
+ // The degenerate case: no interpreter word, so the script IS argv[0] and the
268
+ // interpreter is whatever its shebang names. A program with no separator in
269
+ // it is a PATH lookup and is not this (see the module header).
270
+ if (!isAbsolute(program) && !program.includes("/"))
271
+ return null;
272
+ const path = resolveArgvPath(program, cwd);
273
+ if (path === null)
274
+ return null;
275
+ const digest = fileDigest(path);
276
+ return digest === null ? null : { argv_index: 0, path, ...digest };
277
+ }
278
+ /**
279
+ * The value `approval run`'s `payload_hash` is taken over (SPEC.md §6.2).
280
+ *
281
+ * `{argv, cwd}` when the argv names no readable script, byte for byte what it
282
+ * has always been; `{argv, cwd, script}` when it does. The caller canonicalizes
283
+ * and hashes it — this function decides WHAT is bound and never how it is
284
+ * digested into an approval.
285
+ */
286
+ export function runPayloadValue(argv, cwd) {
287
+ const script = boundScript(argv, cwd);
288
+ const payload = { argv: [...argv], cwd };
289
+ if (script !== null)
290
+ payload.script = script;
291
+ return payload;
292
+ }
293
+ /**
294
+ * One line naming the bound script, for a refusal message or a terminal.
295
+ *
296
+ * Not a rendering an approver signs: the canonical renderer owns that, and this
297
+ * is the executor explaining to an agent WHICH file moved.
298
+ */
299
+ export function describeBoundScript(script) {
300
+ if (script === null)
301
+ return "no script";
302
+ return `argv[${String(script.argv_index)}] ${script.path}, ${String(script.bytes)} bytes, sha256 ${script.sha256}`;
303
+ }
304
+ //# sourceMappingURL=run-payload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-payload.js","sourceRoot":"","sources":["../../../src/core/run-payload.ts"],"names":[],"mappings":"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"}
@@ -153,9 +153,90 @@ export interface EgressAllowance {
153
153
  * reach the profile. Directories deny their whole subtree.
154
154
  */
155
155
  readonly denyRead: readonly string[];
156
+ /**
157
+ * WRITE CONFINEMENT (APRV-325.3): the only absolute subtrees the child may
158
+ * write, or `undefined` for the ordinary egress-only profile.
159
+ *
160
+ * This flips the posture for one rule family and one only. The header explains
161
+ * why the rest of this module is a deny-LIST: a `(deny default)` profile
162
+ * spends itself re-allowing dyld, the process's own binary and every temporary
163
+ * directory, and a control that breaks ordinary development is a control that
164
+ * gets switched off. That reasoning holds for reads and for everything else,
165
+ * and it does NOT hold for writes in a confined session, where the whole
166
+ * property being enforced is "this shell cannot change the canonical
167
+ * workspace". A write deny-list would have to enumerate every path worth
168
+ * protecting; this names the handful worth writing, so a path nobody thought
169
+ * of is denied rather than forgotten.
170
+ *
171
+ * `/dev` is allowed alongside them, unconditionally. Writes there are process
172
+ * I/O rather than filesystem state — `/dev/null`, `/dev/stdout`, `/dev/tty`,
173
+ * the pty a shell needs — and denying them kills the child before it can
174
+ * demonstrate anything, which is the failure mode point 1 of the header
175
+ * records for `network-outbound` and unix sockets.
176
+ *
177
+ * An EMPTY array is meaningful and is not the same as `undefined`: it denies
178
+ * every write outside `/dev`. `undefined` emits no write rules at all.
179
+ */
180
+ readonly writeAllow?: readonly string[];
181
+ /**
182
+ * The READ JAIL (APRV-347): absolute directories the child may read, with
183
+ * everything else denied.
184
+ *
185
+ * Absent or empty leaves the profile exactly as it was — allow-default with a
186
+ * deny-list — and the bytes are identical to the pre-APRV-347 profile, which
187
+ * a fixture test pins. That default is deliberate: a deny-default read
188
+ * profile is a much larger change to what ordinary development can do, and a
189
+ * control that breaks `npm run build` is a control someone switches off.
190
+ *
191
+ * When it IS set, the profile flips to `(deny file-read*)` plus a `subpath`
192
+ * allow for each of these roots and for the fixed runtime set a process needs
193
+ * to exist at all (see {@link RUNTIME_READ_PATHS}). `denyRead` still applies
194
+ * and is emitted AFTER the allows, so a credential file inside an allowed
195
+ * root stays unreadable: last match wins in SBPL, and the credential denial
196
+ * must be the last word.
197
+ */
198
+ readonly allowRead?: readonly string[];
156
199
  }
157
200
  /** The default: nothing allowed, nothing denied beyond the network. */
158
201
  export declare const DENY_ALL_EGRESS: EgressAllowance;
202
+ /**
203
+ * What a process must be able to read to exist, regardless of the jail.
204
+ *
205
+ * Deliberately a fixed compiled-in list rather than anything a caller supplies:
206
+ * a jail whose runtime set could be widened by the party under oversight is not
207
+ * a jail (SPEC.md §11.1, self-reported fields never reduce scrutiny).
208
+ *
209
+ * - `/usr/lib`, `/usr/share`, `/System`, `/Library` — dyld, the shared cache,
210
+ * ICU data and the frameworks every Mach-O binary links. A profile without
211
+ * them kills the process before `main`, which reads as the command failing
212
+ * rather than as the sandbox working.
213
+ * - `/private/var/db/dyld` and `/var/db/dyld` — the dyld shared cache, which
214
+ * moved out of `/usr/lib` and is opened by name.
215
+ * - `/dev` — `/dev/null`, `/dev/urandom`, `/dev/dtracehelper`, the tty.
216
+ * - `/bin`, `/usr/bin`, `/sbin`, `/usr/sbin`, `/opt/homebrew`, `/usr/local` —
217
+ * the interpreters and tools a build shells out to. The node binary's own
218
+ * realpath is added separately by {@link runtimeReadRoots}, because a Node
219
+ * installed by a version manager lives under the user's home and none of
220
+ * these covers it.
221
+ * - `/etc`, `/private/etc` — `resolv.conf`, `passwd`, the locale tables.
222
+ *
223
+ * `node_modules` is NOT here, and that is a stated limit rather than an
224
+ * oversight: a repository's dependencies live inside the repository, so they
225
+ * are covered by the gate root being a read root. A project whose dependencies
226
+ * sit OUTSIDE the root (a pnpm store elsewhere, a linked package) must name
227
+ * that directory in `read_scope.roots`, which is exactly the widening that key
228
+ * exists for. docs/sandboxed-exec.md says so beside the survey.
229
+ *
230
+ * **The temp roots are not here either**, and that one is load-bearing. They
231
+ * reach the profile through the CALLER — `resolveReadRoots` puts the session
232
+ * scratchpad and the system temp root in the effective read scope, so
233
+ * `approval run` and `approval sandbox` open them and build tooling keeps
234
+ * working — and a caller that passes narrower roots gets a narrower jail. A
235
+ * temp root compiled in here would have been a widening no operator could turn
236
+ * off and no test could demonstrate a denial against, which is how it was
237
+ * found.
238
+ */
239
+ export declare const RUNTIME_READ_PATHS: readonly string[];
159
240
  /**
160
241
  * The files beside a log that hold credential material, for the profile's
161
242
  * `denyRead`.
@@ -125,6 +125,77 @@ export const SANDBOX_STATES = [
125
125
  ];
126
126
  /** The default: nothing allowed, nothing denied beyond the network. */
127
127
  export const DENY_ALL_EGRESS = { loopback: false, denyRead: [] };
128
+ /**
129
+ * What a process must be able to read to exist, regardless of the jail.
130
+ *
131
+ * Deliberately a fixed compiled-in list rather than anything a caller supplies:
132
+ * a jail whose runtime set could be widened by the party under oversight is not
133
+ * a jail (SPEC.md §11.1, self-reported fields never reduce scrutiny).
134
+ *
135
+ * - `/usr/lib`, `/usr/share`, `/System`, `/Library` — dyld, the shared cache,
136
+ * ICU data and the frameworks every Mach-O binary links. A profile without
137
+ * them kills the process before `main`, which reads as the command failing
138
+ * rather than as the sandbox working.
139
+ * - `/private/var/db/dyld` and `/var/db/dyld` — the dyld shared cache, which
140
+ * moved out of `/usr/lib` and is opened by name.
141
+ * - `/dev` — `/dev/null`, `/dev/urandom`, `/dev/dtracehelper`, the tty.
142
+ * - `/bin`, `/usr/bin`, `/sbin`, `/usr/sbin`, `/opt/homebrew`, `/usr/local` —
143
+ * the interpreters and tools a build shells out to. The node binary's own
144
+ * realpath is added separately by {@link runtimeReadRoots}, because a Node
145
+ * installed by a version manager lives under the user's home and none of
146
+ * these covers it.
147
+ * - `/etc`, `/private/etc` — `resolv.conf`, `passwd`, the locale tables.
148
+ *
149
+ * `node_modules` is NOT here, and that is a stated limit rather than an
150
+ * oversight: a repository's dependencies live inside the repository, so they
151
+ * are covered by the gate root being a read root. A project whose dependencies
152
+ * sit OUTSIDE the root (a pnpm store elsewhere, a linked package) must name
153
+ * that directory in `read_scope.roots`, which is exactly the widening that key
154
+ * exists for. docs/sandboxed-exec.md says so beside the survey.
155
+ *
156
+ * **The temp roots are not here either**, and that one is load-bearing. They
157
+ * reach the profile through the CALLER — `resolveReadRoots` puts the session
158
+ * scratchpad and the system temp root in the effective read scope, so
159
+ * `approval run` and `approval sandbox` open them and build tooling keeps
160
+ * working — and a caller that passes narrower roots gets a narrower jail. A
161
+ * temp root compiled in here would have been a widening no operator could turn
162
+ * off and no test could demonstrate a denial against, which is how it was
163
+ * found.
164
+ */
165
+ export const RUNTIME_READ_PATHS = [
166
+ "/usr/lib",
167
+ "/usr/share",
168
+ "/System",
169
+ "/Library",
170
+ "/private/var/db/dyld",
171
+ "/var/db/dyld",
172
+ "/dev",
173
+ "/bin",
174
+ "/sbin",
175
+ "/usr/bin",
176
+ "/usr/sbin",
177
+ "/usr/local",
178
+ "/opt/homebrew",
179
+ "/etc",
180
+ "/private/etc",
181
+ ];
182
+ /**
183
+ * The runtime set plus this process's own interpreter.
184
+ *
185
+ * `process.execPath` is resolved because a version manager's `node` is usually
186
+ * a symlink, and a `subpath` rule matches what the KERNEL resolves rather than
187
+ * what the profile was handed (the lesson APRV-193's design lane paid for).
188
+ */
189
+ function runtimeReadRoots() {
190
+ const roots = [...RUNTIME_READ_PATHS];
191
+ try {
192
+ roots.push(dirname(realpathSync(process.execPath)));
193
+ }
194
+ catch {
195
+ // A process that cannot resolve its own binary is one the jail cannot help.
196
+ }
197
+ return roots;
198
+ }
128
199
  // ---------------------------------------------------------------------------
129
200
  // The credential material
130
201
  // ---------------------------------------------------------------------------
@@ -293,6 +364,39 @@ export function seatbeltProfile(allowance) {
293
364
  if (allowance.loopback) {
294
365
  lines.push(";; Carve-out: a suite that starts its own localhost server. A real widening.", '(allow network-outbound (remote ip "localhost:*"))');
295
366
  }
367
+ // APRV-347, the read jail. Emitted between the egress rules and the
368
+ // credential denials, and the ORDER is the policy: SBPL takes the last
369
+ // matching rule, so the roots open the filesystem and `denyRead` closes the
370
+ // credential material inside them afterwards. An empty (or absent)
371
+ // `allowRead` emits nothing at all, which is why a profile built without it
372
+ // is byte-identical to the profile this function produced before this task.
373
+ const allowRead = allowance.allowRead ?? [];
374
+ if (allowRead.length > 0) {
375
+ lines.push(";; APRV-347: the read jail. Deny-default, then the roots and the fixed", ";; runtime set a process needs to start at all.", "(deny file-read*)");
376
+ const seen = new Set();
377
+ for (const path of [...allowRead, ...runtimeReadRoots()]) {
378
+ const resolved = resolveForProfile(path);
379
+ if (seen.has(resolved))
380
+ continue;
381
+ seen.add(resolved);
382
+ lines.push(`(allow file-read* (subpath ${sbplString(resolved)}))`);
383
+ }
384
+ // The ROOT DIRECTORY needs `file-read*`, not metadata, and this was
385
+ // measured rather than reasoned: with every other directory a process needs
386
+ // opened by subpath and `/` allowed for metadata only, `/usr/bin/true`
387
+ // dies with SIGABRT before `main`. `/` alone, as a literal: it opens the
388
+ // top-level directory listing and no file in it.
389
+ lines.push('(allow file-read* (literal "/"))');
390
+ // `file-read-metadata` on every other ancestor of every allowed root:
391
+ // resolving a path means stat-ing each directory on the way down, and a
392
+ // jail that denied that would refuse `/Users/x/dev/muse` for lack of
393
+ // `/Users`. Metadata leaks a directory's existence, never its contents.
394
+ for (const ancestor of ancestorsOf([...seen])) {
395
+ if (ancestor === "/")
396
+ continue;
397
+ lines.push(`(allow file-read-metadata (literal ${sbplString(ancestor)}))`);
398
+ }
399
+ }
296
400
  for (const path of allowance.denyRead) {
297
401
  const resolved = resolveForProfile(path);
298
402
  // `literal` for a file, `subpath` for a directory. Both are emitted for a
@@ -312,8 +416,91 @@ export function seatbeltProfile(allowance) {
312
416
  if (!file)
313
417
  lines.push(`(deny file-read* (subpath ${sbplString(resolved)}))`);
314
418
  }
419
+ if (allowance.writeAllow !== undefined) {
420
+ lines.push(";; APRV-325.3: write confinement. Deny-DEFAULT for this rule family only,", ";; so a path nobody thought of is denied rather than forgotten.", "(deny file-write*)", ";; /dev is process I/O, not filesystem state. Denying it kills a shell", ";; before it can do anything, for the reason a bare network-outbound deny does.", '(allow file-write* (subpath "/dev"))');
421
+ for (const path of allowance.writeAllow) {
422
+ lines.push(`(allow file-write* (subpath ${sbplString(resolveForProfile(path))}))`);
423
+ }
424
+ }
315
425
  return `${lines.join("\n")}\n`;
316
426
  }
427
+ /**
428
+ * The read jail, plus the directories the command being run lives in (APRV-347).
429
+ *
430
+ * You cannot run a program you may not read. `sandbox-exec` execs the RESOLVED
431
+ * path, so a `node` or `npm` installed outside the compiled-in runtime set — a
432
+ * version manager under `~`, a `.local/bin` shim — fails `execvp` with
433
+ * `Operation not permitted` and exits 71, which reads as the command failing
434
+ * rather than as the jail working.
435
+ *
436
+ * Two directories are opened: the resolved executable's own, and its PARENT.
437
+ * The parent is what makes a toolchain work rather than merely start: an
438
+ * interpreter's entry point lives at `<prefix>/bin/<name>` and the library it
439
+ * immediately requires lives at `<prefix>/lib` or beside it, so opening only
440
+ * `bin` gets a process that launches and cannot find itself. `npm` resolves to
441
+ * `…/lib/node_modules/npm/bin/npm-cli.js`, whose parent is the npm package, and
442
+ * that is the shape this rule is for.
443
+ *
444
+ * It is NOT a widening a caller chooses: it is derived from the argv this
445
+ * function was already given, and it does nothing at all when no jail was
446
+ * asked for. Anything a command needs BEYOND its own install prefix — a
447
+ * dependency store elsewhere, a linked package — is the operator's
448
+ * `read_scope.roots` to declare.
449
+ */
450
+ function withExecutableRoots(allowance, command) {
451
+ const jail = allowance.allowRead ?? [];
452
+ if (jail.length === 0)
453
+ return allowance;
454
+ let real = command;
455
+ try {
456
+ real = realpathSync(command);
457
+ }
458
+ catch {
459
+ // Not resolvable: the caller already handles a command that is not there.
460
+ }
461
+ const own = dirname(real);
462
+ // `dirname(command)` as well as `dirname(real)`: a PATH entry is usually a
463
+ // directory of symlinks (`~/.local/bin/npm` -> a version manager's tree), and
464
+ // traversing the link needs the link's own directory open, not just the
465
+ // target's. Leaving it out fails `execvp` before the program starts.
466
+ const roots = [own, dirname(command)];
467
+ // The prefix, behind a DEPTH FLOOR. `dirname("/bin")` is `/`, and a root of
468
+ // `/` would open the entire filesystem — a jail that opened everything and
469
+ // reported nothing, which is the worst failure this file can have. Two
470
+ // segments, the same floor the hook applies to a scratch root and for the
471
+ // same reason: a value derived from somewhere else must not be able to name
472
+ // the root of the disk.
473
+ const prefix = dirname(own);
474
+ if (prefix.split("/").filter((segment) => segment.length > 0).length >= 2) {
475
+ roots.push(prefix);
476
+ }
477
+ return { ...allowance, allowRead: [...jail, ...roots] };
478
+ }
479
+ /**
480
+ * Every proper ancestor directory of these paths, sorted and de-duplicated.
481
+ *
482
+ * `subpath` opens a subtree; it says nothing about the directories ABOVE it,
483
+ * and opening `/Users/carter/dev/muse` is useless if the process may not stat
484
+ * `/Users`, `/Users/carter` and `/Users/carter/dev` on the way down. Metadata
485
+ * only: the names of the intermediate directories leak, their contents do not.
486
+ */
487
+ function ancestorsOf(paths) {
488
+ const out = new Set();
489
+ for (const path of paths) {
490
+ let at = dirname(path);
491
+ for (let depth = 0; depth < 64; depth += 1) {
492
+ if (at === "/" || at === "." || at === "")
493
+ break;
494
+ out.add(at);
495
+ const up = dirname(at);
496
+ if (up === at)
497
+ break;
498
+ at = up;
499
+ }
500
+ }
501
+ out.add("/");
502
+ return [...out].sort();
503
+ }
317
504
  /**
318
505
  * A path as an SBPL string literal.
319
506
  *
@@ -377,7 +564,9 @@ export function wrapForSandbox(mechanism, command, args, allowance = DENY_ALL_EG
377
564
  }
378
565
  const dir = mkdtempSync(join(realTmpdir(), "approval-sandbox-"));
379
566
  const profile = join(dir, "egress-denied.sb");
380
- writeFileSync(profile, seatbeltProfile(allowance), { mode: 0o600 });
567
+ writeFileSync(profile, seatbeltProfile(withExecutableRoots(allowance, command)), {
568
+ mode: 0o600,
569
+ });
381
570
  return {
382
571
  command: SANDBOX_EXEC,
383
572
  args: [...profileFlags(profile), command, ...args],