failproofai 1.0.6-beta.1 → 1.0.7-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 (258) 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 +4 -4
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js +7 -5
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +6 -6
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +13 -11
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +7 -7
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +7 -6
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +41 -8
  63. package/.next/standalone/.next/server/app/settings/page.js +12 -9
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__1j-zsg5._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1_r2rbg._.js → [root-of-the-server]__1bf34x4._.js} +5 -7
  70. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  71. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  74. package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  76. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +5 -0
  79. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0576iuy._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +5 -0
  90. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +4 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
  98. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1x3j9mj._.js → [root-of-the-server]__1p28m0u._.js} +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1v185mo._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/ssr/{_0qlygw6._.js → _0cf1olg._.js} +2 -2
  107. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/{_1_ddpm3._.js → _135-c3y._.js} +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +5 -0
  110. package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
  111. package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/{_0h_douw._.js → _1w_5l7t._.js} +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +4 -4
  115. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  119. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/ssr/lib_utils_ts_0az0sfq._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
  122. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
  123. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  124. package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +5 -0
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +3 -0
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +12 -0
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +3 -0
  128. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +3 -0
  129. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  132. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  133. package/.next/standalone/.next/server/pages/404.html +1 -1
  134. package/.next/standalone/.next/server/pages/500.html +1 -1
  135. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  136. package/.next/standalone/.next/server/server-reference-manifest.json +56 -23
  137. package/.next/standalone/.next/static/chunks/{06fjx8-0_qufs.js → 00i9thnpht4l1.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/{3ht0xsco46pul.js → 0289oapk24zql.js} +1 -1
  139. package/.next/standalone/.next/static/chunks/{1qf2uiipatr80.js → 0pyy9m5cm8zku.js} +1 -1
  140. package/.next/standalone/.next/static/chunks/15heikmy3yjzc.js +1 -0
  141. package/.next/standalone/.next/static/chunks/285spx855h_3r.css +2 -0
  142. package/.next/standalone/.next/static/chunks/{02vwr5js1x28j.js → 2plg9qae1yejv.js} +1 -1
  143. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
  144. package/.next/standalone/.next/static/chunks/{2x776s0x3n_tu.js → 2tqszh84mp_2l.js} +1 -1
  145. package/.next/standalone/.next/static/chunks/34jpdb2xjm-x7.js +1 -0
  146. package/.next/standalone/.next/static/chunks/3cdkxrukvlvul.js +1 -0
  147. package/.next/standalone/.next/static/chunks/3dnd-_29uiaj3.js +6 -0
  148. package/.next/standalone/.next/static/chunks/{0y227_ohdqwvn.js → 3vp7c3knkh8mr.js} +1 -1
  149. package/.next/standalone/.next/static/chunks/{27_bwbg8sv6vj.js → 3vwnzfx9q80yl.js} +1 -1
  150. package/.next/standalone/app/actions/get-jev-config.ts +409 -0
  151. package/.next/standalone/app/actions/pack-actions.ts +12 -0
  152. package/.next/standalone/app/actions/update-jev-config.ts +420 -0
  153. package/.next/standalone/app/components/jev-notices.tsx +96 -0
  154. package/.next/standalone/app/policies/hooks-client.tsx +9 -0
  155. package/.next/standalone/app/settings/jev-panel.tsx +469 -0
  156. package/.next/standalone/app/settings/page.tsx +20 -1
  157. package/.next/standalone/app/settings/settings-client.tsx +27 -1
  158. package/.next/standalone/app/settings/settings.css +79 -0
  159. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +51 -3
  160. package/.next/standalone/fp-cloud-cli/README.md +1 -1
  161. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -1
  162. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +44 -0
  163. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +226 -5
  164. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +11 -0
  165. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +68 -1
  166. package/.next/standalone/fp-cloud-cli/pyproject.toml +1 -1
  167. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +17 -2
  168. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +7 -4
  169. package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +184 -0
  170. package/.next/standalone/fp-cloud-cli/uv.lock +3 -3
  171. package/.next/standalone/package.json +11 -11
  172. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  173. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  174. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +2 -0
  175. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +24 -1
  176. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +41 -6
  177. package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +88 -0
  178. package/.next/standalone/sdk/python/uv.lock +15 -15
  179. package/.next/standalone/server.js +1 -1
  180. package/bin/failproofai.mjs +115 -2
  181. package/dist/cli.mjs +10982 -3787
  182. package/dist/index.js +19 -1
  183. package/dist/worker.mjs +13761 -7834
  184. package/package.json +11 -11
  185. package/pi-extension/index.ts +11 -0
  186. package/scripts/build-policy-pack.mjs +39 -2
  187. package/src/hooks/builtin-policies.ts +21 -3
  188. package/src/hooks/cloud-enrollment-cli.ts +1 -1
  189. package/src/hooks/cloud-managed-policies.ts +22 -0
  190. package/src/hooks/custom-hooks-loader.ts +45 -7
  191. package/src/hooks/custom-hooks-registry.ts +45 -1
  192. package/src/hooks/daemon-client.ts +128 -50
  193. package/src/hooks/daemon-service.ts +30 -0
  194. package/src/hooks/effective-reviewers.ts +79 -0
  195. package/src/hooks/first-run-gate.ts +5 -0
  196. package/src/hooks/fp-home.ts +23 -0
  197. package/src/hooks/handler.ts +265 -6
  198. package/src/hooks/hook-activity-store.ts +105 -1
  199. package/src/hooks/hook-telemetry.ts +41 -0
  200. package/src/hooks/integrations.ts +3 -2
  201. package/src/hooks/jev-activity.ts +385 -0
  202. package/src/hooks/jev-cli.ts +1193 -0
  203. package/src/hooks/loader-utils.ts +6 -0
  204. package/src/hooks/manager.ts +4 -4
  205. package/src/hooks/pack-cli.ts +237 -11
  206. package/src/hooks/pack-manifest.ts +479 -7
  207. package/src/hooks/pack-store.ts +156 -10
  208. package/src/hooks/policy-authority.ts +333 -0
  209. package/src/hooks/policy-catalog.ts +65 -0
  210. package/src/hooks/policy-evaluator.ts +940 -796
  211. package/src/hooks/policy-registry.ts +25 -0
  212. package/src/hooks/policy-reviewability.ts +229 -0
  213. package/src/hooks/policy-types.ts +126 -0
  214. package/src/hooks/semantic/combine.ts +541 -0
  215. package/src/hooks/semantic/compile.ts +176 -0
  216. package/src/hooks/semantic/decide.ts +392 -0
  217. package/src/hooks/semantic/envelope.ts +1296 -0
  218. package/src/hooks/semantic/evaluator.ts +547 -0
  219. package/src/hooks/semantic/facts.ts +292 -0
  220. package/src/hooks/semantic/intent.ts +1190 -0
  221. package/src/hooks/semantic/jev-client.ts +643 -0
  222. package/src/hooks/semantic/jev-config.ts +594 -0
  223. package/src/hooks/semantic/jev-review.ts +374 -0
  224. package/src/hooks/semantic/jev-stats.ts +289 -0
  225. package/src/hooks/semantic/jev-throttle.ts +421 -0
  226. package/src/hooks/semantic/pack-policies.ts +251 -0
  227. package/src/hooks/semantic/policies.ts +596 -0
  228. package/src/hooks/semantic/precondition-names.ts +60 -0
  229. package/src/hooks/semantic/preconditions.ts +58 -0
  230. package/src/hooks/semantic/redact.ts +2910 -0
  231. package/src/hooks/semantic/types.ts +145 -0
  232. package/src/hooks/semver-precedence.ts +128 -0
  233. package/src/hooks/worker-server.ts +119 -26
  234. package/src/index.ts +6 -0
  235. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
  236. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
  237. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
  238. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
  239. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
  240. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
  243. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
  247. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
  249. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +0 -10
  250. package/.next/standalone/.next/static/chunks/0cbvytf8epn34.js +0 -1
  251. package/.next/standalone/.next/static/chunks/0qmsunv04x4na.css +0 -2
  252. package/.next/standalone/.next/static/chunks/1p4xauf-zu6zk.js +0 -1
  253. package/.next/standalone/.next/static/chunks/1uc_s_dftgxiq.js +0 -6
  254. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
  255. package/.next/standalone/.next/static/chunks/2lvr2pt70qn14.js +0 -1
  256. /package/.next/standalone/.next/static/{QCNYz2yCxxdeggg1k0iEq → qbVI48kgBOf9CTOKcFIk2}/_buildManifest.js +0 -0
  257. /package/.next/standalone/.next/static/{QCNYz2yCxxdeggg1k0iEq → qbVI48kgBOf9CTOKcFIk2}/_clientMiddlewareManifest.js +0 -0
  258. /package/.next/standalone/.next/static/{QCNYz2yCxxdeggg1k0iEq → qbVI48kgBOf9CTOKcFIk2}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.6-beta.1",
3
+ "version": "1.0.7-beta.0",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 39 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -81,14 +81,14 @@
81
81
  "access": "public"
82
82
  },
