@lannguyensi/harness 0.63.0 → 1.0.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 (702) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +46 -42
  3. package/dist/cli/add/index.js +17 -21
  4. package/dist/cli/add/index.js.map +1 -1
  5. package/dist/cli/adopt/derive.d.ts +2 -2
  6. package/dist/cli/adopt/derive.js +2 -19
  7. package/dist/cli/adopt/derive.js.map +1 -1
  8. package/dist/cli/adopt/index.d.ts +1 -1
  9. package/dist/cli/adopt/index.js +1 -12
  10. package/dist/cli/adopt/index.js.map +1 -1
  11. package/dist/cli/apply/apply.d.ts +20 -35
  12. package/dist/cli/apply/apply.js +61 -136
  13. package/dist/cli/apply/apply.js.map +1 -1
  14. package/dist/cli/apply/generate-codex-config.js +10 -58
  15. package/dist/cli/apply/generate-codex-config.js.map +1 -1
  16. package/dist/cli/apply/generate-memory-index.js +1 -2
  17. package/dist/cli/apply/generate-memory-index.js.map +1 -1
  18. package/dist/cli/apply/generate-opencode-config.d.ts +2 -10
  19. package/dist/cli/apply/generate-opencode-config.js +74 -113
  20. package/dist/cli/apply/generate-opencode-config.js.map +1 -1
  21. package/dist/cli/apply/generate-settings.d.ts +11 -64
  22. package/dist/cli/apply/generate-settings.js +27 -132
  23. package/dist/cli/apply/generate-settings.js.map +1 -1
  24. package/dist/cli/apply/index.d.ts +1 -1
  25. package/dist/cli/apply/index.js +1 -1
  26. package/dist/cli/apply/index.js.map +1 -1
  27. package/dist/cli/apply/install-codex-config.d.ts +153 -1
  28. package/dist/cli/apply/install-codex-config.js +445 -70
  29. package/dist/cli/apply/install-codex-config.js.map +1 -1
  30. package/dist/cli/apply/next-steps.d.ts +7 -0
  31. package/dist/cli/apply/next-steps.js +13 -1
  32. package/dist/cli/apply/next-steps.js.map +1 -1
  33. package/dist/cli/bounded-stdin.d.ts +0 -23
  34. package/dist/cli/bounded-stdin.js +2 -49
  35. package/dist/cli/bounded-stdin.js.map +1 -1
  36. package/dist/cli/describe.d.ts +1 -1
  37. package/dist/cli/describe.js +0 -3
  38. package/dist/cli/describe.js.map +1 -1
  39. package/dist/cli/diff/engine.d.ts +0 -11
  40. package/dist/cli/diff/engine.js +1 -17
  41. package/dist/cli/diff/engine.js.map +1 -1
  42. package/dist/cli/diff/index.js +14 -25
  43. package/dist/cli/diff/index.js.map +1 -1
  44. package/dist/cli/doctor/claude-mcp.d.ts +4 -19
  45. package/dist/cli/doctor/claude-mcp.js +11 -22
  46. package/dist/cli/doctor/claude-mcp.js.map +1 -1
  47. package/dist/cli/doctor/codex.d.ts +12 -14
  48. package/dist/cli/doctor/codex.js +31 -144
  49. package/dist/cli/doctor/codex.js.map +1 -1
  50. package/dist/cli/doctor/format.js +23 -338
  51. package/dist/cli/doctor/format.js.map +1 -1
  52. package/dist/cli/doctor/hook-liveness.d.ts +56 -0
  53. package/dist/cli/doctor/hook-liveness.js +455 -0
  54. package/dist/cli/doctor/hook-liveness.js.map +1 -0
  55. package/dist/cli/doctor/index.d.ts +13 -87
  56. package/dist/cli/doctor/index.js +57 -705
  57. package/dist/cli/doctor/index.js.map +1 -1
  58. package/dist/cli/doctor/opencode.d.ts +2 -1
  59. package/dist/cli/doctor/opencode.js.map +1 -1
  60. package/dist/cli/doctor/types.d.ts +21 -378
  61. package/dist/cli/doctor/types.js.map +1 -1
  62. package/dist/cli/drain-stdio.d.ts +31 -0
  63. package/dist/cli/drain-stdio.js +76 -0
  64. package/dist/cli/drain-stdio.js.map +1 -0
  65. package/dist/cli/export.d.ts +3 -1
  66. package/dist/cli/export.js +7 -16
  67. package/dist/cli/export.js.map +1 -1
  68. package/dist/cli/gate/codex-disable.d.ts +29 -0
  69. package/dist/cli/gate/codex-disable.js +195 -0
  70. package/dist/cli/gate/codex-disable.js.map +1 -0
  71. package/dist/cli/gate/codex-enable.d.ts +41 -0
  72. package/dist/cli/gate/codex-enable.js +127 -0
  73. package/dist/cli/gate/codex-enable.js.map +1 -0
  74. package/dist/cli/gate/codex-snapshot.d.ts +208 -0
  75. package/dist/cli/gate/codex-snapshot.js +402 -0
  76. package/dist/cli/gate/codex-snapshot.js.map +1 -0
  77. package/dist/cli/gate/disable.d.ts +9 -0
  78. package/dist/cli/gate/disable.js +263 -28
  79. package/dist/cli/gate/disable.js.map +1 -1
  80. package/dist/cli/gate/enable.d.ts +26 -6
  81. package/dist/cli/gate/enable.js +152 -80
  82. package/dist/cli/gate/enable.js.map +1 -1
  83. package/dist/cli/gate/link-chain.d.ts +53 -0
  84. package/dist/cli/gate/link-chain.js +122 -0
  85. package/dist/cli/gate/link-chain.js.map +1 -0
  86. package/dist/cli/gate/snapshot.d.ts +289 -12
  87. package/dist/cli/gate/snapshot.js +481 -22
  88. package/dist/cli/gate/snapshot.js.map +1 -1
  89. package/dist/cli/index.d.ts +0 -9
  90. package/dist/cli/index.js +15 -2607
  91. package/dist/cli/index.js.map +1 -1
  92. package/dist/cli/init/composer.d.ts +3 -6
  93. package/dist/cli/init/composer.js +6 -370
  94. package/dist/cli/init/composer.js.map +1 -1
  95. package/dist/cli/init/dependencies.d.ts +1 -2
  96. package/dist/cli/init/dependencies.js +4 -80
  97. package/dist/cli/init/dependencies.js.map +1 -1
  98. package/dist/cli/init/detect.js +5 -4
  99. package/dist/cli/init/detect.js.map +1 -1
  100. package/dist/cli/init/interactive.d.ts +1 -23
  101. package/dist/cli/init/interactive.js +33 -247
  102. package/dist/cli/init/interactive.js.map +1 -1
  103. package/dist/cli/init/profiles.d.ts +2 -2
  104. package/dist/cli/init/profiles.js +51 -243
  105. package/dist/cli/init/profiles.js.map +1 -1
  106. package/dist/cli/init/templates.d.ts +2 -37
  107. package/dist/cli/init/templates.js +25 -1237
  108. package/dist/cli/init/templates.js.map +1 -1
  109. package/dist/cli/list.d.ts +1 -1
  110. package/dist/cli/list.js +1 -38
  111. package/dist/cli/list.js.map +1 -1
  112. package/dist/cli/loader.d.ts +15 -1
  113. package/dist/cli/loader.js +56 -39
  114. package/dist/cli/loader.js.map +1 -1
  115. package/dist/cli/main.js +7 -2
  116. package/dist/cli/main.js.map +1 -1
  117. package/dist/cli/migrate-home/index.js +2 -2
  118. package/dist/cli/migrate-home/index.js.map +1 -1
  119. package/dist/cli/pack/add.js +1 -1
  120. package/dist/cli/pack/add.js.map +1 -1
  121. package/dist/cli/pack/hook-bootstrap.d.ts +5 -164
  122. package/dist/cli/pack/hook-bootstrap.js +15 -291
  123. package/dist/cli/pack/hook-bootstrap.js.map +1 -1
  124. package/dist/cli/pack/hook-branch-protection.d.ts +28 -21
  125. package/dist/cli/pack/hook-branch-protection.js +365 -340
  126. package/dist/cli/pack/hook-branch-protection.js.map +1 -1
  127. package/dist/cli/pack/index.d.ts +0 -1
  128. package/dist/cli/pack/index.js +0 -1
  129. package/dist/cli/pack/index.js.map +1 -1
  130. package/dist/cli/pack/list.js +1 -6
  131. package/dist/cli/pack/list.js.map +1 -1
  132. package/dist/cli/pack/mutate.d.ts +7 -9
  133. package/dist/cli/pack/mutate.js +6 -9
  134. package/dist/cli/pack/mutate.js.map +1 -1
  135. package/dist/cli/pack/reseed.d.ts +2 -2
  136. package/dist/cli/pack/reseed.js +16 -22
  137. package/dist/cli/pack/reseed.js.map +1 -1
  138. package/dist/cli/register-gate-group.d.ts +5 -0
  139. package/dist/cli/register-gate-group.js +348 -0
  140. package/dist/cli/register-gate-group.js.map +1 -0
  141. package/dist/cli/register-inspect-group.d.ts +6 -0
  142. package/dist/cli/register-inspect-group.js +189 -0
  143. package/dist/cli/register-inspect-group.js.map +1 -0
  144. package/dist/cli/register-operator-lifecycle.d.ts +5 -0
  145. package/dist/cli/register-operator-lifecycle.js +20 -0
  146. package/dist/cli/register-operator-lifecycle.js.map +1 -0
  147. package/dist/cli/register-pack-group.d.ts +5 -0
  148. package/dist/cli/register-pack-group.js +134 -0
  149. package/dist/cli/register-pack-group.js.map +1 -0
  150. package/dist/cli/register-setup-group.d.ts +5 -0
  151. package/dist/cli/register-setup-group.js +495 -0
  152. package/dist/cli/register-setup-group.js.map +1 -0
  153. package/dist/cli/remove/index.d.ts +0 -14
  154. package/dist/cli/remove/index.js +2 -115
  155. package/dist/cli/remove/index.js.map +1 -1
  156. package/dist/cli/remove/mutate.d.ts +0 -2
  157. package/dist/cli/remove/mutate.js +1 -19
  158. package/dist/cli/remove/mutate.js.map +1 -1
  159. package/dist/cli/remove/register.d.ts +5 -0
  160. package/dist/cli/remove/register.js +25 -0
  161. package/dist/cli/remove/register.js.map +1 -0
  162. package/dist/cli/shell-quote.d.ts +7 -0
  163. package/dist/cli/shell-quote.js +17 -0
  164. package/dist/cli/shell-quote.js.map +1 -0
  165. package/dist/cli/uninstall/index.d.ts +4 -2
  166. package/dist/cli/uninstall/index.js +15 -7
  167. package/dist/cli/uninstall/index.js.map +1 -1
  168. package/dist/cli/uninstall/register.d.ts +5 -0
  169. package/dist/cli/uninstall/register.js +171 -0
  170. package/dist/cli/uninstall/register.js.map +1 -0
  171. package/dist/cli/uninstall/snapshot.d.ts +15 -0
  172. package/dist/cli/uninstall/snapshot.js +38 -0
  173. package/dist/cli/uninstall/snapshot.js.map +1 -1
  174. package/dist/cli/validate/checks.d.ts +0 -77
  175. package/dist/cli/validate/checks.js +3 -1049
  176. package/dist/cli/validate/checks.js.map +1 -1
  177. package/dist/cli/validate/index.js +19 -15
  178. package/dist/cli/validate/index.js.map +1 -1
  179. package/dist/index.d.ts +0 -3
  180. package/dist/index.js +0 -3
  181. package/dist/index.js.map +1 -1
  182. package/dist/io/claude-mcp.d.ts +1 -1
  183. package/dist/io/claude-mcp.js +27 -14
  184. package/dist/io/claude-mcp.js.map +1 -1
  185. package/dist/io/harness-lock.js +2 -2
  186. package/dist/io/harness-lock.js.map +1 -1
  187. package/dist/io/lock.d.ts +0 -26
  188. package/dist/io/lock.js +10 -27
  189. package/dist/io/lock.js.map +1 -1
  190. package/dist/io/merge-settings.js +6 -4
  191. package/dist/io/merge-settings.js.map +1 -1
  192. package/dist/io/project-name.d.ts +22 -39
  193. package/dist/io/project-name.js +25 -42
  194. package/dist/io/project-name.js.map +1 -1
  195. package/dist/io/read-regular-file.d.ts +103 -40
  196. package/dist/io/read-regular-file.js +196 -35
  197. package/dist/io/read-regular-file.js.map +1 -1
  198. package/dist/io/restart-hints.js +3 -6
  199. package/dist/io/restart-hints.js.map +1 -1
  200. package/dist/io/version-compare.d.ts +1 -3
  201. package/dist/io/version-compare.js +1 -3
  202. package/dist/io/version-compare.js.map +1 -1
  203. package/dist/io/write-regular-file.d.ts +30 -0
  204. package/dist/io/write-regular-file.js +85 -0
  205. package/dist/io/write-regular-file.js.map +1 -0
  206. package/dist/overrides/index.d.ts +1 -0
  207. package/dist/overrides/index.js +1 -0
  208. package/dist/overrides/index.js.map +1 -1
  209. package/dist/overrides/merge-layers.d.ts +43 -0
  210. package/dist/overrides/merge-layers.js +76 -0
  211. package/dist/overrides/merge-layers.js.map +1 -0
  212. package/dist/overrides/merge.d.ts +16 -0
  213. package/dist/overrides/merge.js +21 -8
  214. package/dist/overrides/merge.js.map +1 -1
  215. package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +4 -69
  216. package/dist/policy-packs/builtin/branch-protection-runtime.js +12 -84
  217. package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -1
  218. package/dist/policy-packs/builtin/branch-protection.d.ts +7 -30
  219. package/dist/policy-packs/builtin/branch-protection.js +56 -89
  220. package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
  221. package/dist/policy-packs/config-check.d.ts +7 -5
  222. package/dist/policy-packs/config-check.js +2 -2
  223. package/dist/policy-packs/config-check.js.map +1 -1
  224. package/dist/policy-packs/expand.d.ts +1 -3
  225. package/dist/policy-packs/expand.js +5 -42
  226. package/dist/policy-packs/expand.js.map +1 -1
  227. package/dist/policy-packs/index.d.ts +3 -3
  228. package/dist/policy-packs/index.js +2 -2
  229. package/dist/policy-packs/index.js.map +1 -1
  230. package/dist/policy-packs/registry.d.ts +9 -38
  231. package/dist/policy-packs/registry.js +5 -65
  232. package/dist/policy-packs/registry.js.map +1 -1
  233. package/dist/policy-packs/runtime.d.ts +0 -4
  234. package/dist/policy-packs/runtime.js +9 -22
  235. package/dist/policy-packs/runtime.js.map +1 -1
  236. package/dist/policy-packs/types.d.ts +0 -19
  237. package/dist/policy-packs/ux-compare.d.ts +1 -11
  238. package/dist/policy-packs/ux-compare.js +6 -42
  239. package/dist/policy-packs/ux-compare.js.map +1 -1
  240. package/dist/policy-packs/ux-drift-check.d.ts +5 -5
  241. package/dist/policy-packs/ux-drift-check.js +17 -22
  242. package/dist/policy-packs/ux-drift-check.js.map +1 -1
  243. package/dist/policy-packs/version-check.d.ts +5 -26
  244. package/dist/policy-packs/version-check.js +19 -74
  245. package/dist/policy-packs/version-check.js.map +1 -1
  246. package/dist/probes/memory.d.ts +1 -1
  247. package/dist/probes/memory.js +1 -1
  248. package/dist/probes/memory.js.map +1 -1
  249. package/dist/runtime/agent-facing.d.ts +9 -0
  250. package/dist/runtime/agent-facing.js +28 -14
  251. package/dist/runtime/agent-facing.js.map +1 -1
  252. package/dist/runtime/bounded-text.d.ts +15 -0
  253. package/dist/runtime/bounded-text.js +27 -0
  254. package/dist/runtime/bounded-text.js.map +1 -0
  255. package/dist/runtime/git-branch.d.ts +125 -0
  256. package/dist/runtime/git-branch.js +381 -0
  257. package/dist/runtime/git-branch.js.map +1 -0
  258. package/dist/runtime/hermetic-spawn-guard.d.ts +2 -2
  259. package/dist/runtime/hermetic-spawn-guard.js +24 -34
  260. package/dist/runtime/hermetic-spawn-guard.js.map +1 -1
  261. package/dist/runtime/home-dir.js +3 -2
  262. package/dist/runtime/home-dir.js.map +1 -1
  263. package/dist/runtime/tool-name-aliases.d.ts +0 -6
  264. package/dist/runtime/tool-name-aliases.js +5 -18
  265. package/dist/runtime/tool-name-aliases.js.map +1 -1
  266. package/dist/schema/grounding.d.ts +7 -54
  267. package/dist/schema/grounding.js +10 -12
  268. package/dist/schema/grounding.js.map +1 -1
  269. package/dist/schema/hooks.d.ts +39 -136
  270. package/dist/schema/hooks.js +3 -6
  271. package/dist/schema/hooks.js.map +1 -1
  272. package/dist/schema/index.d.ts +114 -3828
  273. package/dist/schema/index.js +41 -80
  274. package/dist/schema/index.js.map +1 -1
  275. package/dist/schema/memory.d.ts +54 -131
  276. package/dist/schema/memory.js +2 -2
  277. package/dist/schema/memory.js.map +1 -1
  278. package/dist/schema/policy-packs.d.ts +3 -45
  279. package/dist/schema/policy-packs.js +7 -13
  280. package/dist/schema/policy-packs.js.map +1 -1
  281. package/dist/schema/policy-ux.d.ts +7 -0
  282. package/dist/schema/policy-ux.js +18 -0
  283. package/dist/schema/policy-ux.js.map +1 -0
  284. package/dist/schema/removed-keys.d.ts +185 -0
  285. package/dist/schema/removed-keys.js +531 -0
  286. package/dist/schema/removed-keys.js.map +1 -0
  287. package/dist/schema/tools.d.ts +30 -268
  288. package/dist/schema/tools.js +4 -4
  289. package/dist/schema/tools.js.map +1 -1
  290. package/package.json +5 -16
  291. package/dist/cli/approve/branch-protection.d.ts +0 -69
  292. package/dist/cli/approve/branch-protection.js +0 -144
  293. package/dist/cli/approve/branch-protection.js.map +0 -1
  294. package/dist/cli/approve/risk.d.ts +0 -112
  295. package/dist/cli/approve/risk.js +0 -197
  296. package/dist/cli/approve/risk.js.map +0 -1
  297. package/dist/cli/approve/stdin-report.d.ts +0 -57
  298. package/dist/cli/approve/stdin-report.js +0 -145
  299. package/dist/cli/approve/stdin-report.js.map +0 -1
  300. package/dist/cli/approve/understanding.d.ts +0 -410
  301. package/dist/cli/approve/understanding.js +0 -996
  302. package/dist/cli/approve/understanding.js.map +0 -1
  303. package/dist/cli/audit.d.ts +0 -80
  304. package/dist/cli/audit.js +0 -252
  305. package/dist/cli/audit.js.map +0 -1
  306. package/dist/cli/delegate/index.d.ts +0 -76
  307. package/dist/cli/delegate/index.js +0 -443
  308. package/dist/cli/delegate/index.js.map +0 -1
  309. package/dist/cli/doctor/auto-approve-mode.d.ts +0 -37
  310. package/dist/cli/doctor/auto-approve-mode.js +0 -80
  311. package/dist/cli/doctor/auto-approve-mode.js.map +0 -1
  312. package/dist/cli/doctor/bypass-without-auto-approve.d.ts +0 -38
  313. package/dist/cli/doctor/bypass-without-auto-approve.js +0 -95
  314. package/dist/cli/doctor/bypass-without-auto-approve.js.map +0 -1
  315. package/dist/cli/doctor/codex-config-drift.d.ts +0 -28
  316. package/dist/cli/doctor/codex-config-drift.js +0 -240
  317. package/dist/cli/doctor/codex-config-drift.js.map +0 -1
  318. package/dist/cli/doctor/rogue-ledger.d.ts +0 -53
  319. package/dist/cli/doctor/rogue-ledger.js +0 -153
  320. package/dist/cli/doctor/rogue-ledger.js.map +0 -1
  321. package/dist/cli/doctor/session-start-preflight-setup-version.d.ts +0 -83
  322. package/dist/cli/doctor/session-start-preflight-setup-version.js +0 -114
  323. package/dist/cli/doctor/session-start-preflight-setup-version.js.map +0 -1
  324. package/dist/cli/doctor/settings-drift.d.ts +0 -25
  325. package/dist/cli/doctor/settings-drift.js +0 -213
  326. package/dist/cli/doctor/settings-drift.js.map +0 -1
  327. package/dist/cli/doctor/toolchain-parity.d.ts +0 -63
  328. package/dist/cli/doctor/toolchain-parity.js +0 -154
  329. package/dist/cli/doctor/toolchain-parity.js.map +0 -1
  330. package/dist/cli/doctor/ug-auto-approvals.d.ts +0 -40
  331. package/dist/cli/doctor/ug-auto-approvals.js +0 -91
  332. package/dist/cli/doctor/ug-auto-approvals.js.map +0 -1
  333. package/dist/cli/doctor/ug-delegations.d.ts +0 -26
  334. package/dist/cli/doctor/ug-delegations.js +0 -121
  335. package/dist/cli/doctor/ug-delegations.js.map +0 -1
  336. package/dist/cli/doctor/ug-inflight.d.ts +0 -35
  337. package/dist/cli/doctor/ug-inflight.js +0 -57
  338. package/dist/cli/doctor/ug-inflight.js.map +0 -1
  339. package/dist/cli/doctor/understanding-mode-env.d.ts +0 -51
  340. package/dist/cli/doctor/understanding-mode-env.js +0 -98
  341. package/dist/cli/doctor/understanding-mode-env.js.map +0 -1
  342. package/dist/cli/dry-run.d.ts +0 -46
  343. package/dist/cli/dry-run.js +0 -250
  344. package/dist/cli/dry-run.js.map +0 -1
  345. package/dist/cli/event-input.d.ts +0 -28
  346. package/dist/cli/event-input.js +0 -73
  347. package/dist/cli/event-input.js.map +0 -1
  348. package/dist/cli/explain-action.d.ts +0 -20
  349. package/dist/cli/explain-action.js +0 -27
  350. package/dist/cli/explain-action.js.map +0 -1
  351. package/dist/cli/explain-policy.d.ts +0 -135
  352. package/dist/cli/explain-policy.js +0 -266
  353. package/dist/cli/explain-policy.js.map +0 -1
  354. package/dist/cli/explain.d.ts +0 -31
  355. package/dist/cli/explain.js +0 -159
  356. package/dist/cli/explain.js.map +0 -1
  357. package/dist/cli/gc/index.d.ts +0 -63
  358. package/dist/cli/gc/index.js +0 -566
  359. package/dist/cli/gc/index.js.map +0 -1
  360. package/dist/cli/pack/approve-escape.d.ts +0 -19
  361. package/dist/cli/pack/approve-escape.js +0 -190
  362. package/dist/cli/pack/approve-escape.js.map +0 -1
  363. package/dist/cli/pack/auto-approve-path.d.ts +0 -146
  364. package/dist/cli/pack/auto-approve-path.js +0 -462
  365. package/dist/cli/pack/auto-approve-path.js.map +0 -1
  366. package/dist/cli/pack/hook-codex-post-tool-use.d.ts +0 -25
  367. package/dist/cli/pack/hook-codex-post-tool-use.js +0 -160
  368. package/dist/cli/pack/hook-codex-post-tool-use.js.map +0 -1
  369. package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +0 -48
  370. package/dist/cli/pack/hook-codex-pre-tool-use.js +0 -474
  371. package/dist/cli/pack/hook-codex-pre-tool-use.js.map +0 -1
  372. package/dist/cli/pack/hook-codex-stop.d.ts +0 -35
  373. package/dist/cli/pack/hook-codex-stop.js +0 -329
  374. package/dist/cli/pack/hook-codex-stop.js.map +0 -1
  375. package/dist/cli/pack/hook-codex-user-prompt-submit.d.ts +0 -24
  376. package/dist/cli/pack/hook-codex-user-prompt-submit.js +0 -93
  377. package/dist/cli/pack/hook-codex-user-prompt-submit.js.map +0 -1
  378. package/dist/cli/pack/hook-post-merge-gate-record.d.ts +0 -37
  379. package/dist/cli/pack/hook-post-merge-gate-record.js +0 -190
  380. package/dist/cli/pack/hook-post-merge-gate-record.js.map +0 -1
  381. package/dist/cli/pack/hook-post-merge-gate.d.ts +0 -34
  382. package/dist/cli/pack/hook-post-merge-gate.js +0 -277
  383. package/dist/cli/pack/hook-post-merge-gate.js.map +0 -1
  384. package/dist/cli/pack/hook-post-tool-use.d.ts +0 -45
  385. package/dist/cli/pack/hook-post-tool-use.js +0 -137
  386. package/dist/cli/pack/hook-post-tool-use.js.map +0 -1
  387. package/dist/cli/pack/hook-pre-tool-use.d.ts +0 -92
  388. package/dist/cli/pack/hook-pre-tool-use.js +0 -1328
  389. package/dist/cli/pack/hook-pre-tool-use.js.map +0 -1
  390. package/dist/cli/pack/hook-runtime-reality.d.ts +0 -63
  391. package/dist/cli/pack/hook-runtime-reality.js +0 -210
  392. package/dist/cli/pack/hook-runtime-reality.js.map +0 -1
  393. package/dist/cli/pack/hook-solution-acceptance-writeguard.d.ts +0 -32
  394. package/dist/cli/pack/hook-solution-acceptance-writeguard.js +0 -337
  395. package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +0 -1
  396. package/dist/cli/pack/hook-solution-acceptance.d.ts +0 -157
  397. package/dist/cli/pack/hook-solution-acceptance.js +0 -530
  398. package/dist/cli/pack/hook-solution-acceptance.js.map +0 -1
  399. package/dist/cli/pack/hook-stay-in-scope.d.ts +0 -37
  400. package/dist/cli/pack/hook-stay-in-scope.js +0 -128
  401. package/dist/cli/pack/hook-stay-in-scope.js.map +0 -1
  402. package/dist/cli/pack/hook-subagent-start.d.ts +0 -23
  403. package/dist/cli/pack/hook-subagent-start.js +0 -88
  404. package/dist/cli/pack/hook-subagent-start.js.map +0 -1
  405. package/dist/cli/pack/hook-subagent-stop.d.ts +0 -19
  406. package/dist/cli/pack/hook-subagent-stop.js +0 -41
  407. package/dist/cli/pack/hook-subagent-stop.js.map +0 -1
  408. package/dist/cli/pack/hook-track-active-claim.d.ts +0 -27
  409. package/dist/cli/pack/hook-track-active-claim.js +0 -275
  410. package/dist/cli/pack/hook-track-active-claim.js.map +0 -1
  411. package/dist/cli/pack/transcript-report-scan.d.ts +0 -71
  412. package/dist/cli/pack/transcript-report-scan.js +0 -359
  413. package/dist/cli/pack/transcript-report-scan.js.map +0 -1
  414. package/dist/cli/pack/understanding-report-schema-hint.d.ts +0 -26
  415. package/dist/cli/pack/understanding-report-schema-hint.js +0 -120
  416. package/dist/cli/pack/understanding-report-schema-hint.js.map +0 -1
  417. package/dist/cli/pack/upgrade.d.ts +0 -32
  418. package/dist/cli/pack/upgrade.js +0 -273
  419. package/dist/cli/pack/upgrade.js.map +0 -1
  420. package/dist/cli/pause/index.d.ts +0 -113
  421. package/dist/cli/pause/index.js +0 -251
  422. package/dist/cli/pause/index.js.map +0 -1
  423. package/dist/cli/pause-check.d.ts +0 -22
  424. package/dist/cli/pause-check.js +0 -54
  425. package/dist/cli/pause-check.js.map +0 -1
  426. package/dist/cli/policy/intercept.d.ts +0 -233
  427. package/dist/cli/policy/intercept.js +0 -1020
  428. package/dist/cli/policy/intercept.js.map +0 -1
  429. package/dist/cli/policy/risk-envelope-enrichment.d.ts +0 -40
  430. package/dist/cli/policy/risk-envelope-enrichment.js +0 -108
  431. package/dist/cli/policy/risk-envelope-enrichment.js.map +0 -1
  432. package/dist/cli/record/index.d.ts +0 -81
  433. package/dist/cli/record/index.js +0 -348
  434. package/dist/cli/record/index.js.map +0 -1
  435. package/dist/cli/resolve-env.d.ts +0 -32
  436. package/dist/cli/resolve-env.js +0 -47
  437. package/dist/cli/resolve-env.js.map +0 -1
  438. package/dist/cli/session-export/index.d.ts +0 -46
  439. package/dist/cli/session-export/index.js +0 -169
  440. package/dist/cli/session-export/index.js.map +0 -1
  441. package/dist/cli/session-export/redact.d.ts +0 -22
  442. package/dist/cli/session-export/redact.js +0 -47
  443. package/dist/cli/session-export/redact.js.map +0 -1
  444. package/dist/cli/session-export/transcript.d.ts +0 -24
  445. package/dist/cli/session-export/transcript.js +0 -164
  446. package/dist/cli/session-export/transcript.js.map +0 -1
  447. package/dist/cli/session-start/branch-check.d.ts +0 -18
  448. package/dist/cli/session-start/branch-check.js +0 -140
  449. package/dist/cli/session-start/branch-check.js.map +0 -1
  450. package/dist/cli/session-start/index.d.ts +0 -121
  451. package/dist/cli/session-start/index.js +0 -591
  452. package/dist/cli/session-start/index.js.map +0 -1
  453. package/dist/cli/session-start/shared-options.d.ts +0 -103
  454. package/dist/cli/session-start/shared-options.js +0 -107
  455. package/dist/cli/session-start/shared-options.js.map +0 -1
  456. package/dist/cli/session-start/stale-base-check.d.ts +0 -46
  457. package/dist/cli/session-start/stale-base-check.js +0 -352
  458. package/dist/cli/session-start/stale-base-check.js.map +0 -1
  459. package/dist/cli/session-start/toolchain-parity.d.ts +0 -267
  460. package/dist/cli/session-start/toolchain-parity.js +0 -797
  461. package/dist/cli/session-start/toolchain-parity.js.map +0 -1
  462. package/dist/cli/smoke/assertions.d.ts +0 -46
  463. package/dist/cli/smoke/assertions.js +0 -170
  464. package/dist/cli/smoke/assertions.js.map +0 -1
  465. package/dist/cli/smoke/index.d.ts +0 -76
  466. package/dist/cli/smoke/index.js +0 -245
  467. package/dist/cli/smoke/index.js.map +0 -1
  468. package/dist/cli/smoke/runner.d.ts +0 -55
  469. package/dist/cli/smoke/runner.js +0 -134
  470. package/dist/cli/smoke/runner.js.map +0 -1
  471. package/dist/cli/smoke/stream-parser.d.ts +0 -65
  472. package/dist/cli/smoke/stream-parser.js +0 -115
  473. package/dist/cli/smoke/stream-parser.js.map +0 -1
  474. package/dist/cli/test-risk.d.ts +0 -26
  475. package/dist/cli/test-risk.js +0 -34
  476. package/dist/cli/test-risk.js.map +0 -1
  477. package/dist/io/display-path.d.ts +0 -11
  478. package/dist/io/display-path.js +0 -29
  479. package/dist/io/display-path.js.map +0 -1
  480. package/dist/io/duration.d.ts +0 -5
  481. package/dist/io/duration.js +0 -50
  482. package/dist/io/duration.js.map +0 -1
  483. package/dist/io/extract.d.ts +0 -79
  484. package/dist/io/extract.js +0 -226
  485. package/dist/io/extract.js.map +0 -1
  486. package/dist/io/ledger-record.d.ts +0 -83
  487. package/dist/io/ledger-record.js +0 -301
  488. package/dist/io/ledger-record.js.map +0 -1
  489. package/dist/io/read-json-dir-entries.d.ts +0 -36
  490. package/dist/io/read-json-dir-entries.js +0 -48
  491. package/dist/io/read-json-dir-entries.js.map +0 -1
  492. package/dist/io/safe-json-parse.d.ts +0 -5
  493. package/dist/io/safe-json-parse.js +0 -21
  494. package/dist/io/safe-json-parse.js.map +0 -1
  495. package/dist/policies/index.d.ts +0 -5
  496. package/dist/policies/index.js +0 -6
  497. package/dist/policies/index.js.map +0 -1
  498. package/dist/policies/ledger-client.d.ts +0 -113
  499. package/dist/policies/ledger-client.js +0 -500
  500. package/dist/policies/ledger-client.js.map +0 -1
  501. package/dist/policies/producers.d.ts +0 -12
  502. package/dist/policies/producers.js +0 -61
  503. package/dist/policies/producers.js.map +0 -1
  504. package/dist/policies/requires.d.ts +0 -75
  505. package/dist/policies/requires.js +0 -240
  506. package/dist/policies/requires.js.map +0 -1
  507. package/dist/policies/timestamp.d.ts +0 -14
  508. package/dist/policies/timestamp.js +0 -36
  509. package/dist/policies/timestamp.js.map +0 -1
  510. package/dist/policy-packs/builtin/permission-profiles.d.ts +0 -11
  511. package/dist/policy-packs/builtin/permission-profiles.js +0 -74
  512. package/dist/policy-packs/builtin/permission-profiles.js.map +0 -1
  513. package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +0 -286
  514. package/dist/policy-packs/builtin/post-merge-gate-runtime.js +0 -397
  515. package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +0 -1
  516. package/dist/policy-packs/builtin/post-merge-gate.d.ts +0 -56
  517. package/dist/policy-packs/builtin/post-merge-gate.js +0 -347
  518. package/dist/policy-packs/builtin/post-merge-gate.js.map +0 -1
  519. package/dist/policy-packs/builtin/solution-acceptance-reconnect.d.ts +0 -50
  520. package/dist/policy-packs/builtin/solution-acceptance-reconnect.js +0 -151
  521. package/dist/policy-packs/builtin/solution-acceptance-reconnect.js.map +0 -1
  522. package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +0 -323
  523. package/dist/policy-packs/builtin/solution-acceptance-runtime.js +0 -672
  524. package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +0 -1
  525. package/dist/policy-packs/builtin/solution-acceptance.d.ts +0 -45
  526. package/dist/policy-packs/builtin/solution-acceptance.js +0 -226
  527. package/dist/policy-packs/builtin/solution-acceptance.js.map +0 -1
  528. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.d.ts +0 -25
  529. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js +0 -94
  530. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js.map +0 -1
  531. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.d.ts +0 -35
  532. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.js +0 -86
  533. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.js.map +0 -1
  534. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.d.ts +0 -210
  535. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js +0 -361
  536. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js.map +0 -1
  537. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.d.ts +0 -304
  538. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js +0 -734
  539. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js.map +0 -1
  540. package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.d.ts +0 -36
  541. package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.js +0 -73
  542. package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.js.map +0 -1
  543. package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +0 -19
  544. package/dist/policy-packs/builtin/understanding-before-execution/index.js +0 -83
  545. package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +0 -1
  546. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.d.ts +0 -135
  547. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js +0 -618
  548. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js.map +0 -1
  549. package/dist/policy-packs/builtin/understanding-before-execution/ledger.d.ts +0 -25
  550. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js +0 -56
  551. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js.map +0 -1
  552. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.d.ts +0 -20
  553. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js +0 -93
  554. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js.map +0 -1
  555. package/dist/policy-packs/builtin/understanding-before-execution/markers.d.ts +0 -126
  556. package/dist/policy-packs/builtin/understanding-before-execution/markers.js +0 -245
  557. package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +0 -1
  558. package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.d.ts +0 -29
  559. package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.js +0 -82
  560. package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.js.map +0 -1
  561. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.d.ts +0 -71
  562. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.js +0 -175
  563. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.js.map +0 -1
  564. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +0 -620
  565. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +0 -1047
  566. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +0 -1
  567. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.d.ts +0 -128
  568. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +0 -213
  569. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +0 -1
  570. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.d.ts +0 -129
  571. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.js +0 -166
  572. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.js.map +0 -1
  573. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.d.ts +0 -164
  574. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js +0 -275
  575. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js.map +0 -1
  576. package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +0 -1
  577. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +0 -10
  578. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +0 -1
  579. package/dist/policy-packs/builtin/understanding-before-execution.d.ts +0 -526
  580. package/dist/policy-packs/builtin/understanding-before-execution.js +0 -1074
  581. package/dist/policy-packs/builtin/understanding-before-execution.js.map +0 -1
  582. package/dist/policy-packs/permission-translator.d.ts +0 -9
  583. package/dist/policy-packs/permission-translator.js +0 -76
  584. package/dist/policy-packs/permission-translator.js.map +0 -1
  585. package/dist/runtime/action-envelope.d.ts +0 -64
  586. package/dist/runtime/action-envelope.js +0 -46
  587. package/dist/runtime/action-envelope.js.map +0 -1
  588. package/dist/runtime/approval-signing.d.ts +0 -111
  589. package/dist/runtime/approval-signing.js +0 -384
  590. package/dist/runtime/approval-signing.js.map +0 -1
  591. package/dist/runtime/bash-match-facts.d.ts +0 -60
  592. package/dist/runtime/bash-match-facts.js +0 -196
  593. package/dist/runtime/bash-match-facts.js.map +0 -1
  594. package/dist/runtime/bash-match-registry.d.ts +0 -112
  595. package/dist/runtime/bash-match-registry.js +0 -406
  596. package/dist/runtime/bash-match-registry.js.map +0 -1
  597. package/dist/runtime/bash-prefix-parse.d.ts +0 -31
  598. package/dist/runtime/bash-prefix-parse.js +0 -576
  599. package/dist/runtime/bash-prefix-parse.js.map +0 -1
  600. package/dist/runtime/command-normalize.d.ts +0 -658
  601. package/dist/runtime/command-normalize.js +0 -2410
  602. package/dist/runtime/command-normalize.js.map +0 -1
  603. package/dist/runtime/deletion-target-resolve.d.ts +0 -28
  604. package/dist/runtime/deletion-target-resolve.js +0 -1078
  605. package/dist/runtime/deletion-target-resolve.js.map +0 -1
  606. package/dist/runtime/destructive-shell-floor.d.ts +0 -19
  607. package/dist/runtime/destructive-shell-floor.js +0 -611
  608. package/dist/runtime/destructive-shell-floor.js.map +0 -1
  609. package/dist/runtime/environment-resolver.d.ts +0 -36
  610. package/dist/runtime/environment-resolver.js +0 -138
  611. package/dist/runtime/environment-resolver.js.map +0 -1
  612. package/dist/runtime/git-context.d.ts +0 -201
  613. package/dist/runtime/git-context.js +0 -422
  614. package/dist/runtime/git-context.js.map +0 -1
  615. package/dist/runtime/index.d.ts +0 -12
  616. package/dist/runtime/index.js +0 -13
  617. package/dist/runtime/index.js.map +0 -1
  618. package/dist/runtime/intercept.d.ts +0 -494
  619. package/dist/runtime/intercept.js +0 -1313
  620. package/dist/runtime/intercept.js.map +0 -1
  621. package/dist/runtime/kube-context.d.ts +0 -16
  622. package/dist/runtime/kube-context.js +0 -63
  623. package/dist/runtime/kube-context.js.map +0 -1
  624. package/dist/runtime/kubectl-target-parse.d.ts +0 -31
  625. package/dist/runtime/kubectl-target-parse.js +0 -249
  626. package/dist/runtime/kubectl-target-parse.js.map +0 -1
  627. package/dist/runtime/ledger-add.d.ts +0 -16
  628. package/dist/runtime/ledger-add.js +0 -150
  629. package/dist/runtime/ledger-add.js.map +0 -1
  630. package/dist/runtime/ledger-writer.d.ts +0 -27
  631. package/dist/runtime/ledger-writer.js +0 -46
  632. package/dist/runtime/ledger-writer.js.map +0 -1
  633. package/dist/runtime/pause-sentinel.d.ts +0 -60
  634. package/dist/runtime/pause-sentinel.js +0 -156
  635. package/dist/runtime/pause-sentinel.js.map +0 -1
  636. package/dist/runtime/pending-approval.d.ts +0 -23
  637. package/dist/runtime/pending-approval.js +0 -67
  638. package/dist/runtime/pending-approval.js.map +0 -1
  639. package/dist/runtime/read-only-bash.d.ts +0 -141
  640. package/dist/runtime/read-only-bash.js +0 -2204
  641. package/dist/runtime/read-only-bash.js.map +0 -1
  642. package/dist/runtime/recovery-git-commit.d.ts +0 -22
  643. package/dist/runtime/recovery-git-commit.js +0 -284
  644. package/dist/runtime/recovery-git-commit.js.map +0 -1
  645. package/dist/runtime/reject-malformed-session-id.d.ts +0 -8
  646. package/dist/runtime/reject-malformed-session-id.js +0 -16
  647. package/dist/runtime/reject-malformed-session-id.js.map +0 -1
  648. package/dist/runtime/risk-classifier.d.ts +0 -38
  649. package/dist/runtime/risk-classifier.js +0 -311
  650. package/dist/runtime/risk-classifier.js.map +0 -1
  651. package/dist/runtime/session-id.d.ts +0 -113
  652. package/dist/runtime/session-id.js +0 -193
  653. package/dist/runtime/session-id.js.map +0 -1
  654. package/dist/runtime/shell-word.d.ts +0 -62
  655. package/dist/runtime/shell-word.js +0 -410
  656. package/dist/runtime/shell-word.js.map +0 -1
  657. package/dist/runtime/task-providers/agent-tasks.d.ts +0 -103
  658. package/dist/runtime/task-providers/agent-tasks.js +0 -245
  659. package/dist/runtime/task-providers/agent-tasks.js.map +0 -1
  660. package/dist/runtime/when-eval.d.ts +0 -62
  661. package/dist/runtime/when-eval.js +0 -212
  662. package/dist/runtime/when-eval.js.map +0 -1
  663. package/dist/runtime/workflow-policies.d.ts +0 -207
  664. package/dist/runtime/workflow-policies.js +0 -596
  665. package/dist/runtime/workflow-policies.js.map +0 -1
  666. package/dist/schema/audit.d.ts +0 -71
  667. package/dist/schema/audit.js +0 -32
  668. package/dist/schema/audit.js.map +0 -1
  669. package/dist/schema/doctor.d.ts +0 -25
  670. package/dist/schema/doctor.js +0 -23
  671. package/dist/schema/doctor.js.map +0 -1
  672. package/dist/schema/environments.d.ts +0 -215
  673. package/dist/schema/environments.js +0 -101
  674. package/dist/schema/environments.js.map +0 -1
  675. package/dist/schema/extract.d.ts +0 -9
  676. package/dist/schema/extract.js +0 -86
  677. package/dist/schema/extract.js.map +0 -1
  678. package/dist/schema/permission-profiles.d.ts +0 -2292
  679. package/dist/schema/permission-profiles.js +0 -60
  680. package/dist/schema/permission-profiles.js.map +0 -1
  681. package/dist/schema/policies.d.ts +0 -974
  682. package/dist/schema/policies.js +0 -298
  683. package/dist/schema/policies.js.map +0 -1
  684. package/dist/schema/requires.d.ts +0 -131
  685. package/dist/schema/requires.js +0 -88
  686. package/dist/schema/requires.js.map +0 -1
  687. package/dist/schema/risk.d.ts +0 -144
  688. package/dist/schema/risk.js +0 -194
  689. package/dist/schema/risk.js.map +0 -1
  690. package/dist/schema/session-start-preflight.d.ts +0 -10
  691. package/dist/schema/session-start-preflight.js +0 -106
  692. package/dist/schema/session-start-preflight.js.map +0 -1
  693. package/dist/schema/stale-base-check.d.ts +0 -18
  694. package/dist/schema/stale-base-check.js +0 -37
  695. package/dist/schema/stale-base-check.js.map +0 -1
  696. package/dist/schema/toolchain-parity.d.ts +0 -21
  697. package/dist/schema/toolchain-parity.js +0 -48
  698. package/dist/schema/toolchain-parity.js.map +0 -1
  699. package/dist/schema/workflows.d.ts +0 -519
  700. package/dist/schema/workflows.js +0 -81
  701. package/dist/schema/workflows.js.map +0 -1
  702. package/scripts/runtime-reality-docker-probe.mjs +0 -71