83
83
  "devDependencies": {
84
- "@anthropic-ai/sdk": "^0.125.0",
84
+ "@anthropic-ai/sdk": "^0.127.0",
85
85
  "@mdx-js/mdx": "^3.1.1",
86
86
  "@tailwindcss/postcss": "^4.3.1",
87
87
  "@tanstack/react-virtual": "^3.14.10",
88
88
  "@testing-library/jest-dom": "^7.0.0",
89
89
  "@testing-library/react": "^16.3.2",
90
90
  "@testing-library/user-event": "^14.6.5",
91
- "@types/node": "26.5.1",
91
+ "@types/node": "26.6.1",
92
92
  "@types/react": "19.3.0",
93
93
  "@types/react-dom": "^19.2.3",
94
94
  "@vitejs/plugin-react": "^6.1.0",
@@ -122,15 +122,15 @@
122
122
  "browserslist": "4.28.8"
123
123
  },
124
124
  "optionalDependencies": {
125
- "@failproofai/failproofaid-linux-x64": "1.0.6-beta.1",
126
- "@failproofai/failproofaid-linux-arm64": "1.0.6-beta.1",
127
- "@failproofai/failproofaid-darwin-x64": "1.0.6-beta.1",
128
- "@failproofai/failproofaid-darwin-arm64": "1.0.6-beta.1"
125
+ "@failproofai/failproofaid-linux-x64": "1.0.7-beta.0",
126
+ "@failproofai/failproofaid-linux-arm64": "1.0.7-beta.0",
127
+ "@failproofai/failproofaid-darwin-x64": "1.0.7-beta.0",
128
+ "@failproofai/failproofaid-darwin-arm64": "1.0.7-beta.0"
129
129
  },
130
130
  "failproofaidBinaries": {
131
- "linux-x64": "b6f877cd407c3322ff8ad2f98a096b6ea15b0bd1d63de6276c689368345c1cd2",
132
- "linux-arm64": "0da90a816b9dc707efe9b82176923cbf0f00026179c816e34eaad362ce2c0fa4",
133
- "darwin-x64": "bcbec488353fa04a1953a87cad15054e4796117cac4fd60b17fb1c87d62b7b1e",
134
- "darwin-arm64": "6830989a91a27642cdcc63fcfad529c69c0deb3f3abcf5ad40fd3516289b451d"
131
+ "linux-x64": "3ac234868fe9149179c32981da9ae1209371e98df7ffb09c6a4947664a3d6857",
132
+ "linux-arm64": "3603faac5741c321a63ba5a963e0cb4bb38c84663a63b53f764faae020116f90",
133
+ "darwin-x64": "c8a5b32a1e97b5a7f76ce4e42c78f0d566d108be65fdca3c6cd59c405f39b946",
134
+ "darwin-arm64": "255ac741c3ee3069591b1eb8bbf397baa0722ef240b83a8540b73f3f1b35722c"
135
135
  }
136
136
  }
@@ -404,6 +404,17 @@ export default function failproofaiBridge(pi: PiExtensionApi) {
404
404
  session_id: resolveSessionId(e.sessionId, resolveCwd(e.cwd)),
405
405
  cwd: resolveCwd(e.cwd),
406
406
  hook_event_name: "UserPromptSubmit",
407
+ // "interactive" | "rpc" | "extension": which channel the input came
408
+ // through, and the only mark Pi gives about who wrote the prompt. It
409
+ // names the channel, not the author — `pi -p "<text>"` reports
410
+ // `interactive` exactly as a person typing does — so the Jev evaluator
411
+ // uses it only in the direction it is reliable in (`piMachineTurn`,
412
+ // src/hooks/semantic/intent.ts): `extension`, another extension's
413
+ // `sendUserMessage()` whose text can be model-written or repo-derived,
414
+ // is refused, while `interactive`, `rpc` and an absent value are all
415
+ // recorded as the human's request. Policies read it too, and a later Pi
416
+ // build may say more here.
417
+ input_source: e.source,
407
418
  });
408
419
  if (decision.block) {
409
420
  console.error(`[failproofai] prompt blocked: ${decision.reason ?? "blocked by policy"}`);
@@ -31,6 +31,24 @@
31
31
  * that stops an agent disabling failproofai, it ships compiled in, and
32
32
  * `pack-manifest.ts` REFUSES any pack that declares `alwaysOn`. A pack containing
33
33
  * it would be rejected by our own loader — correctly.
34
+ *
35
+ * **Every policy states its authority** — whether Jev may clear its verdict —
36
+ * as the registry would resolve it (`manifestAuthority`), not as a copy of
37
+ * whatever the catalog happened to spell. A machine running this pack reads
38
+ * authority from the MANIFEST, so a field left off here is a policy that is
39
+ * silently hard everywhere it is installed. The entry passes the same fields to
40
+ * `customPolicies.add`, so a manifest rebuilt from the entry (`failproofai
41
+ * publish`) says the same thing.
42
+ *
43
+ * **It emits no `semantic` array, deliberately.** A pack that declares one
44
+ * REPLACES this build's compiled-in semantic set wholesale
45
+ * (`semantic/pack-policies.ts`), and this pack is the dress rehearsal for the
46
+ * regex floor leaving the package — not for Jev's question set leaving it. So
47
+ * the 39 builtins travel and the 16 semantic policies stay compiled in, which
48
+ * also keeps `manifestAuthority` below judging `reviewedBy` against the right
49
+ * list: the builtin reviewer names, which is what a machine installing this pack
50
+ * will still be running. The separate `FailproofAI/jev-policies` pack is where
51
+ * the semantic half ships, and it declares its own `minCliVersion`.
34
52
  */
35
53
  import { execFileSync } from "node:child_process";
36
54
  import { createHash } from "node:crypto";
@@ -40,6 +58,7 @@ import { fileURLToPath } from "node:url";
40
58
  // The one place the pack's id is decided. Imported rather than restated — see
41
59
  // PACK_ID below for what restating it cost.
42
60
  import { CORE_SOURCE } from "../src/hooks/pack-store.ts";
61
+ import { manifestAuthority } from "../src/hooks/policy-authority.ts";
43
62
 
44
63
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
45
64
  // `--out <dir>` lets the conformance test generate into a temp directory rather
@@ -95,6 +114,9 @@ for (const policy of BUILTIN_POLICIES) {
95
114
  defaultEnabled: policy.defaultEnabled === true,
96
115
  match: policy.match,
97
116
  fn: policy.fn,
117
+ // Whether Jev may clear it; see the note on authority at the top.
118
+ ...(policy.authority !== undefined ? { authority: policy.authority } : {}),
119
+ ...(policy.reviewedBy !== undefined ? { reviewedBy: policy.reviewedBy } : {}),
98
120
  });
99
121
  }