@@ -1,1313 +0,0 @@
1
- // Phase 4 #5 — runtime hook interceptor + policy_decision audit log.
2
- //
3
- // Pure orchestration: takes a parsed event + a ledger client + the manifest,
4
- // runs every matching policy through the Phase 4 #1/#2/#3 pipeline, returns
5
- // the decisions and the Claude Code deny-JSON (or null when all allow).
6
- // Side effects (stdin, stdout, ledger I/O) live in the thin CLI entrypoint
7
- // that wraps this.
8
- import * as fs from "node:fs";
9
- import * as path from "node:path";
10
- import { evaluateExtract, evaluateRequires, firstInputMatchMismatch, parseDurationSeconds, substituteTemplate, } from "../policies/index.js";
11
- import { renderProducers } from "../policies/producers.js";
12
- import { buildActionEnvelope } from "./action-envelope.js";
13
- import { renderAgentFacing } from "./agent-facing.js";
14
- import { normalizeCommand, normalizeCommandAmpAware, normalizeCommandQuoteAware, segmentViewOf, } from "./command-normalize.js";
15
- import { resolveEnvironment, } from "./environment-resolver.js";
16
- import { resolveDeletionTarget, } from "./deletion-target-resolve.js";
17
- import { DEFAULT_SAFE_DELETION_ROOTS } from "../schema/risk.js";
18
- import { resolveGitContext } from "./git-context.js";
19
- import { POLICY_DECISION_TYPE } from "../io/ledger-record.js";
20
- import { classifyRisk } from "./risk-classifier.js";
21
- import { resolveSessionId } from "./session-id.js";
22
- import { expandToolNameAliases, extractShellCommand, } from "./tool-name-aliases.js";
23
- import { evaluateWhen, UNCLASSIFIED_FALLBACK_SEVERITY } from "./when-eval.js";
24
- /**
25
- * The sentence PREPENDED to a `ux:`-declared policy's `cannot:` text when
26
- * the deny was caused by the unclassified fallback rather than a real
27
- * classification (task 2929c5b7).
28
- *
29
- * Everything variable in it is interpolated: the RESOLVED environment
30
- * name and the policy's OWN declared threshold. Hard-coding "production"
31
- * and "critical" made both halves wrong for an unscoped
32
- * `severity_at_least: high` policy on a feature branch, which reported a
33
- * production context that had not been resolved and a critical-severity
34
- * comparison that never ran. The fallback rung itself comes from
35
- * `when-eval.ts`'s exported constant, not a second literal here.
36
- *
37
- * A policy whose `when:` block declares no `severity_at_least` can still
38
- * set the flag (via `risk.category_in` / `action.reversible`, which keep
39
- * the blanket "unknown is not safe" fallback), so the threshold-free
40
- * wording is a real case, not a defensive branch.
41
- */
42
- function unclassifiedFallbackPrefix(environmentName, threshold) {
43
- const env = environmentName ?? "unknown";
44
- const article = /^[aeiou]/i.test(env) ? "an" : "a";
45
- const lead = `This is an unclassified action in ${article} ${env} context: no risk classifier pattern recognized it, so`;
46
- return threshold === undefined
47
- ? `${lead} the fail-closed unclassified rule satisfied this policy's when: clause, rather than a genuine risk classification.`
48
- : `${lead} the fail-closed severity fallback (treated as ${UNCLASSIFIED_FALLBACK_SEVERITY}) satisfied this policy's severity_at_least: ${threshold}, rather than a genuine ${threshold}-severity match.`;
49
- }
50
- /**
51
- * Build the Action Envelope for an event and run it through the Risk
52
- * Classifier (#3) and Context Resolver (#4). Pure: every host fact
53
- * arrives via `riskContext`; when it is absent the envelope is built
54
- * from the event alone (unclassified risk, `unknown` environment).
55
- */
56
- function enrichEnvelope(manifest, event, riskContext, now) {
57
- const rc = riskContext;
58
- const envelope = buildActionEnvelope(event, {
59
- cwd: rc?.cwd ?? (typeof event.cwd === "string" ? event.cwd : ""),
60
- git: rc?.git ?? { repo: "", branch: "", sha: "" },
61
- user: rc?.user ?? "",
62
- host: rc?.host ?? "",
63
- now: now ?? new Date(),
64
- });
65
- const risk = classifyRisk(envelope, manifest.risk.classifiers);
66
- const environment = resolveEnvironment(envelope, manifest.environments.resolvers, {
67
- env: rc?.env ?? {},
68
- kubeContext: rc?.kubeContext ?? "",
69
- kubeNamespace: rc?.kubeNamespace ?? "",
70
- });
71
- // Static deletion-target resolution (task d03af8f6) needs only the raw
72
- // command — unlike the environment resolver above, it deliberately
73
- // does not consult `riskContext` (cwd, env, kube): a relative target is
74
- // UNRESOLVABLE by design, not resolved against ambient cwd. See
75
- // `deletion-target-resolve.ts`'s module doc for the full rationale.
76
- const deletionShellCommand = extractShellCommand({ raw_input: envelope.raw_input });
77
- const deletionTarget = deletionShellCommand === null
78
- ? null
79
- : resolveDeletionTarget(deletionShellCommand,
80
- // Defensive fallback (not just the schema `.default()`): a
81
- // hand-built `Manifest` test fixture that constructs `risk:`
82
- // directly, bypassing `RiskSchema.parse`, may omit this field
83
- // entirely — never trust it present.
84
- manifest.risk.safe_deletion_roots ?? DEFAULT_SAFE_DELETION_ROOTS);
85
- return { risk, environment, deletionTarget };
86
- }
87
- /**
88
- * Does a policy's `trigger:` match this event? This is the WHICH-tool-
89
- * calls filter; the WHETHER-it-applies filter is `policy.when:`,
90
- * evaluated separately (`evaluateWhen`). A policy fires only when both
91
- * hold. Exported so `harness explain-policy` can report the trigger
92
- * verdict on its own.
93
- *
94
- * `precomputedNormalizedCommand` is an optional caller-supplied
95
- * `NormalizedCommand` for the event's command: `intercept()` below
96
- * resolves ONE `NormalizedCommand` per event (via
97
- * `options.normalizedCommand`, itself computed once by
98
- * `runInterceptCli`) and threads it into every `policyMatchesEvent`
99
- * call in its `matching` loop, so a raw-miss event normalises the
100
- * command exactly ONCE across the whole manifest instead of once per
101
- * policy. Omitted by standalone callers (`harness explain-policy`, most
102
- * tests), which fall back to computing it lazily right here — correct
103
- * either way, since `normalizeCommand` is a pure function of the
104
- * command string alone.
105
- *
106
- * `ampNormalizedCommandThunk` (task aabbad63) is the SAME
107
- * resolved-by-the-wrapper pattern for the ampersand-aware SECOND
108
- * normalisation pass, but threaded as a memoised THUNK rather than a
109
- * precomputed value — see `InterceptOptions.ampNormalizedCommandThunk`
110
- * for why laziness matters here specifically. Omitted callers fall back
111
- * to calling `normalizeCommandAmpAware` directly, same shape as the
112
- * `precomputedNormalizedCommand` fallback above.
113
- *
114
- * `quoteNormalizedCommandThunk` (task cf3dff51) is the SAME thunk pattern
115
- * again, for the quote-aware THIRD normalisation pass — see
116
- * `InterceptOptions.quoteNormalizedCommandThunk`. Omitted callers fall
117
- * back to calling `normalizeCommandQuoteAware` directly, same shape as
118
- * the two fallbacks above.
119
- */
120
- export function policyMatchesEvent(policy, event, precomputedNormalizedCommand, ampNormalizedCommandThunk, quoteNormalizedCommandThunk) {
121
- if (policy.trigger.event !== event.hook_event_name)
122
- return false;
123
- if (policy.trigger.match !== undefined) {
124
- if (typeof event.tool_name !== "string")
125
- return false;
126
- const toolNames = expandToolNameAliases(event.tool_name);
127
- if (!toolNames.some((toolName) => toolName.includes(policy.trigger.match))) {
128
- return false;
129
- }
130
- }
131
- // `input_match` (task 2699b476): literal equality against the tool
132
- // call's own arguments, ANDed onto the tool-name match above. This is
133
- // what separates `task_finish { autoMerge: true }` (a merge, gated)
134
- // from a plain `task_finish` (not a merge, not gated) without needing
135
- // two different tool names. Evaluated from the SAME `toolArgs` context
136
- // `trigger.extract` reads (`buildEventContext`) for a SINGLE-envelope
137
- // event, and additionally against BOTH `tool_input` and `raw_input`
138
- // when a payload carries both as non-null objects (review round 1,
139
- // task 2699b476 round 2, see `inputMatchMismatchesEvent` below).
140
- // Mirrored in `policyMatchesTool` (`src/cli/dry-run.ts`) for the
141
- // single-envelope case only, since dry-run's `--input` is always one
142
- // object; `harness policy dry-run` cannot reproduce the mixed-envelope
143
- // arm. Both `input_match` parity and the mixed-envelope-is-intercept-only
144
- // caveat are recorded in docs/okf/debug-verb-selection.md's
145
- // trigger-matching parity paragraph.
146
- if (policy.trigger.input_match !== undefined) {
147
- if (inputMatchMismatchesEvent(policy.trigger.input_match, event)) {
148
- return false;
149
- }
150
- }
151
- if (policy.trigger.bash_match !== undefined) {
152
- const command = extractShellCommand(event);
153
- if (command === null)
154
- return false;
155
- let re;
156
- try {
157
- re = new RegExp(policy.trigger.bash_match);
158
- }
159
- catch {
160
- return false;
161
- }
162
- // Raw-OR-normalised-OR-amp-normalised-OR-quote-normalised (D-003, run
163
- // 2026-07-27-gate-target-repo-resolution; third arm added task
164
- // aabbad63; fourth arm added task cf3dff51): test the RAW command
165
- // first — cheap, and byte-identical to the pre-fix behaviour — then,
166
- // only if that fails, the primary NORMALISED command (wrapper
167
- // prefixes peeled, git global options dropped, whitespace collapsed,
168
- // BOUNDARY_RE segmentation) — then, only if THAT also fails, the
169
- // ampersand-aware second pass (AMP_BOUNDARY_RE segmentation, closing
170
- // the bare-`&` family BOUNDARY_RE cannot see: `A=x&env -C /tmp git
171
- // status`, `echo hi & nice git status`) — then, only if THAT also
172
- // fails, the quote-aware third pass (BOUNDARY_RE's own alphabet, but
173
- // quote-tracking, closing a shell-boundary character sitting INSIDE a
174
- // quoted assignment value: `VAR='a; b' git push origin master`).
175
- // Strictly additive at every step: a command that matched today keeps
176
- // matching via the raw test alone; the primary normalised form can
177
- // only ADD a match (env/nice/command wrappers, extra git global
178
- // options, doubled whitespace); the amp-aware form can only add a
179
- // FURTHER match on top of those two; the quote-aware form can only
180
- // add a FOURTH match on top of all three, never remove one any of the
181
- // others already found. Replacing the matcher input instead of OR-ing
182
- // it in risks silently REMOVING an existing match if some pass ever
183
- // mangles a shape — the fail-open direction for a gate — so this
184
- // stays additive rather than a substitution at every arm.
185
- if (!re.test(command)) {
186
- const { normalized } = precomputedNormalizedCommand ?? normalizeCommand(command);
187
- if (!re.test(normalized)) {
188
- const amp = ampNormalizedCommandThunk
189
- ? ampNormalizedCommandThunk()
190
- : normalizeCommandAmpAware(command);
191
- if (!re.test(amp.normalized)) {
192
- const quoted = quoteNormalizedCommandThunk
193
- ? quoteNormalizedCommandThunk()
194
- : normalizeCommandQuoteAware(command);
195
- if (!re.test(quoted.normalized))
196
- return false;
197
- }
198
- }
199
- }
200
- }
201
- return true;
202
- }
203
- function buildEventContext(event) {
204
- return contextWithToolArgs(event, event.tool_input ?? event.raw_input ?? event.input);
205
- }
206
- function contextWithToolArgs(event, toolArgs) {
207
- return {
208
- toolArgs,
209
- event,
210
- session: { id: event.session_id ?? "" },
211
- git: {},
212
- };
213
- }
214
- function isNonNullObject(value) {
215
- return typeof value === "object" && value !== null;
216
- }
217
- /**
218
- * Whether `input_match` fails to arm the gate for this event (review
219
- * round 1, task 2699b476 round 2, MEDIUM security finding).
220
- *
221
- * The default `toolArgs` context (`buildEventContext`) resolves ONE
222
- * field with `tool_input ?? raw_input ?? input`, so a payload that
223
- * carries a benign value in the PREFERRED field and the actual merge
224
- * request in the OTHER field never gets read:
225
- * `{tool_input:{taskId:"t"}, raw_input:{taskId:"t",autoMerge:true}}`
226
- * resolves `toolArgs` to `tool_input` alone, `autoMerge` reads as
227
- * absent, and the narrowed `task_finish` gate stays unarmed for a call
228
- * that DOES request an auto-merge.
229
- *
230
- * Mirrors the two-field handling `resolveCodexExemptionCommand`
231
- * (`hook-codex-pre-tool-use.ts`) already established for this exact
232
- * shape, but in the opposite fail-closed direction: that function
233
- * REFUSES an exemption when the two fields disagree (an exemption is an
234
- * allow, so disagreement must not grant it); here a `requires:` gate is
235
- * a block, so disagreement must not WITHHOLD it. When both `tool_input`
236
- * and `raw_input` are present as non-null objects, `input_match` is
237
- * evaluated against BOTH; if either one matches, the predicate holds
238
- * (the gate is armed) regardless of what the other field says. Only
239
- * when NEITHER field matches does the gate stay unarmed.
240
- */
241
- function inputMatchMismatchesEvent(inputMatch, event) {
242
- const toolInput = event.tool_input;
243
- const rawInput = event.raw_input;
244
- if (isNonNullObject(toolInput) && isNonNullObject(rawInput)) {
245
- const toolInputMismatch = firstInputMatchMismatch(inputMatch, contextWithToolArgs(event, toolInput));
246
- const rawInputMismatch = firstInputMatchMismatch(inputMatch, contextWithToolArgs(event, rawInput));
247
- return toolInputMismatch !== null && rawInputMismatch !== null;
248
- }
249
- return firstInputMatchMismatch(inputMatch, buildEventContext(event)) !== null;
250
- }
251
- /** Map a failed-`requires` policy to its decision outcome by enforcement. */
252
- function outcomeForFailedRequires(enforcement) {
253
- switch (enforcement) {
254
- case "block":
255
- return "deny";
256
- case "warn":
257
- return "warn";
258
- case "require_approval":
259
- return "require_approval";
260
- }
261
- }
262
- /**
263
- * Does a decision abort the tool call? Phase 7 #6 makes the Risk Gate
264
- * authoritative at the `PreToolUse` boundary:
265
- * - `deny` aborts (a `block`-enforcement policy whose requires failed,
266
- * the Phase 4 mechanism, unchanged).
267
- * - `require_approval` aborts until the approval evidence exists. In
268
- * Phase 7 #5 this outcome was returned but did not block; #6 makes
269
- * it block. The approval tag is satisfiable through the policy's
270
- * `requires:` (an operator runs `harness approve risk`); once the
271
- * tag is on record the requires evaluation passes and the outcome
272
- * is `allow` instead.
273
- * - `deny-degraded` aborts (task f1aea826): it is only ever produced
274
- * for `block` / `require_approval` policies under the default
275
- * `preserve_enforcement` posture (see `degradedOutcome`), so its
276
- * presence alone means "an incident-preventing gate could not read
277
- * its evidence" — fail closed.
278
- * - `allow` / `warn` / `warn-degraded` never abort.
279
- *
280
- * Exported for the CLI wrapper's pending-approval staging
281
- * (`src/cli/policy/intercept.ts`), which must agree with the runtime on
282
- * WHICH decision is the first blocking one — a hand-rolled copy there
283
- * drifted when `deny-degraded` was added (review 2026-08-08, low
284
- * finding). Keep this the single definition.
285
- */
286
- export function isBlockingDecision(d) {
287
- if (d.outcome === "deny")
288
- return d.enforcement === "block";
289
- if (d.outcome === "deny-degraded")
290
- return true;
291
- return d.outcome === "require_approval";
292
- }
293
- /**
294
- * Outcome for a policy whose `requires` could NOT be evaluated at all —
295
- * degraded/thrown ledger query, unresolved template variables, invalid
296
- * `within`, thrown `evaluateRequires`, or the defensive
297
- * schema-invariant branch. The fail posture is derived from the
298
- * policy's own `enforcement:` (task f1aea826):
299
- *
300
- * warn → `warn-degraded` (availability first,
301
- * unchanged: advisory friction never
302
- * bricks the session)
303
- * block / require_approval → `deny-degraded` (fail closed: a gate
304
- * against an irreversible incident must
305
- * not open because its evidence source
306
- * is unreadable)
307
- *
308
- * The manifest-level opt-out `risk.degraded_fail_posture: fail_open`
309
- * restores the pre-0.45 availability-first mapping (`warn-degraded`
310
- * for every tier). The hand-built-manifest case (`options.manifest.risk`
311
- * absent, only possible past `harness validate`) defaults to the
312
- * fail-closed posture, matching the schema default.
313
- */
314
- function degradedOutcome(policy, manifest) {
315
- if (policy.enforcement === "warn")
316
- return "warn-degraded";
317
- const posture = manifest.risk?.degraded_fail_posture ?? "preserve_enforcement";
318
- return posture === "fail_open" ? "warn-degraded" : "deny-degraded";
319
- }
320
- /**
321
- * Control characters (C0 range plus DEL), built via fromCharCode so the
322
- * source file itself stays free of raw control bytes. Matches what the
323
- * envelope sanitiser collapses to a single space.
324
- */
325
- const ENVELOPE_CONTROL_CHARS = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}]+`, "g");
326
- /**
327
- * Bound and clean a transport-level reason before it is interpolated
328
- * into the agent-facing deny-degraded envelope. The string can embed
329
- * output captured from the grounding-mcp SUBPROCESS (`exitDiagnostic`
330
- * in `src/policies/ledger-client.ts` appends the child's last stderr
331
- * line), i.e. untrusted content that previously only reached stderr
332
- * and the audit ledger, never model-visible text. The full, untouched
333
- * string still goes to the audit row and the verbose diagnostic; only
334
- * the envelope interpolation and the default-verbosity operator hint
335
- * are bounded.
336
- *
337
- * Exported for the CLI wrapper's deny-degraded operator hint (review
338
- * 2026-08-08, round 3; default-verbosity only, suppressed under
339
- * verbose), which interpolates the same untrusted reason into its
340
- * one-line stderr surface.
341
- */
342
- export function sanitizeEnvelopeReason(reason) {
343
- const stripped = reason.replace(ENVELOPE_CONTROL_CHARS, " ");
344
- return stripped.length > 200 ? `${stripped.slice(0, 200)}...` : stripped;
345
- }
346
- const EMPTY_REPO_MESSAGE = "no git repository was found for this command: the working directory (or the directory the command targets) is not inside a git repository, so the repository-scoped evidence this policy checks cannot be looked up. " +
347
- "Change into the target repository first (`cd <repo>`) or name it explicitly (`git -C <repo> ...`), then retry the command.";
348
- /**
349
- * Names the repository whose HEAD is detached, so a detached working
350
- * directory that is denied next to a `git -C <other repo>` target is
351
- * recognisable as the working directory's own repository. The name is the
352
- * resolved `${REPO}` value: the work-tree basename, a `HARNESS_REPO`
353
- * override, or the value of a policy extract named `REPO` (which can
354
- * carry tool input), so it is cleaned and bounded like any other untrusted
355
- * text before it reaches the agent envelope: control characters blanked,
356
- * at most 200 characters (`sanitizeEnvelopeReason`).
357
- */
358
- function emptyBranchMessage(repo) {
359
- const name = sanitizeEnvelopeReason(repo);
360
- return (`no branch is checked out in repository \`${name}\`: HEAD is detached, so the branch-scoped evidence this policy checks cannot be looked up. ` +
361
- "Check out a named branch there (`git switch <branch>`, or `git switch -c <branch>` for a new one), create the evidence for that branch, then retry the command.");
362
- }
363
- function isBlankIdentifier(value) {
364
- return value === undefined || value.trim().length === 0;
365
- }
366
- /**
367
- * Does `ledgerTagTemplate` reference `${REPO}` / `${BRANCH}` while the
368
- * value resolved for this context is empty? An empty identifier must
369
- * never be rendered into a ledger tag: `preflight:` is a substring of
370
- * EVERY preflight fact, so a blank tag lets any unrelated fact satisfy
371
- * the gate (and `preflight-before-push` would render "You cannot push
372
- * branch yet."). The caller decides per enforcement with
373
- * `message` instead of querying the ledger.
374
- *
375
- * `REPO` wins over `BRANCH`, and a branch-only template evaluated where
376
- * REPO is empty too (no repository at all) gets the no-repository text,
377
- * since "HEAD is detached" would be false there. The messages name a
378
- * state the agent can establish (`cd` / `git -C` / `git switch`), never
379
- * a tag to produce, so there is no producer trap, and never an opt-out
380
- * (a block that names its own disable path is not a gate).
381
- *
382
- * Exported so `harness dry-run` shows the same hint instead of a blank
383
- * tag (`src/cli/dry-run.ts`).
384
- */
385
- export function emptyIdentifierGuard(ledgerTagTemplate, values) {
386
- const refsRepo = ledgerTagTemplate.includes("${REPO}");
387
- const refsBranch = ledgerTagTemplate.includes("${BRANCH}");
388
- if (refsRepo && isBlankIdentifier(values.REPO)) {
389
- return { identifier: "REPO", message: EMPTY_REPO_MESSAGE };
390
- }
391
- if (refsBranch && isBlankIdentifier(values.BRANCH)) {
392
- return isBlankIdentifier(values.REPO)
393
- ? { identifier: "REPO", message: EMPTY_REPO_MESSAGE }
394
- : { identifier: "BRANCH", message: emptyBranchMessage(values.REPO ?? "") };
395
- }
396
- return null;
397
- }
398
- /**
399
- * Placeholder `ledgerTag` recorded on an `operator_only` decision (and on
400
- * the defensive schema-invariant-violated branch below). Both outcomes
401
- * are decided WITHOUT ever substituting or querying a real tag, so this
402
- * is a readable marker for `harness audit` / `explain --trace` / the
403
- * stderr diagnostic, not a value anything matches against.
404
- */
405
- const NO_LEDGER_TAG = "(operator-only: no ledger tag — never evaluated)";
406
- async function evaluateOnePolicy(policy, options) {
407
- const evaluatedAt = (options.now ?? new Date()).toISOString();
408
- const ctx = buildEventContext(options.event);
409
- const extract = evaluateExtract(policy.trigger.extract ?? {}, ctx, options.builtins);
410
- // Unconditional operator-only deny (schema `operator_only: true`, task
411
- // 2cc73f55). The schema's superRefine guarantees this form carries NO
412
- // `requires:` and `enforcement: block`, so short-circuit here, BEFORE
413
- // the requires pipeline: no ledger query, no template substitution, no
414
- // `evaluateRequires` call. That is the load-bearing property — there is
415
- // no in-session evidence (ledger tag, marker file, flag) this branch
416
- // ever reads, so none can flip the outcome to allow. `extract` above is
417
- // still computed (cheap, pure) only because a `ux:`/`producers:` block
418
- // on this policy may reference `${VAR}`s from `trigger.extract`; it
419
- // plays no role in the outcome below.
420
- if (policy.operator_only === true) {
421
- return {
422
- policyName: policy.name,
423
- enforcement: policy.enforcement,
424
- outcome: "deny",
425
- reason: "operator-only: this policy declares no requires: and cannot be satisfied by any in-session evidence",
426
- extractValues: extract.values,
427
- ledgerTag: NO_LEDGER_TAG,
428
- evaluatedAt,
429
- };
430
- }
431
- const requires = policy.requires;
432
- if (requires === undefined) {
433
- // Unreachable under a schema-validated manifest: PolicySchema's
434
- // superRefine requires either `requires:` or `operator_only: true`.
435
- // Defensive branch only, for a manifest that bypassed
436
- // `harness validate` (hand-built Policy object, stale cached parse).
437
- //
438
- // Deliberately NOT a plain `deny`: this is the SAME "could not
439
- // decide" family as the other degraded returns in this function
440
- // (unresolved template variables, a degraded ledger query, an
441
- // invalid `within`, a throwing `evaluateRequires`) — the contract
442
- // for all of them is "the evaluator could not form a real verdict",
443
- // routed through `degradedOutcome` so the policy's own enforcement
444
- // decides the fail posture (task f1aea826): `warn` stays the
445
- // non-blocking `warn-degraded`, `block`/`require_approval` fail
446
- // closed as `deny-degraded`. Failing this branch as plain `deny`
447
- // would treat an internal schema-invariant violation as if it were
448
- // a deliberate `operator_only: true` policy authored by the
449
- // operator, which it is not — `deny-degraded` keeps the two
450
- // observably distinct in every audit row and envelope.
451
- return {
452
- policyName: policy.name,
453
- enforcement: policy.enforcement,
454
- outcome: degradedOutcome(policy, options.manifest),
455
- reason: "policy declares neither requires: nor operator_only: true (schema invariant violated)",
456
- extractValues: extract.values,
457
- ledgerTag: NO_LEDGER_TAG,
458
- evaluatedAt,
459
- };
460
- }
461
- // Empty-identifier guard: decided BEFORE any tag is rendered or the
462
- // ledger is queried, per the policy's own enforcement (block -> deny,
463
- // require_approval -> require_approval, warn -> warn). Deliberately NOT
464
- // routed through `degradedOutcome` / the unresolved-variable branch
465
- // below: that path ends in the `deny-degraded` envelope, which blames
466
- // an unreadable ledger and tells the agent to ask the operator to
467
- // check grounding-mcp, false and misleading for an empty identifier.
468
- const emptyGuard = emptyIdentifierGuard(requires.ledger_tag, extract.values);
469
- if (emptyGuard !== null) {
470
- return {
471
- policyName: policy.name,
472
- enforcement: policy.enforcement,
473
- outcome: outcomeForFailedRequires(policy.enforcement),
474
- reason: emptyGuard.message,
475
- extractValues: extract.values,
476
- ledgerTag: `(empty ${emptyGuard.identifier}: no ledger tag rendered, no ledger query)`,
477
- emptyIdentifier: emptyGuard.identifier,
478
- evaluatedAt,
479
- };
480
- }
481
- const missingExtracts = extract.traceData
482
- .filter((t) => t.source === "missing")
483
- .map((t) => t.var);
484
- const sub = substituteTemplate(requires.ledger_tag, extract.values);
485
- const ledgerTag = sub.result;
486
- const unresolved = [...missingExtracts, ...sub.missing];
487
- if (unresolved.length > 0) {
488
- return {
489
- policyName: policy.name,
490
- enforcement: policy.enforcement,
491
- outcome: degradedOutcome(policy, options.manifest),
492
- reason: `template variables unresolved: ${unresolved.join(", ")}`,
493
- extractValues: extract.values,
494
- ledgerTag,
495
- evaluatedAt,
496
- };
497
- }
498
- const sessionId = resolveSessionId(options.event.session_id);
499
- let queryResult;
500
- try {
501
- queryResult = await options.ledger.query(ledgerTag, sessionId, options.ledgerTimeoutMs);
502
- }
503
- catch (err) {
504
- queryResult = {
505
- kind: "degraded",
506
- reason: `ledger query threw: ${err.message}`,
507
- };
508
- }
509
- if (queryResult.kind === "degraded") {
510
- return {
511
- policyName: policy.name,
512
- enforcement: policy.enforcement,
513
- outcome: degradedOutcome(policy, options.manifest),
514
- reason: queryResult.reason,
515
- extractValues: extract.values,
516
- ledgerTag,
517
- evaluatedAt,
518
- };
519
- }
520
- // Pre-validate `within` against the runtime parser so a manifest that
521
- // bypassed `harness validate` doesn't throw uncaught.
522
- if (requires.within !== undefined) {
523
- try {
524
- parseDurationSeconds(requires.within);
525
- }
526
- catch {
527
- return {
528
- policyName: policy.name,
529
- enforcement: policy.enforcement,
530
- outcome: degradedOutcome(policy, options.manifest),
531
- reason: `invalid within: ${requires.within}`,
532
- extractValues: extract.values,
533
- ledgerTag,
534
- evaluatedAt,
535
- };
536
- }
537
- }
538
- const evalOpts = {
539
- ...(options.now && { now: options.now }),
540
- ...(options.currentHeadSha !== undefined &&
541
- options.currentHeadSha.length > 0 && {
542
- currentHeadSha: options.currentHeadSha,
543
- }),
544
- };
545
- const filtered = filterEntriesByTag(queryResult.entries, ledgerTag);
546
- let evaluation;
547
- try {
548
- evaluation = evaluateRequires({ ...requires, ledger_tag: ledgerTag }, filtered, evalOpts);
549
- }
550
- catch (err) {
551
- return {
552
- policyName: policy.name,
553
- enforcement: policy.enforcement,
554
- outcome: degradedOutcome(policy, options.manifest),
555
- reason: `requires eval threw: ${err.message}`,
556
- extractValues: extract.values,
557
- ledgerTag,
558
- evaluatedAt,
559
- };
560
- }
561
- // Four-way decision (Phase 7 #5). A satisfied `requires` always
562
- // `allow`s; a failed one is mapped by the policy's enforcement —
563
- // `block` → `deny`, `warn` → `warn`, `require_approval` →
564
- // `require_approval`. The evaluator only RETURNS `require_approval`
565
- // here; Phase 7 #6 makes it block.
566
- const outcome = evaluation.allowed
567
- ? "allow"
568
- : outcomeForFailedRequires(policy.enforcement);
569
- return {
570
- policyName: policy.name,
571
- enforcement: policy.enforcement,
572
- outcome,
573
- reason: evaluation.reason,
574
- extractValues: extract.values,
575
- ledgerTag,
576
- requiresEval: {
577
- matchedCount: evaluation.matchedCount,
578
- reason: evaluation.reason,
579
- },
580
- recordHint: evaluation.recordHint,
581
- evaluatedAt,
582
- };
583
- }
584
- function filterEntriesByTag(entries, tag) {
585
- // The ledger client returns the entire session's entries; filter to those
586
- // whose content/source matches the substituted tag. evaluateRequires also
587
- // does a substring match, but pre-filtering here keeps the trace quieter
588
- // and avoids the requires evaluator iterating unrelated session entries.
589
- //
590
- // Phase 5 #4 — also drop `policy_decision` rows so a past audit
591
- // payload doesn't incidentally match the same tag the decision was
592
- // about (the substring-pollution bug from PR #39's dogfood). The
593
- // requires evaluator's entryMatches has the same guard, but
594
- // pre-filtering keeps matchedCount honest in the trace data.
595
- return entries.filter((e) => e.type !== POLICY_DECISION_TYPE &&
596
- // Legacy backstop for pre-Phase-5-#4 rows: they were stored as
597
- // type='fact' but carry the `policy_decision:` content prefix.
598
- // Drop them at the same gate so a user upgrading harness without
599
- // flushing their dev ledger doesn't keep paying the pollution
600
- // tax. New rows are caught by the type check above.
601
- !e.content.startsWith(`${POLICY_DECISION_TYPE}:`) &&
602
- (e.content.includes(tag) ||
603
- (e.source !== undefined && e.source.includes(tag))));
604
- }
605
- /**
606
- * Which of a policy's own trigger-satisfying segments name a target — the
607
- * attribution sibling of `policyMatchesEvent` (task `98ad072f`, T-003).
608
- * Re-tests the policy's OWN `bash_match` regex against each segment's
609
- * `text` in isolation: every shipped `bash_match` alternation includes a
610
- * `^` branch, so testing it against one segment's canonicalised text
611
- * alone (no leading boundary character — see `CommandSegment.text`'s own
612
- * doc comment) is well-defined. Returns every segment that matches on its
613
- * own — usually one, occasionally several (D-004: a decoy read and a cwd
614
- * read chained in one command can each independently satisfy the SAME
615
- * trigger).
616
- *
617
- * Never changes WHETHER a policy matches — `policyMatchesEvent` already
618
- * decided that from the WHOLE command (raw-or-normalised-or-amp-
619
- * normalised), unchanged by this function. This only narrows down WHICH
620
- * segment(s), if any, are individually responsible for the match, so
621
- * `intercept()` below knows which segment's target — if any — to resolve
622
- * the `${REPO}`/`${BRANCH}`/`currentHeadSha` builtins from instead of the
623
- * event's own cwd. `[]` when the policy has no `bash_match` trigger (an
624
- * MCP-tool-name-triggered policy — no segment concept applies), when its
625
- * regex is malformed (mirrors `policyMatchesEvent`'s own defensive
626
- * `try/catch`), or when the whole-command match came ONLY from the
627
- * ampersand-aware third arm: `segments` here is always built from the
628
- * PRIMARY (`BOUNDARY_RE`) segmentation, which — by construction — cannot
629
- * itself contain the bare-`&` split the amp arm relies on, so an amp-
630
- * only match finds no individually-matching segment and this returns
631
- * `[]` (D-003: cwd builtins, identical to shipped).
632
- */
633
- export function attributeTriggerSegments(policy, segments) {
634
- if (policy.trigger.bash_match === undefined)
635
- return [];
636
- let re;
637
- try {
638
- re = new RegExp(policy.trigger.bash_match);
639
- }
640
- catch {
641
- return [];
642
- }
643
- return segments.filter((seg) => re.test(seg.text));
644
- }
645
- /**
646
- * Does a policy's `requires:` reference the per-repo `${REPO}`/`${BRANCH}`
647
- * builtins, or ask for `at_head`? Only these policies pay the per-policy
648
- * attribution cost below — every other matching policy keeps the plain,
649
- * per-event cwd builtins `options.builtins` already carries, byte-
650
- * identical to before this task. D-005: `at_head` is included even when
651
- * `ledger_tag` itself doesn't reference `${REPO}`/`${BRANCH}`, because
652
- * `at_head` compares against `currentHeadSha`, which this task resolves
653
- * from the SAME per-policy context — splitting the two would check
654
- * `at_head` against the cwd's HEAD while `${REPO}` (if present elsewhere)
655
- * named a different repo, the worse inconsistency D-005 rejects.
656
- */
657
- function usesPerRepoBuiltins(policy) {
658
- const requires = policy.requires;
659
- if (requires === undefined)
660
- return false;
661
- return (requires.at_head === true ||
662
- requires.ledger_tag.includes("${REPO}") ||
663
- requires.ledger_tag.includes("${BRANCH}"));
664
- }
665
- /**
666
- * Bound on the number of DISTINCT attributed contexts one policy is
667
- * evaluated against for one event (D-013, fix round, run
668
- * 2026-08-02-per-repo-gate-scoping-redesign). Each distinct context costs
669
- * one ledger query and one audit write in `intercept()`'s evaluation loop
670
- * below; on a manifest with several per-repo-builtins policies (the
671
- * shipped `FULL_TEMPLATE` has four), an event naming K distinct targets
672
- * amplifies to 4K queries/writes, unbounded by `MAX_NORMALIZE_LENGTH` —
673
- * measured 200/200 at K=200 by reviewer 2, on a hook budget whose timeout
674
- * is ALLOW (same class as the 07-27 quadratic hot-path fail-open: a slow
675
- * enough event silently passes). `resolveAttributedContexts` returns a
676
- * `"bounded"` result instead of a `contexts` array once a policy's DISTINCT
677
- * targets would exceed this constant; `intercept()` denies that policy
678
- * directly (naming the ambiguity) without querying the ledger for any of
679
- * them, rather than silently evaluating all of them.
680
- */
681
- export const MAX_ATTRIBUTED_CONTEXTS = 4;
682
- /**
683
- * Realpath a path for identity comparison, never throwing (D-012, fix
684
- * round, run 2026-08-02-per-repo-gate-scoping-redesign). Falls back to the
685
- * LEXICAL path unchanged when the target does not exist or is otherwise
686
- * unreadable (`fs.realpathSync.native` throws `ENOENT` for a path this
687
- * module was never guaranteed to have on disk, and this function must
688
- * never throw — same fail-safe posture as `resolveGitContext` itself,
689
- * which returns empty strings rather than throwing on a bad path).
690
- * `.native` (not the plain JS `fs.realpathSync`) resolves via the OS
691
- * syscall directly — cheaper, and avoids Node's own pure-JS symlink-loop
692
- * bookkeeping for a value this function only uses for an identity
693
- * comparison, never for a filesystem walk of its own.
694
- */
695
- function realpathOrSelf(p) {
696
- try {
697
- return fs.realpathSync.native(p);
698
- }
699
- catch {
700
- return p;
701
- }
702
- }
703
- /**
704
- * Resolve the distinct `${REPO}`/`${BRANCH}`/`currentHeadSha` contexts a
705
- * `usesPerRepoBuiltins` policy must be evaluated against (task `98ad072f`,
706
- * T-003, `01-plan.md` Proposed Approach items 2-4). The engine trusts a
707
- * trigger-satisfying segment's `effectiveTarget` UNIFORMLY as the
708
- * directory that segment's own invocation genuinely runs in (bash
709
- * semantics) — orchestrator decision D-010, 2026-08-02: an earlier
710
- * revision of this function additionally distrusted an inherited target
711
- * whenever a DIFFERENT invocation intervened between the `cd` and the
712
- * satisfying segment — REJECTED: that coupling has no basis in bash's own
713
- * semantics, is dodgeable by inserting any harmless read between the `cd`
714
- * and the gated verb, and carried its own unverified `cd`-recognition
715
- * gap. The `cd <B> && git log && git push` shape this was meant to guard
716
- * is not a regression at all under this design — `git push` genuinely
717
- * runs inside B — see `tests/runtime/intercept-cli.test.ts`'s "leading-cd
718
- * is now a deliverable" block.
719
- *
720
- * D-021 (UNIVERSAL-ADDITIVE, operator decision, fix round, run
721
- * 2026-08-02-per-repo-gate-scoping-redesign): attribution is uniformly
722
- * ADDITIVE, never REPLACE. Every satisfying segment with an attributable
723
- * target — `seg.effectiveTarget !== null`, whether it came from the
724
- * segment's OWN explicit repo-relocating flag (`-C`/`env -C`/`--git-dir`;
725
- * `--work-tree` is excluded from ever producing a target at all, D-017)
726
- * or was INHERITED from a preceding `cd` — demands the cwd context AND
727
- * that target's own context, side by side; `seg.ownTarget` is not
728
- * consulted for this decision. Any unsatisfied context blocks the whole
729
- * event (the existing per-context evaluation machinery below is
730
- * unchanged). `seg.effectiveTarget === null` (fully unattributable —
731
- * D-003) still adds ONLY the cwd context — there is no foreign target to
732
- * be additive WITH.
733
- *
734
- * Demanding cwd unconditionally makes the engine structurally immune to
735
- * misattribution of the foreign target: no gap in `command-normalize.ts`'s
736
- * static, string-only model of shell control flow (subshells, pipes,
737
- * `cd -`, a later `cd`, an unmodeled git flag composition) can ever make a
738
- * gate WEAKER than the shipped, cwd-only engine, because cwd's own demand
739
- * is never dropped — only added to. This replaces an earlier REPLACE-for-
740
- * own-target design (D-011) that trusted a statically extracted own
741
- * target as proof of "this one invocation operates there"; four
742
- * independent review passes each measured a live bypass of that
743
- * invariant against the shipped binary (a forged `.git/HEAD` directory
744
- * reached through, respectively, a non-persisting `cd`, `--work-tree`,
745
- * more than one repo-relocating flag, and a tilde-valued flag not counted
746
- * by the multi-flag guard) — see `03-decisions.md` D-011/D-017/D-018/
747
- * D-019/D-020/D-021 for the full history. The static-analysis-proves-it
748
- * invariant those bypasses each disproved is not carried forward here.
749
- * Cost accepted by the operator: a legitimate `git -C B` from cwd A now
750
- * demands BOTH A's and B's context, where the pre-D-021 engine demanded
751
- * only B's.
752
- *
753
- * One exception to "never dropped": when the working directory is outside
754
- * every git repository (a blank `${REPO}` for the policy, as the guard
755
- * sees it), its cwd context is not demanded next to a segment whose own
756
- * target resolved to a real repository (see the loop body). That
757
- * exception relies on two static models: the target attribution, whose
758
- * known misattribution (a `GIT_DIR=` prefix or a third repository reached
759
- * through another construct) exists for every cwd, and the check that the
760
- * cwd is outside every repository, which errs toward inside: it holds only
761
- * when neither the cwd nor any ancestor has an entry named `HEAD` or
762
- * `.git` (`mayBeInsideRepository`). A detached cwd and every other
763
- * non-blank cwd context are still never dropped.
764
- *
765
- * D-012: a target reached through a symlink resolves to its REAL
766
- * (realpath'd) repository identity, not the symlink's own lexical
767
- * basename — `resolveGitContext` derives `repo` from the basename of
768
- * wherever it's pointed, and never itself realpaths, so an agent could
769
- * otherwise pick the demanded repo identity by naming a symlink.
770
- *
771
- * D-013: distinct contexts are bounded at `MAX_ATTRIBUTED_CONTEXTS`; see
772
- * that constant's own comment.
773
- *
774
- * D-015: the cwd context is deduped by its OWN `[REPO, BRANCH, sha]`
775
- * signature (not the literal string `"cwd"`), so a foreign path that
776
- * resolves to the SAME repository identity as cwd (e.g. a subdirectory of
777
- * the cwd repo reached via an explicit `-C`) collapses into the cwd
778
- * context instead of producing a spurious duplicate decision/audit write.
779
- * `repoOverridden` / `branchOverridden` (from `InterceptOptions`, both
780
- * default `false`) keep an operator's `HARNESS_REPO`/`HARNESS_BRANCH`
781
- * override intact in an attributed foreign context instead of letting the
782
- * target repo's own identity silently overwrite it — independently per
783
- * field. `satisfying` is a `Set` (not `Array.includes` per segment) for
784
- * O(1) membership tests instead of O(n) per segment.
785
- *
786
- * A policy with no attributable foreign target (no `bash_match`, no
787
- * individually-matching segment, only an amp-arm-only match, an
788
- * unattributable composition, a target resolving to the SAME repository
789
- * identity as cwd, or a target outside any git repo — D-003) is evaluated
790
- * EXACTLY as it is today, with the EXACT SAME `options.builtins` object
791
- * reference (no clone) when nothing attributed, keeping that the
792
- * byte-identical common case.
793
- *
794
- * `resolveGitContextMemo` is a per-`intercept()`-call cache keyed by the
795
- * REALPATH'D absolute path (never module-level state — no cross-event
796
- * caching), so several policies (or several satisfying segments) naming
797
- * the same foreign path within one event pay the `fs` cost once, not
798
- * once per policy per segment. `mayBeInsideRepositoryMemo` is the same
799
- * kind of per-call cache for the cwd's own outside-every-repository
800
- * check, keyed by the cwd's real path, so that walk runs once per event.
801
- */
802
- function resolveAttributedContexts(policy, segments, cwdBuiltins, cwdCurrentHeadSha, resolveGitContextMemo, mayBeInsideRepositoryMemo, repoOverridden, branchOverridden, extractContext) {
803
- const cwdContext = { builtins: cwdBuiltins, currentHeadSha: cwdCurrentHeadSha };
804
- const satisfying = new Set(attributeTriggerSegments(policy, segments));
805
- if (satisfying.size === 0)
806
- return { kind: "contexts", contexts: [cwdContext] };
807
- const cwdSignature = [cwdBuiltins.REPO, cwdBuiltins.BRANCH, cwdCurrentHeadSha ?? ""].join("|");
808
- const seenSignatures = new Set();
809
- const contexts = [];
810
- const addCwdOnce = () => {
811
- if (seenSignatures.has(cwdSignature))
812
- return;
813
- seenSignatures.add(cwdSignature);
814
- contexts.push(cwdContext);
815
- };
816
- const cwdReal = realpathOrSelf(cwdBuiltins.CWD);
817
- // Decided from the same values `evaluateOnePolicy` guards on: the
818
- // policy's own `trigger.extract` evaluated against the cwd builtins, so
819
- // an extract named `REPO` / `BRANCH` that shadows a builtin is honoured
820
- // here exactly as it is there.
821
- const cwdGuard = emptyIdentifierGuard(policy.requires?.ledger_tag ?? "", evaluateExtract(policy.trigger.extract ?? {}, extractContext, cwdBuiltins).values);
822
- // "Outside every repository" is checked on the cwd's real path: the
823
- // builtins resolve the path as given, while git runs in the physical
824
- // directory, so a symlink into a repository is not outside it. An empty
825
- // CWD names no directory at all and never qualifies. The check is the
826
- // conservative `mayBeInsideRepository`, not the static
827
- // `resolveGitContext` the builtins come from: that resolver misses
828
- // repositories git still finds (see the helper's doc comment), and here
829
- // a miss would drop a demand. It runs only for a blank `${REPO}`, at
830
- // most once per event (`mayBeInsideRepositoryMemo`).
831
- const cwdOutsideEveryRepository = cwdGuard?.identifier === "REPO" &&
832
- cwdBuiltins.CWD.length > 0 &&
833
- !mayBeInsideRepositoryMemoised(cwdReal, mayBeInsideRepositoryMemo);
834
- for (const seg of segments) {
835
- if (!satisfying.has(seg))
836
- continue;
837
- if (seg.effectiveTarget === null) {
838
- // D-003: fully unattributable — cwd only, no foreign target to be
839
- // additive with.
840
- addCwdOnce();
841
- continue;
842
- }
843
- // D-021 (UNIVERSAL-ADDITIVE, operator decision after the four-pass
844
- // halt — see this function's own doc comment): the cwd context is
845
- // demanded UNCONDITIONALLY here, regardless of whether the target came
846
- // from the segment's own explicit flag or was inherited from a
847
- // preceding `cd`. `seg.ownTarget` is no longer read for this decision.
848
- // The one exception is `cwdOutsideEveryRepository` below: it is
849
- // decided after the target resolved, so the cwd context is added from
850
- // the branches of this loop body instead of up front, in the same
851
- // order as before.
852
- const resolvedLexical = path.resolve(cwdBuiltins.CWD, seg.effectiveTarget);
853
- const resolved = realpathOrSelf(resolvedLexical);
854
- if (resolved === cwdReal) {
855
- addCwdOnce();
856
- continue;
857
- }
858
- let gitCtx = resolveGitContextMemo.get(resolved);
859
- if (gitCtx === undefined) {
860
- gitCtx = resolveGitContext(resolved);
861
- resolveGitContextMemo.set(resolved, gitCtx);
862
- }
863
- if (gitCtx.repo.length === 0) {
864
- // D-003: outside any repo → cwd fallback, not a distinct context.
865
- addCwdOnce();
866
- continue;
867
- }
868
- const signature = [gitCtx.repo, gitCtx.branch, gitCtx.sha].join("|");
869
- // Empty-identifier exception to the universal-additive rule, limited
870
- // to a working directory outside every git repository. That cwd
871
- // context has a blank `${REPO}` (the value the guard sees, after the
872
- // policy's own extract) and can never be satisfied: the
873
- // empty-identifier guard denies it without a ledger query. Demanding
874
- // it next to a target that resolved to a real repository would deny
875
- // the very remedy the guard's message names (`git -C <repo> ...`, or
876
- // `cd <repo> && ...` in one command). The skip applies only when the
877
- // cwd is outside every repository and this segment's own target
878
- // resolved to a real repository; the target's own context is still
879
- // demanded in full. It relies on two static models: the target
880
- // attribution, whose known misattribution (a `GIT_DIR=` prefix or a
881
- // third repository reached through another construct) exists for every
882
- // cwd, and the cwd resolution, which is conservative: the skip applies
883
- // only when neither the cwd's real path nor any ancestor up to the
884
- // filesystem root holds an entry named `HEAD` or `.git` (any type) and
885
- // no lookup failed other than with ENOENT (`mayBeInsideRepository`).
886
- // What remains outside both models: a third repository the command
887
- // really runs in (a `GIT_DIR=` prefix, or a `cd` into another
888
- // repository before the misattributed segment), state the command
889
- // itself creates while it runs (a `.git` it links or writes before the
890
- // git verb), and an ambient `GIT_DIR` / `GIT_COMMON_DIR` in the
891
- // environment git runs with. A
892
- // detached cwd (non-blank `${REPO}`, blank `${BRANCH}`) and
893
- // every non-blank cwd context keep the rule above unchanged: the
894
- // detached remedy (`git switch` in that repository) is establishable,
895
- // and dropping that context would let a misattributed push from the
896
- // detached HEAD pass on the target's evidence. A segment without a
897
- // resolved target (a bare `git status`) keeps its cwd context and
898
- // still denies with the hint.
899
- if (!cwdOutsideEveryRepository)
900
- addCwdOnce();
901
- if (signature === cwdSignature) {
902
- // D-015: a foreign target that resolves to cwd's own REAL identity
903
- // (e.g. a subdirectory of the cwd repo reached via `-C`, a
904
- // different literal path than `cwdBuiltins.CWD` itself but the SAME
905
- // `.git`) collapses into the cwd context regardless of segment
906
- // order — not just when a bare cwd-reading segment happened to add
907
- // it first.
908
- addCwdOnce();
909
- continue;
910
- }
911
- if (seenSignatures.has(signature))
912
- continue;
913
- if (contexts.length >= MAX_ATTRIBUTED_CONTEXTS) {
914
- // D-013: fail CLOSED — do not evaluate any of them, name the
915
- // ambiguity instead. `contexts.length + 1` names "at least this
916
- // many distinct targets", the count observed before bailing, not
917
- // necessarily the final total (the loop stops here).
918
- return { kind: "bounded", distinctCount: contexts.length + 1 };
919
- }
920
- seenSignatures.add(signature);
921
- contexts.push({
922
- builtins: {
923
- ...cwdBuiltins,
924
- REPO: repoOverridden ? cwdBuiltins.REPO : gitCtx.repo,
925
- BRANCH: branchOverridden ? cwdBuiltins.BRANCH : gitCtx.branch,
926
- },
927
- currentHeadSha: gitCtx.sha.length > 0 ? gitCtx.sha : undefined,
928
- });
929
- }
930
- // Defensive fallback: every satisfying segment existed but somehow none
931
- // was added above (should not happen — every branch above adds either
932
- // the cwd context or a foreign one). Never leave a matched, per-repo-
933
- // builtins policy with zero contexts to evaluate against.
934
- return { kind: "contexts", contexts: contexts.length > 0 ? contexts : [cwdContext] };
935
- }
936
- /**
937
- * Could git find a repository from `dir`? Decided for the one place that
938
- * drops a demand on the answer (the cwd-outside-every-repository exception
939
- * in `resolveAttributedContexts`), so every doubt counts as inside. The
940
- * static `resolveGitContext` walk that resolves the `REPO` / `BRANCH`
941
- * builtins is left as it is for its other callers; it reports no
942
- * repository where git still finds one: a `.git` directory without `HEAD`
943
- * ends its walk (git skips it and walks on), its walk stops after 128
944
- * levels, and it never recognises a directory that is itself a git
945
- * directory (a bare repository, or one whose objects live elsewhere
946
- * through a `commondir` file).
947
- *
948
- * This walk does not list layouts. It uses the one structural fact every
949
- * git directory shares: git accepts a directory as a git directory only
950
- * when it holds an entry named `HEAD`, and it reaches a repository from a
951
- * working directory only through a `.git` entry (directory or `gitdir:`
952
- * file) or a git directory on the way up. So, from `dir` up to the
953
- * filesystem root with no depth bound:
954
- *
955
- * - an entry named `HEAD` or `.git`, of any type, valid or not, counts as
956
- * inside (never followed, read or validated);
957
- * - an `lstat` error other than ENOENT counts as inside (the entry may
958
- * exist).
959
- *
960
- * The cost is a conservative false inside: a directory that merely holds
961
- * an entry named `HEAD` (also `head` or `Head` on a case-insensitive
962
- * volume), or lies below one, keeps its cwd context and denies with the
963
- * no-repository hint (fail closed). An ambient `GIT_DIR` /
964
- * `GIT_COMMON_DIR` in git's environment is not visible here.
965
- */
966
- function mayBeInsideRepository(dir) {
967
- let current = path.resolve(dir);
968
- for (;;) {
969
- if (entryMayExist(path.join(current, ".git")))
970
- return true;
971
- if (entryMayExist(path.join(current, "HEAD")))
972
- return true;
973
- const parent = path.dirname(current);
974
- if (parent === current)
975
- return false;
976
- current = parent;
977
- }
978
- }
979
- /**
980
- * `false` only when `lstat` proves the entry absent (ENOENT, reported as
981
- * `undefined` without building an exception); every other error counts as
982
- * present.
983
- */
984
- function entryMayExist(entryPath) {
985
- try {
986
- return fs.lstatSync(entryPath, { throwIfNoEntry: false }) !== undefined;
987
- }
988
- catch (error) {
989
- return error.code !== "ENOENT";
990
- }
991
- }
992
- /**
993
- * `mayBeInsideRepository(dir)`, computed at most once per `dir` within one
994
- * `intercept()` call. `memo` is that call's own map (never module-level
995
- * state), so no answer outlives the event it was computed for.
996
- */
997
- function mayBeInsideRepositoryMemoised(dir, memo) {
998
- const known = memo.get(dir);
999
- if (known !== undefined)
1000
- return known;
1001
- const inside = mayBeInsideRepository(dir);
1002
- memo.set(dir, inside);
1003
- return inside;
1004
- }
1005
- /**
1006
- * Lazily resolve the event's per-segment view for attribution, computed
1007
- * at most once per `intercept()` call (the caller, `intercept()` below,
1008
- * memoises the RESULT in its own `segmentsForAttribution` local — this
1009
- * function itself is called at most once per call already, but stays
1010
- * side-effect-free so that remains true regardless of caller changes).
1011
- * Prefers `options.commandSegmentsThunk` (D-015 fix round: the deferred,
1012
- * compute-only-if-needed seam — see its own doc comment), then the eager
1013
- * `options.commandSegments`, then falls back to computing it directly
1014
- * from the event's own command for standalone callers/tests that inject
1015
- * neither. `null` (truncated — see `segmentViewOf`) and "no Bash command
1016
- * on this event" both collapse to `[]`, the same "nothing to attribute,
1017
- * cwd builtins" shape as a policy with no `bash_match` at all.
1018
- */
1019
- function resolveCommandSegments(options) {
1020
- if (options.commandSegmentsThunk !== undefined)
1021
- return options.commandSegmentsThunk() ?? [];
1022
- if (options.commandSegments !== undefined)
1023
- return options.commandSegments ?? [];
1024
- const command = extractShellCommand(options.event);
1025
- if (command === null)
1026
- return [];
1027
- return segmentViewOf(command) ?? [];
1028
- }
1029
- /**
1030
- * Synthesise the single decision `intercept()` records for a policy whose
1031
- * distinct attributed contexts exceeded `MAX_ATTRIBUTED_CONTEXTS` (D-013).
1032
- * Routes through `outcomeForFailedRequires` — the SAME enforcement-to-
1033
- * outcome mapping every other "could not safely evaluate" branch in
1034
- * `evaluateOnePolicy` uses — rather than a hardcoded outcome, so a
1035
- * `block`-enforcement policy (the security-critical case: preflight/push/
1036
- * merge gates) genuinely denies, while a `warn`-enforcement policy warns
1037
- * instead of hard-blocking, consistent with how every other failed-
1038
- * evaluation branch in this module already respects the policy's own
1039
- * declared enforcement. No ledger query, no template substitution against
1040
- * a resolved `ledger_tag` (there is no single context to substitute one
1041
- * against) — `extractValues` is still computed from `trigger.extract`
1042
- * against the CWD builtins (cheap, pure, matches the `operator_only`
1043
- * short-circuit's own reasoning) so a `ux:`/`producers:` block on this
1044
- * policy can still render.
1045
- */
1046
- function boundedContextsDecision(policy, event, distinctCount, cwdBuiltins, evaluatedAt) {
1047
- const extract = evaluateExtract(policy.trigger.extract ?? {}, buildEventContext(event), cwdBuiltins);
1048
- return {
1049
- policyName: policy.name,
1050
- enforcement: policy.enforcement,
1051
- outcome: outcomeForFailedRequires(policy.enforcement),
1052
- reason: `ambiguous: this command names at least ${distinctCount} distinct repository targets for this policy, ` +
1053
- `exceeding the ${MAX_ATTRIBUTED_CONTEXTS}-context bound — refusing to evaluate all of them`,
1054
- extractValues: extract.values,
1055
- ledgerTag: "(bounded: too many distinct attributed targets — no context queried)",
1056
- evaluatedAt,
1057
- };
1058
- }
1059
- export async function intercept(options) {
1060
- const { manifest, event } = options;
1061
- // The Risk Gate is active only when some policy declares a `when:`
1062
- // block. A manifest with none — every Phase 4 / 5 / 6 manifest — skips
1063
- // envelope enrichment entirely: no `buildActionEnvelope`, no
1064
- // classifier, no resolver, and decisions carry no `risk` / `environment`.
1065
- // That keeps such manifests byte-for-byte identical to pre-Phase-7-#5.
1066
- const riskGateActive = manifest.policies.some((p) => p.when !== undefined);
1067
- const enriched = riskGateActive
1068
- ? enrichEnvelope(manifest, event, options.riskContext, options.now)
1069
- : undefined;
1070
- // A policy fires only when its `trigger:` matches AND — when declared
1071
- // — every `when:` clause holds against the enriched envelope.
1072
- //
1073
- // For each matching when:-bearing policy we also record its
1074
- // `unclassifiedFallback` flag so the decision record and deny message
1075
- // can distinguish a genuine classification hit from a fail-closed
1076
- // unclassified command (M7: runtime audit + block message). Policies
1077
- // that have no `when:` block are not inserted into the map, so a
1078
- // later `whenFallbackMap.get(name) === true` test is unambiguous.
1079
- //
1080
- // Explicit loop rather than Array.filter() so the map is built as a
1081
- // first-class step: a filter predicate is expected to be pure, and a
1082
- // future refactor that parallelises the filter would silently break the
1083
- // audit flag if the mutation were still hiding inside the predicate.
1084
- const whenFallbackMap = new Map();
1085
- const matching = [];
1086
- for (const p of manifest.policies) {
1087
- if (!policyMatchesEvent(p, event, options.normalizedCommand, options.ampNormalizedCommandThunk, options.quoteNormalizedCommandThunk)) {
1088
- continue;
1089
- }
1090
- if (p.when === undefined) {
1091
- matching.push(p);
1092
- continue;
1093
- }
1094
- // `enriched` is defined here: a policy with `when:` set `riskGateActive`.
1095
- const whenEval = evaluateWhen(p.when, enriched);
1096
- if (whenEval.matched) {
1097
- whenFallbackMap.set(p.name, whenEval.unclassifiedFallback);
1098
- matching.push(p);
1099
- }
1100
- }
1101
- // Per-policy attribution (task `98ad072f`, T-003): `segmentsForAttribution`
1102
- // is resolved at most once, lazily, only if some matching policy actually
1103
- // uses `${REPO}`/`${BRANCH}`/`at_head` — a manifest with none of those
1104
- // (every Phase 4/5/6-only manifest) never computes it.
1105
- // `resolveGitContextMemo` is this call's own, non-module-level cache
1106
- // (task constraint: no eager/global filesystem work) — see
1107
- // `resolveAttributedContexts`'s own comment. `mayBeInsideRepositoryMemo`
1108
- // is the same kind of per-call cache for the cwd walk.
1109
- let segmentsForAttribution;
1110
- const resolveGitContextMemo = new Map();
1111
- const mayBeInsideRepositoryMemo = new Map();
1112
- const decisions = [];
1113
- for (const policy of matching) {
1114
- const attributed = usesPerRepoBuiltins(policy)
1115
- ? resolveAttributedContexts(policy, (segmentsForAttribution ??= resolveCommandSegments(options)), options.builtins, options.currentHeadSha, resolveGitContextMemo, mayBeInsideRepositoryMemo, options.repoOverridden === true, options.branchOverridden === true, buildEventContext(options.event))
1116
- : { kind: "contexts", contexts: [{ builtins: options.builtins, currentHeadSha: options.currentHeadSha }] };
1117
- if (attributed.kind === "bounded") {
1118
- // D-013: fail CLOSED without querying the ledger for any of the
1119
- // (too many) distinct targets — one synthetic decision, one audit
1120
- // write, then move on to the next policy. Ledger-query count for
1121
- // THIS policy stays at zero regardless of how many distinct targets
1122
- // the command actually names, instead of scaling with them.
1123
- const evaluatedAt = (options.now ?? new Date()).toISOString();
1124
- const decision = boundedContextsDecision(policy, event, attributed.distinctCount, options.builtins, evaluatedAt);
1125
- decisions.push(decision);
1126
- try {
1127
- await options.ledger.record(decision, resolveSessionId(event.session_id));
1128
- }
1129
- catch (err) {
1130
- (options.stderr ?? process.stderr).write(`harness runtime intercept: audit-write failed for ${decision.policyName}: ${err instanceof Error ? err.message : String(err)}\n`);
1131
- }
1132
- continue;
1133
- }
1134
- for (const context of attributed.contexts) {
1135
- // Same object reference as `options` on the (overwhelmingly common)
1136
- // single-cwd-context path — no clone, byte-identical to the
1137
- // pre-attribution call shape. Only a genuinely foreign context
1138
- // builds a shallow override.
1139
- const contextOptions = context.builtins === options.builtins &&
1140
- context.currentHeadSha === options.currentHeadSha
1141
- ? options
1142
- : { ...options, builtins: context.builtins, currentHeadSha: context.currentHeadSha };
1143
- const base = await evaluateOnePolicy(policy, contextOptions);
1144
- // Attach the per-event Risk Gate verdicts so `harness audit` /
1145
- // `explain --trace` can replay the classification + environment
1146
- // that the `when:` match was made against. Also carry the
1147
- // unclassifiedFallback flag (M7) when the when: evaluation set it
1148
- // to true; leave the field absent otherwise so decisions from
1149
- // manifests without a `when:` policy stay byte-identical.
1150
- const whenFallback = whenFallbackMap.get(policy.name);
1151
- const decision = enriched
1152
- ? {
1153
- ...base,
1154
- risk: enriched.risk,
1155
- environment: enriched.environment,
1156
- ...(whenFallback === true ? { whenUnclassifiedFallback: true } : {}),
1157
- }
1158
- : base;
1159
- decisions.push(decision);
1160
- try {
1161
- await options.ledger.record(decision, resolveSessionId(event.session_id));
1162
- }
1163
- catch (err) {
1164
- // Audit-write failure must not block; the decision is still applied.
1165
- // Surface the failure to stderr so a persistently-failing recorder
1166
- // does not silently leave `harness audit` / `explain --trace` blind.
1167
- // Goes to stderr to keep the stdout deny-JSON contract intact.
1168
- (options.stderr ?? process.stderr).write(`harness runtime intercept: audit-write failed for ${decision.policyName}: ${err instanceof Error ? err.message : String(err)}\n`);
1169
- }
1170
- }
1171
- }
1172
- // First blocking decision wins the envelope. `deny` and
1173
- // `require_approval` both abort (Phase 7 #6); the search order is the
1174
- // manifest's policy order, same as Phase 4.
1175
- const blocking = decisions.find(isBlockingDecision);
1176
- if (blocking) {
1177
- const sessionId = resolveSessionId(options.event.session_id);
1178
- // Append the "to satisfy" hint so Claude Code's deny message tells
1179
- // the operator (or the agent reading the same surface) what evidence
1180
- // would unblock the gate, instead of just naming the missing tag.
1181
- // The hint is content + window only; it does not prescribe a
1182
- // recording verb so the deny path stays neutral on producer (see
1183
- // agent-tasks/88ca4bb3 for why "use mcp__..." would be the wrong
1184
- // suggestion when the engine is the source of that suggestion).
1185
- //
1186
- // It DOES name the sessionId namespace the entry must be written
1187
- // under — this runtime session's id (the value shown), not the
1188
- // agent-tasks task UUID. Naming an identity is not a producer
1189
- // verb, so this stays compatible with the producer-neutrality above.
1190
- // The two namespaces are a known production footgun: an entry written
1191
- // under the agent-tasks task UUID never satisfies a harness runtime
1192
- // gate, which keys off the runtime session id (2026-05-17 incident,
1193
- // harness PRs #174/#175 — first attempt used the task UUID and was
1194
- // rejected, second used the session id and passed).
1195
- const hintSuffix = blocking.recordHint
1196
- ? ` To satisfy: ${blocking.recordHint}, under this runtime session's id \`${sessionId}\` (not the agent-tasks task UUID).`
1197
- : "";
1198
- // Opt-in producer block: when the policy declares `producers:` in
1199
- // the manifest, render the structured remediation list (bash / mcp
1200
- // / ask recipes) with ${VAR} placeholders substituted against the
1201
- // same extract.values the ledger_tag was resolved with. Schema
1202
- // validation guarantees at least one `mcp` producer per declared
1203
- // list, so an agent stuck in a Bash lockout always has an ungated
1204
- // recovery path. Policies without `producers:` get the legacy
1205
- // neutral deny envelope unchanged (agent-tasks/3804b785).
1206
- const blockingPolicy = matching.find((p) => p.name === blocking.policyName);
1207
- // When the policy declares `ux:`, the agent-facing surface
1208
- // becomes the plain-language `{cannot, required, run}` shape
1209
- // instead of the engine-vocabulary deny envelope. The internal
1210
- // decision (reason, recordHint, requiresEval, ledgerTag) is
1211
- // unchanged and still recorded to the audit ledger above. The
1212
- // producers block is suppressed when ux is declared because
1213
- // `run:` is the canonical remedy surface and rendering both would
1214
- // give the agent two different command suggestions.
1215
- let reasonText;
1216
- if (blocking.outcome === "deny-degraded") {
1217
- // Degraded-specific envelope (task f1aea826). Deliberately takes
1218
- // precedence over the policy's `ux:` surface: the operator-curated
1219
- // ux text describes the MISSING-evidence case ("run the producer,
1220
- // then retry"), which is actively misleading here — the evidence
1221
- // could not be READ, so producing the tag cannot unblock anything
1222
- // until the ledger is reachable again. The envelope names the real
1223
- // cause (the transport-level reason, sanitised — it embeds output
1224
- // from the grounding-mcp subprocess) and the posture, so nobody
1225
- // debugs a phantom missing tag.
1226
- //
1227
- // The `risk.degraded_fail_posture: fail_open` opt-out is
1228
- // deliberately NOT named here (review 2026-08-08, high finding):
1229
- // this text is fed back to the very agent that was just blocked,
1230
- // and nothing prevents that agent from editing the manifest — a
1231
- // deny that includes its own disable recipe is not a gate. The
1232
- // opt-out lives on operator surfaces only: the stderr diagnostic
1233
- // (`formatDecisionDiagnostic`), docs/risk-gate.md, and the OKF
1234
- // fail-posture matrix. Same reasoning as the producer-neutrality
1235
- // norm above and the operator-only framing of `harness pause`.
1236
- reasonText =
1237
- `${blocking.policyName}: required evidence could not be read ` +
1238
- `(evidence ledger degraded: ${sanitizeEnvelopeReason(blocking.reason)}). ` +
1239
- `This ${blocking.enforcement} policy fails closed while its evidence ` +
1240
- `source is unreadable; producing the required tag will not unblock it ` +
1241
- `until the ledger is reachable again. Ask your operator to check ` +
1242
- `grounding-mcp (harness doctor), then retry. Session: ${sessionId}.`;
1243
- }
1244
- else if (blocking.emptyIdentifier !== undefined) {
1245
- // Empty ${REPO} / ${BRANCH}: the decision's own reason names the
1246
- // state the agent can establish (`cd` / `git -C` / `git switch`).
1247
- // Takes precedence over `ux:` (whose `cannot:` text would render
1248
- // the blank identifier, "You cannot push branch yet.") and over
1249
- // `producers:` / the record hint (both would send the agent to
1250
- // produce a tag the gate will not read in this state). Names no
1251
- // opt-out, same reasoning as the degraded envelope above.
1252
- reasonText = `${blocking.policyName}: ${blocking.reason}`;
1253
- }
1254
- else if (blockingPolicy?.ux) {
1255
- // The ux surface is operator-curated plain language. Task
1256
- // 2929c5b7: a ux-declared policy's `cannot:` text used to be
1257
- // rendered unchanged even when the match was a fail-closed
1258
- // unclassified hit, not a genuine classification — so
1259
- // gate-prod-destructive's "You cannot run this critical
1260
- // destructive action against production." was shown verbatim for
1261
- // an unrecognized READ (e.g. `cat`/`sed -n`/`curl`) the moment the
1262
- // environment resolved to production, which is exactly the false
1263
- // positive this task exists to fix. When `whenUnclassifiedFallback`
1264
- // is set, a fallback-specific sentence is PREPENDED naming the real
1265
- // cause before the operator's own `cannot:` text, which is left
1266
- // byte-for-byte intact (added to, never replaced): the operator
1267
- // still chose that wording for the genuine-classification case,
1268
- // which keeps rendering unchanged. See docs/risk-gate.md,
1269
- // "Unclassified actions and the fail-close rule".
1270
- const uxText = renderAgentFacing(blockingPolicy.ux, {
1271
- ...blocking.extractValues,
1272
- SESSION_ID: sessionId,
1273
- });
1274
- reasonText = blocking.whenUnclassifiedFallback
1275
- ? `${unclassifiedFallbackPrefix(enriched?.environment.name, blockingPolicy.when?.["risk.severity_at_least"])} ${uxText}`
1276
- : uxText;
1277
- }
1278
- else {
1279
- const producersBlock = renderProducers(blockingPolicy?.producers, blocking.extractValues);
1280
- // M7: when the policy matched only because the action was
1281
- // unclassified (fail-closed), insert an operator-facing note so a
1282
- // deny caused by an unknown command is distinguishable from a deny
1283
- // caused by a genuine critical-severity classification. The note is
1284
- // placed after the base reason but BEFORE hintSuffix so the cause
1285
- // precedes the remedy ("why this fired" before "how to unblock").
1286
- // Neutral deny envelope only (not the ux path above).
1287
- const unclassifiedClause = blocking.whenUnclassifiedFallback
1288
- ? " (matched via the fail-closed unclassified rule, not a real risk classification)"
1289
- : "";
1290
- reasonText = `${blocking.policyName}: ${blocking.reason}.${unclassifiedClause}${hintSuffix}${producersBlock}`;
1291
- }
1292
- const block = {
1293
- decision: "block",
1294
- reason: reasonText,
1295
- };
1296
- // permissionDecision is documented for PreToolUse only; emitting the
1297
- // envelope on other events would invent a shape Claude Code does not
1298
- // define, so we restrict it strictly. Other event kinds still block
1299
- // via the top-level `decision: "block"`.
1300
- if (options.event.hook_event_name === "PreToolUse") {
1301
- block.hookSpecificOutput = {
1302
- hookEventName: "PreToolUse",
1303
- permissionDecision: "deny",
1304
- // Duplicates `reason` intentionally: legacy consumers read the
1305
- // top-level field, modern PreToolUse consumers read this one.
1306
- permissionDecisionReason: reasonText,
1307
- };
1308
- }
1309
- return { decisions, blockJson: block };
1310
- }
1311
- return { decisions, blockJson: null };
1312
- }
1313
- //# sourceMappingURL=intercept.js.map