100
122
  `;
@@ -120,7 +142,20 @@ try {
120
142
  // validates these with the loader's own rules, so a shape the loader would
121
143
  // refuse fails this build rather than shipping.
122
144
  const { POLICY_CATALOG } = await import(join(ROOT, "src/hooks/policy-catalog.ts"));
123
- const policies = POLICY_CATALOG.filter((p) => !p.alwaysOn).map((p) => ({ ...p }));
145
+ const policies = POLICY_CATALOG.filter((p) => !p.alwaysOn).map((p) => {
146
+ // Throws on a declaration the registry would quietly downgrade: in the
147
+ // catalog that is a typo or a renamed semantic policy, and publishing it
148
+ // would turn a reviewed decision into an unannounced hard one. The fields
149
+ // come from the same call, so the check cannot be skipped without the
150
+ // manifest losing its authority too.
151
+ const resolved = manifestAuthority(p);
152
+ // Spread first so every field keeps its catalog position; only the two
153
+ // authority fields are replaced by their resolved values.
154
+ const entry = { ...p, authority: resolved.authority };
155
+ if (resolved.reviewedBy) entry.reviewedBy = resolved.reviewedBy;
156
+ else delete entry.reviewedBy;
157
+ return entry;
158
+ });
124
159
 
125
160
  const manifest = JSON.stringify(
126
161
  { id: PACK_ID, version, effect: "enforce", policies },
@@ -138,8 +173,10 @@ writeFileSync(
138
173
  );
139
174
 
140
175
  const omitted = POLICY_CATALOG.filter((p) => p.alwaysOn).map((p) => p.name);
176
+ const reviewable = policies.filter((p) => p.authority === "reviewable").length;
141
177
  console.log(
142
- `[policy-pack] ${PACK_ID}@${version} — ${policies.length} policies, ` +
178
+ `[policy-pack] ${PACK_ID}@${version} — ${policies.length} policies ` +
179
+ `(${reviewable} reviewable by Jev, ${policies.length - reviewable} hard), ` +
143
180
  `${(entryBytes.length / 1024).toFixed(1)} KB entry` +
144
181
  (omitted.length ? `; omitted alwaysOn: ${omitted.join(", ")}` : ""),
145
182
  );
@@ -2215,17 +2215,29 @@ const READ_LIKE_CMDS =
2215
2215
  * separator characters that appear in compound argv tokens (':' for Docker
2216
2216
  * volume mounts and PATH-like lists, '=' for env var assignments) so that a
2217
2217
  * suffix like '/dashboard.mdx' in 'docs/STAR/dashboard.mdx' or '/docs' in
2218
- * '-v HOST_DIR:/docs' is not misread as a standalone absolute path.
2218
+ * '-v HOST_DIR:/docs' is not misread as a standalone absolute path. '/' is in
2219
+ * that list too, so a match cannot START on the second slash of a protocol
2220
+ * separator: 'http://localhost:3000/x' used to yield '/localhost:3000/x'.
2221
+ * A match that starts on the FIRST slash is untouched, so '//etc/passwd' —
2222
+ * a real, working spelling of /etc/passwd — is still extracted and still
2223
+ * resolves to /etc/passwd.
2224
+ *
2225
+ * Runs of slashes with nothing else ('//', '///') are dropped: those are a
2226
+ * line-comment marker, not a directory. They used to resolve to the filesystem
2227
+ * root, which denied any read-like command carrying a '// …' comment — a
2228
+ * heredoc writing a TypeScript file into the project, say. A lone '/' is the
2229
+ * root and is kept.
2219
2230
  */
2220
2231
  function extractAbsolutePaths(command: string): string[] {
2221
2232
  const paths: string[] = [];
2222
- const pathRe = /(?<![a-zA-Z0-9_.\-~\\*?:=])(?:~\/[^\s;|&"'()\[\]{}]*|~(?=\s|$|[;|&"'()\[\]{}])|\/[^\s;|&"'()\[\]{}]*)/g;
2233
+ const pathRe = /(?<![a-zA-Z0-9_.\-~\\*?:=/])(?:~\/[^\s;|&"'()\[\]{}]*|~(?=\s|$|[;|&"'()\[\]{}])|\/[^\s;|&"'()\[\]{}]*)/g;
2223
2234
 
2224
2235
  function addPaths(s: string): void {
2225
2236
  pathRe.lastIndex = 0;
2226
2237
  let m: RegExpExecArray | null;
2227
2238
  while ((m = pathRe.exec(s)) !== null) {
2228
2239
  let p = m[0];
2240
+ if (/^\/{2,}$/.test(p)) continue; // a `//` comment marker, not the root
2229
2241
  if (p === "~") p = homedir();
2230
2242
  else if (p.startsWith("~/")) p = join(homedir(), p.slice(2));
2231
2243
  paths.push(p);
@@ -3178,7 +3190,13 @@ export function registerBuiltinPolicies(enabledNames: string[]): void {
3178
3190
  // A guard against the agent disabling failproofai that any of those can
3179
3191
  // switch off is not a guard.
3180
3192
  if (policy.alwaysOn || enabledSet.has(normalizePolicyName(policy.name))) {
3181
- registerPolicy(policy.name, policy.description, policy.fn, policy.match, 0, policy.params);
3193
+ // `alwaysOn` travels with the declaration so the self-protection guard
3194
+ // resolves to hard even if its catalog entry ever said otherwise.
3195
+ registerPolicy(policy.name, policy.description, policy.fn, policy.match, 0, policy.params, {
3196
+ authority: policy.authority,
3197
+ reviewedBy: policy.reviewedBy,
3198
+ alwaysOn: policy.alwaysOn,
3199
+ });
3182
3200
  }
3183
3201
  }
3184
3202
  }
@@ -377,7 +377,7 @@ export function versionStatusLines(): string[] {
377
377
  const daemon = recorded?.daemon ?? "not installed";
378
378
  return [
379
379
  `CLI ${cliVersion} · daemon ${daemon}${skew ? " (STALE)" : ""} · layout ${recorded?.layout ?? "-"}`,
380
- ...(skew ? [" Run `failproofai config` to update the daemon."] : []),
380
+ ...(skew ? [" Run `failproofai update` to update the daemon."] : []),
381
381
  ];
382
382
  }
383
383
 
@@ -8,6 +8,8 @@ import { createHash } from "node:crypto";
8
8
  import { existsSync, readFileSync, realpathSync, rmSync } from "node:fs";
9
9
  import { isAbsolute, relative, resolve } from "node:path";
10
10
  import { cloudPoliciesDir } from "./fp-home";
11
+ import { authorityFieldsOf } from "./policy-authority";
12
+ import type { PolicyAuthority } from "./policy-types";
11
13
 
12
14
  /**
13
15
  * Active-manifest schema versions this reader accepts.
@@ -44,6 +46,19 @@ export interface CloudManagedPolicyArtifact {
44
46
  sha256: string;
45
47
  path: string;
46
48
  deployment: number;
49
+ /**
50
+ * Whether Jev may clear this policy's verdict once it is configured, as the
51
+ * ASSIGNMENT declares it — code inside the artifact cannot grant itself this.
52
+ * Absent means `hard`, which is what keeps Jev from weakening central
53
+ * enforcement by default: a cloud policy is only ever reviewable because
54
+ * whoever deployed it said so. Applies to every hook the artifact registers.
55
+ *
56
+ * Read and validated here; the daemon has to carry it into `active.json`
57
+ * (`ActivePolicy` in `cloud_policies.rs`) before a deployment can set it.
58
+ */
59
+ authority?: PolicyAuthority;
60
+ /** The semantic policies that must all be asked and none answer `deny`; see `authority`. */
61
+ reviewedBy?: string[];
47
62
  }
48
63
 
49
64
  interface ActiveManifest {
@@ -55,6 +70,8 @@ interface ActiveManifest {
55
70
  effect?: string;
56
71
  sha256: string;
57
72
  path: string;
73
+ authority?: unknown;
74
+ reviewedBy?: unknown;
58
75
  }>;
59
76
  }
60
77
 
@@ -206,6 +223,11 @@ export function readActiveCloudManagedPolicies(): CloudManagedPolicyArtifact[] {
206
223
  sha256: policy.sha256,
207
224
  path,
208
225
  deployment: manifest.deployment,
226
+ // Unlike `effect`, a malformed authority is dropped rather than refused:
227
+ // dropping it makes this policy `hard`, the default that keeps enforcing,
228
+ // while refusing would take the whole deployment down over an optional
229
+ // field. Absent stays absent.
230
+ ...authorityFieldsOf(policy as unknown as Record<string, unknown>),
209
231
  };
210
232
  });
211
233
  }
@@ -33,6 +33,7 @@ import type { CustomHook, PolicyCatalogEntry } from "./policy-types";
33
33
  import type { CloudManagedPolicyArtifact } from "./cloud-managed-policies";
34
34
  import type { ResolvedPack } from "./pack-manifest";
35
35
  import { customPoliciesDir, shimsDir } from "./fp-home";
36
+ import { warnAuthority, withMergedAuthority } from "./policy-authority";
36
37
 
37
38
  const LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
38
39
 
@@ -448,9 +449,19 @@ export async function loadAllCustomHooks(
448
449
  `cloud-managed policies ${existing.id} and ${policy.id} have identical source, so they share ` +
449
450
  `one artifact and load as one policy; enforcing it if either asks to enforce`,
450
451
  );
451
- if (existing.effect !== "enforce" && policy.effect === "enforce") {
452
- cloudManagedByPath.set(key, policy);
452
+ // Authority resolves toward HARD, for the same reason: reviewable only if
453
+ // every assignment behind these bytes says so. Keeping one record's
454
+ // declaration let the order of active.json decide, so a team's reviewable
455
+ // assignment could clear what an org-wide hard one enforces.
456
+ const winner = existing.effect !== "enforce" && policy.effect === "enforce" ? policy : existing;
457
+ const { merged, overruled } = withMergedAuthority(winner, [existing, policy]);
458
+ if (overruled) {
459
+ warnAuthority(
460
+ `cloud-managed policies ${existing.id} and ${policy.id} share one artifact and do not all declare it ` +
461
+ `reviewable, so it stays hard`,
462
+ );
453
463
  }
464
+ cloudManagedByPath.set(key, merged);
454
465
  }
455
466
 
456
467
  // Installed packs, keyed by artifact path — the same content-addressing, and
@@ -482,14 +493,34 @@ export async function loadAllCustomHooks(
482
493
  *
483
494
  * Winner precedence on a name both declare, because the merged record carries
484
495
  * the winner's id and version and there is no merging two different defaults.
496
+ *
497
+ * Except AUTHORITY, which resolves toward hard like the effect resolves toward
498
+ * enforce (`withMergedAuthority`): a name is reviewable only if every pack
499
+ * behind the artifact declares it reviewable. A pack whose manifest does not
500
+ * list the name at all counts as hard — its copy of the artifact registers
501
+ * the policy all the same, undeclared, and an undeclared pack policy is hard.
502
+ * Winner precedence here would let one pack's manifest make another pack's
503
+ * policy reviewable, which is the one thing a manifest may never do.
485
504
  */
486
505
  const unionCatalog = (
487
506
  winner: PolicyCatalogEntry[],
488
507
  other: PolicyCatalogEntry[],
489
- ): PolicyCatalogEntry[] => [
490
- ...winner,
491
- ...other.filter((p) => !winner.some((w) => w.name === p.name)),
492
- ];
508
+ ): { policies: PolicyCatalogEntry[]; overruled: string[] } => {
509
+ /** Names a pack asked to be reviewable and did not get, for the warning. */
510
+ const overruled: string[] = [];
511
+ const harden = (entry: PolicyCatalogEntry, peer: PolicyCatalogEntry | undefined) => {
512
+ const r = withMergedAuthority(entry, [entry, peer ?? {}]);
513
+ if (r.overruled) overruled.push(entry.name);
514
+ return r.merged;
515
+ };
516
+ return {
517
+ policies: [
518
+ ...winner.map((w) => harden(w, other.find((p) => p.name === w.name))),
519
+ ...other.filter((p) => !winner.some((w) => w.name === p.name)).map((p) => harden(p, undefined)),
520
+ ],
521
+ overruled,
522
+ };
523
+ };
493
524
 
494
525
  const packByPath = new Map<string, ResolvedPack>();
495
526
  /**
@@ -535,12 +566,19 @@ export async function loadAllCustomHooks(
535
566
  // being intersected away.
536
567
  const winner = existing.effect !== "enforce" && pack.effect === "enforce" ? pack : existing;
537
568
  const other = winner === existing ? pack : existing;
569
+ const catalog = unionCatalog(winner.policies, other.policies);
538
570
  packByPath.set(key, {
539
571
  ...winner,
540
- policies: unionCatalog(winner.policies, other.policies),
572
+ policies: catalog.policies,
541
573
  enabled: unionSelection(winner.enabled, other.enabled),
542
574
  clis: unionSelection(winner.clis, other.clis),
543
575
  });
576
+ if (catalog.overruled.length > 0) {
577
+ warnAuthority(
578
+ `packs ${existing.id} and ${pack.id} share one artifact and do not all declare ` +
579
+ `${catalog.overruled.join(", ")} reviewable, so ${catalog.overruled.length === 1 ? "it stays" : "they stay"} hard`,
580
+ );
581
+ }
544
582
  }
545
583
 
546
584
  // 1. Explicit custom policy paths. Accept a string for callers/configs using
@@ -4,9 +4,14 @@
4
4
  * from 'failproofai') and the hook handler (which reads from this file) share the
5
5
  * same in-process state when running within the same Node.js process.
6
6
  */
7
- import type { CustomHook } from "./policy-types";
7
+ import type { CustomHook, SemanticPolicyDeclaration } from "./policy-types";
8
8
 
9
9
  const REGISTRY_KEY = "__failproofai_custom_hooks__";
10
+ /**
11
+ * Beside the hook registry, on the same key namespace and cleared by the same
12
+ * call — the two are one registration pass and a build step reads both.
13
+ */
14
+ const SEMANTIC_KEY = "__failproofai_semantic_policies__";
10
15
 
11
16
  function getRegistry(): CustomHook[] {
12
17
  const g = globalThis as Record<string, unknown>;
@@ -14,17 +19,56 @@ function getRegistry(): CustomHook[] {
14
19
  return g[REGISTRY_KEY] as CustomHook[];
15
20
  }
16
21
 
22
+ function getSemanticRegistry(): SemanticPolicyDeclaration[] {
23
+ const g = globalThis as Record<string, unknown>;
24
+ if (!Array.isArray(g[SEMANTIC_KEY])) g[SEMANTIC_KEY] = [];
25
+ return g[SEMANTIC_KEY] as SemanticPolicyDeclaration[];
26
+ }
27
+
17
28
  export const customPolicies = {
18
29
  add(hook: CustomHook): void {
19
30
  getRegistry().push(hook);
20
31
  },
21
32
  };
22
33
 
34
+ /**
35
+ * Register a semantic (Jev) policy — a question set for the classifier, read by
36
+ * `failproofai publish` and emitted as the pack manifest's `semantic` array.
37
+ *
38
+ * A SEPARATE namespace from `customPolicies`, deliberately, rather than a
39
+ * `customPolicies.addSemantic`. A semantic policy has no `fn` and no `match`,
40
+ * and nothing about it ever executes on a user's machine: what installs is the
41
+ * declaration in the manifest, and the artifact's call here matters only at
42
+ * build time. Putting it behind the object whose entries return
43
+ * `allow()`/`deny()` invites exactly the shape confusion the manifest parser
44
+ * would then have to catch — a function on a semantic entry, a probe list on a
45
+ * regex one — and both mistakes would be silent until somebody read a manifest.
46
+ *
47
+ * Nothing is validated here. `parsePackSemanticPolicy` in `pack-manifest.ts`
48
+ * owns every rule, and it is applied at BUILD time (where the author can still
49
+ * fix it) and again at load time (where the bytes may have come from anywhere).
50
+ * Validating a third time in the setter would be a third copy of the rules.
51
+ */
52
+ export const semanticPolicies = {
53
+ add(policy: SemanticPolicyDeclaration): void {
54
+ getSemanticRegistry().push(policy);
55
+ },
56
+ };
57
+
23
58
  export function getCustomHooks(): CustomHook[] {
24
59
  return getRegistry();
25
60
  }
26
61
 
62
+ export function getSemanticRegistrations(): SemanticPolicyDeclaration[] {
63
+ return getSemanticRegistry();
64
+ }
65
+
27
66
  export function clearCustomHooks(): void {
28
67
  const g = globalThis as Record<string, unknown>;
29
68
  g[REGISTRY_KEY] = [];
69
+ // Both maps, on one call. A loader that cleared only the hooks would carry
70
+ // one entry file's semantic policies into the next file's build — and since
71
+ // the semantic half is read after the load rather than during it, the extras
72
+ // would be published under the wrong pack with nothing to notice.
73
+ g[SEMANTIC_KEY] = [];
30
74
  }
@@ -93,6 +93,26 @@ export type DaemonAttempt =
93
93
  | { ok: true; response: DaemonHookResponse }
94
94
  | { ok: false; failure: DaemonFailure };
95
95
 
96
+ export interface DaemonPolicyEvaluationRequest {
97
+ integration: IntegrationType;
98
+ event: string;
99
+ payload: Record<string, unknown>;
100
+ cwd?: string;
101
+ }
102
+
103
+ export type DaemonPolicyEvaluationAttempt =
104
+ | { ok: true }
105
+ | { ok: false; failure: DaemonFailure };
106
+
107
+ interface DaemonRequestOptions {
108
+ /** Override the response budget; the connect budget is never relaxed. */
109
+ responseTimeoutMs?: number;
110
+ }
111
+
112
+ type DaemonWireAttempt =
113
+ | { ok: true; message: Record<string, unknown> }
114
+ | { ok: false; failure: DaemonFailure };
115
+
96
116
  /**
97
117
  * Whether a daemon is listening, regardless of how it was started.
98
118
  *
@@ -176,29 +196,22 @@ function encodeFrame(value: unknown): Buffer {
176
196
  return Buffer.concat([header, body]);
177
197
  }
178
198
 
179
- /** Attempts a daemon evaluation while preserving the failure category. */
180
- export async function attemptDaemonHook(
181
- req: DaemonHookRequest,
182
- opts?: {
183
- /**
184
- * Override the RESPONSE budget only — the connect budget is never
185
- * relaxed. Used by the health probe (`probeDaemonEndToEnd`), which runs
186
- * from an interactive command rather than a hook and must not make a
187
- * person wait out the full 30s hook budget to be told their daemon is
188
- * broken. Never set this on the hook path: 30s is matched to the
189
- * daemon's own read timeout so this side never gives up on a request the
190
- * daemon is still honestly working on.
191
- */
192
- responseTimeoutMs?: number;
193
- },
194
- ): Promise<DaemonAttempt> {
199
+ /**
200
+ * Sends one framed daemon request while preserving the failure category.
201
+ * Shape validation belongs to the typed caller: both shell hooks and native
202
+ * integrations share the transport, but require different response variants.
203
+ */
204
+ async function attemptDaemonRequest(
205
+ request: Record<string, unknown>,
206
+ opts?: DaemonRequestOptions,
207
+ ): Promise<DaemonWireAttempt> {
195
208
  // Windows never has a daemon in this phase (see the plan's platform
196
209
  // scope) — skip the attempt outright rather than depending on however
197
210
  // Node happens to behave when handed a POSIX socket path on Windows.
198
211
  if (process.platform === "win32") return { ok: false, failure: "unreachable" };
199
212
  const responseBudget = opts?.responseTimeoutMs ?? DAEMON_RESPONSE_TIMEOUT_MS;
200
213
 
201
- return new Promise<DaemonAttempt>((resolvePromise) => {
214
+ return new Promise<DaemonWireAttempt>((resolvePromise) => {
202
215
  let settled = false;
203
216
  let timer: ReturnType<typeof setTimeout>;
204
217
  const arm = (ms: number) => {
@@ -210,7 +223,7 @@ export async function attemptDaemonHook(
210
223
  // process lingers if something upstream forgets to await us.
211
224
  timer.unref?.();
212
225
  };
213
- const finish = (result: DaemonAttempt) => {
226
+ const finish = (result: DaemonWireAttempt) => {
214
227
  if (settled) return;
215
228
  settled = true;
216
229
  clearTimeout(timer);
@@ -230,16 +243,7 @@ export async function attemptDaemonHook(
230
243
  // Connected: the daemon is demonstrably reachable, so the question is
231
244
  // no longer "is it there" but "how long does this evaluation take".
232
245
  arm(responseBudget);
233
- socket.write(
234
- encodeFrame({
235
- type: "hook",
236
- protocolVersion: PROTOCOL_VERSION,
237
- hookEvent: req.hookEvent,
238
- cli: req.cli,
239
- stdin: req.stdin,
240
- cwd: req.cwd,
241
- }),
242
- );
246
+ socket.write(encodeFrame(request));
243
247
  });
244
248
 
245
249
  socket.on("data", (chunk: Buffer) => {
@@ -258,13 +262,18 @@ export async function attemptDaemonHook(
258
262
  if (recvBuf.length < declaredLen) return;
259
263
 
260
264
  const body = recvBuf.subarray(0, declaredLen);
261
- let message: Record<string, unknown>;
265
+ let parsed: unknown;
262
266
  try {
263
- message = JSON.parse(body.toString("utf8")) as Record<string, unknown>;
267
+ parsed = JSON.parse(body.toString("utf8"));
264
268
  } catch {
265
269
  fail("unreachable");
266
270
  return;
267
271
  }
272
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
273
+ fail("unreachable");
274
+ return;
275
+ }
276
+ const message = parsed as Record<string, unknown>;
268
277
 
269
278
  if (message.protocolVersion !== PROTOCOL_VERSION) {
270
279
  // Catches BOTH directions. A newer CLI sends v2 and the daemon replies
@@ -274,29 +283,98 @@ export async function attemptDaemonHook(
274
283
  fail("protocol-mismatch");
275
284
  return;
276
285
  }
277
- if (
278
- message.type === "hookResult" &&
279
- typeof message.exitCode === "number" &&
280
- typeof message.stdout === "string" &&
281
- typeof message.stderr === "string"
282
- ) {
283
- finish({
284
- ok: true,
285
- response: {
286
- exitCode: message.exitCode,
287
- stdout: message.stdout,
288
- stderr: message.stderr,
289
- },
290
- });
291
- return;
292
- }
293
- // Anything else — an explicit `error` message at a MATCHING protocol
294
- // version, a `pong` (protocol confusion), or a well-formed-but-wrong-shape
295
- // body — is treated identically to a connection failure: no partial trust.
296
- fail("unreachable");
286
+ finish({ ok: true, message });
297
287
  });
298
288
 
299
289
  socket.on("error", () => fail("unreachable"));
300
290
  socket.on("close", () => fail("unreachable"));
301
291
  });
302
292
  }
293
+
294
+ /** Attempts a shell-hook daemon evaluation while preserving the failure category. */
295
+ export async function attemptDaemonHook(
296
+ req: DaemonHookRequest,
297
+ opts?: DaemonRequestOptions,
298
+ ): Promise<DaemonAttempt> {
299
+ const attempt = await attemptDaemonRequest(
300
+ {
301
+ type: "hook",
302
+ protocolVersion: PROTOCOL_VERSION,
303
+ hookEvent: req.hookEvent,
304
+ cli: req.cli,
305
+ stdin: req.stdin,
306
+ cwd: req.cwd,
307
+ },
308
+ opts,
309
+ );
310
+ if (!attempt.ok) return attempt;
311
+
312
+ const message = attempt.message;
313
+ if (
314
+ message.type === "hookResult" &&
315
+ typeof message.exitCode === "number" &&
316
+ typeof message.stdout === "string" &&
317
+ typeof message.stderr === "string"
318
+ ) {
319
+ return {
320
+ ok: true,
321
+ response: {
322
+ exitCode: message.exitCode,
323
+ stdout: message.stdout,
324
+ stderr: message.stderr,
325
+ },
326
+ };
327
+ }
328
+
329
+ // An explicit matching-version error, pong, or malformed hook result is not
330
+ // an evaluation. Never grant partial trust to a daemon that answered the
331
+ // connection but not the request we sent.
332
+ return { ok: false, failure: "unreachable" };
333
+ }
334
+
335
+ function isStringArray(value: unknown): value is string[] {
336
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
337
+ }
338
+
339
+ /**
340
+ * Attempts the structured request used by native in-process integrations.
341
+ * A matching protocol version is not sufficient: pre-native v1 daemons speak
342
+ * `hook` but cannot deserialize `policyEvaluation`, so only a complete,
343
+ * well-shaped `policyResult` proves this capability exists.
344
+ */
345
+ export async function attemptDaemonPolicyEvaluation(
346
+ req: DaemonPolicyEvaluationRequest,
347
+ opts?: DaemonRequestOptions,
348
+ ): Promise<DaemonPolicyEvaluationAttempt> {
349
+ const attempt = await attemptDaemonRequest(
350
+ {
351
+ type: "policyEvaluation",
352
+ protocolVersion: PROTOCOL_VERSION,
353
+ integration: req.integration,
354
+ event: req.event,
355
+ payload: req.payload,
356
+ cwd: req.cwd,
357
+ },
358
+ opts,
359
+ );
360
+ if (!attempt.ok) return attempt;
361
+
362
+ const message = attempt.message;
363
+ const validOptionalString = (value: unknown) =>
364
+ value === null || value === undefined || typeof value === "string";
365
+ if (
366
+ message.type === "policyResult" &&
367
+ (message.decision === "allow" || message.decision === "deny" || message.decision === "instruct") &&
368
+ isStringArray(message.policyNames) &&
369
+ validOptionalString(message.reason) &&
370
+ isStringArray(message.matchedPolicies) &&
371
+ typeof message.durationMs === "number" &&
372
+ Number.isInteger(message.durationMs) &&
373
+ message.durationMs >= 0 &&
374
+ validOptionalString(message.toolName)
375
+ ) {
376
+ return { ok: true };
377
+ }
378
+
379
+ return { ok: false, failure: "unreachable" };
380
+ }