failproofai 1.0.1 → 1.0.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (390) 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 +6 -6
  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 +2 -2
  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 +2 -2
  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 +3 -2
  24. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -4
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -3
  31. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
  33. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
  35. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  38. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  39. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/index.html +1 -1
  42. package/.next/standalone/.next/server/app/index.rsc +15 -15
  43. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  44. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/page.js +2 -2
  48. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +66 -22
  51. package/.next/standalone/.next/server/app/policies/page.js +5 -4
  52. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  64. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  65. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  68. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  69. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/chunks/{[externals]__1qzqcio._.js → [externals]__0e32-7c._.js} +1 -1
  72. package/.next/standalone/.next/server/chunks/[externals]__1s462yl._.js +3 -0
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -6
  84. package/.next/standalone/.next/server/chunks/lib_telemetry-id_ts_1k8gj9o._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +12 -0
  89. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
  93. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0spkm68._.js → [root-of-the-server]__07gm3zl._.js} +21 -20
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0l4748x._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +5 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
  102. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/_0bwu160._.js +23 -0
  104. package/.next/standalone/.next/server/chunks/ssr/{_0u5zauv._.js → _0ns6k3a._.js} +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +13 -13
  108. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_08o7bqo._.js +10 -0
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  114. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  115. package/.next/standalone/.next/server/middleware.js +1 -1
  116. package/.next/standalone/.next/server/pages/404.html +1 -1
  117. package/.next/standalone/.next/server/pages/500.html +1 -1
  118. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.json +71 -27
  120. package/.next/standalone/.next/static/chunks/{3c808qrfcq0im.js → 01u1cf8js2yd6.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{0k5050ezjvv49.js → 0izgu7nqh2zp-.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/0u4-org_e7zde.js +1 -0
  123. package/.next/standalone/.next/static/chunks/1mfzsja6ftim5.css +2 -0
  124. package/.next/standalone/.next/static/chunks/{2zpp1we2jv6tb.js → 1tynnrrh711lr.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{1ide1wl2f7zi1.js → 22rqvlr7p_1mv.js} +13 -13
  126. package/.next/standalone/.next/static/chunks/{31s1smp-g7gks.js → 2whxqyskpirbk.js} +1 -1
  127. package/.next/standalone/.next/static/chunks/31mwk8k0q3_3g.js +6 -0
  128. package/.next/standalone/.next/static/chunks/3ms6gxfm82t6s.js +1 -0
  129. package/.next/standalone/.next/static/chunks/{37fcpc36x9gpe.js → 3tbbs_d8xrvyp.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/43nlc236oo898.js +1 -0
  131. package/.next/standalone/SECURITY.md +5 -4
  132. package/.next/standalone/app/actions/get-hooks-config.ts +108 -17
  133. package/.next/standalone/app/actions/pack-actions.ts +163 -0
  134. package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
  135. package/.next/standalone/app/audit/_components/share-templates.ts +13 -13
  136. package/.next/standalone/app/policies/hooks-client.tsx +320 -15
  137. package/.next/standalone/components/navbar.tsx +2 -28
  138. package/.next/standalone/components/reach-developers.tsx +8 -1
  139. package/.next/standalone/docs-old/agenteye/python-sdk-skill.mdx +1 -1
  140. package/.next/standalone/docs-old/agenteye/python-sdk.mdx +59 -35
  141. package/.next/standalone/docs-old/i18n/README.ar.md +1 -0
  142. package/.next/standalone/docs-old/i18n/README.de.md +1 -0
  143. package/.next/standalone/docs-old/i18n/README.es.md +1 -0
  144. package/.next/standalone/docs-old/i18n/README.fr.md +1 -0
  145. package/.next/standalone/docs-old/i18n/README.he.md +1 -0
  146. package/.next/standalone/docs-old/i18n/README.hi.md +1 -0
  147. package/.next/standalone/docs-old/i18n/README.it.md +1 -0
  148. package/.next/standalone/docs-old/i18n/README.ja.md +1 -0
  149. package/.next/standalone/docs-old/i18n/README.ko.md +1 -0
  150. package/.next/standalone/docs-old/i18n/README.pt-br.md +1 -0
  151. package/.next/standalone/docs-old/i18n/README.ru.md +1 -0
  152. package/.next/standalone/docs-old/i18n/README.tr.md +1 -0
  153. package/.next/standalone/docs-old/i18n/README.vi.md +1 -0
  154. package/.next/standalone/docs-old/i18n/README.zh.md +1 -0
  155. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +431 -0
  156. package/.next/standalone/fp-cloud-cli/LICENSE +42 -0
  157. package/.next/standalone/fp-cloud-cli/MANIFEST.in +10 -0
  158. package/.next/standalone/fp-cloud-cli/README.md +259 -0
  159. package/.next/standalone/fp-cloud-cli/fp_cli/__init__.py +10 -0
  160. package/.next/standalone/fp-cloud-cli/fp_cli/__main__.py +4 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +64 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/_context.py +332 -0
  163. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -0
  164. package/.next/standalone/fp-cloud-cli/fp_cli/analytics.py +432 -0
  165. package/.next/standalone/fp-cloud-cli/fp_cli/analytics_config.py +77 -0
  166. package/.next/standalone/fp-cloud-cli/fp_cli/analytics_registry.py +83 -0
  167. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +492 -0
  168. package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +160 -0
  169. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1694 -0
  170. package/.next/standalone/fp-cloud-cli/fp_cli/commands/__init__.py +0 -0
  171. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +214 -0
  172. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +407 -0
  173. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +445 -0
  174. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +1054 -0
  175. package/.next/standalone/fp-cloud-cli/fp_cli/commands/auth_cmds.py +512 -0
  176. package/.next/standalone/fp-cloud-cli/fp_cli/commands/errors_cmds.py +190 -0
  177. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evals_cmds.py +161 -0
  178. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +159 -0
  179. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +416 -0
  180. package/.next/standalone/fp-cloud-cli/fp_cli/commands/guardrails_cmds.py +148 -0
  181. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +472 -0
  182. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +407 -0
  183. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +63 -0
  184. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +319 -0
  185. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +499 -0
  186. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +378 -0
  187. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +151 -0
  188. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +150 -0
  189. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +35 -0
  190. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +404 -0
  191. package/.next/standalone/fp-cloud-cli/fp_cli/config.py +330 -0
  192. package/.next/standalone/fp-cloud-cli/fp_cli/dates.py +78 -0
  193. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +345 -0
  194. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +98 -0
  195. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +891 -0
  196. package/.next/standalone/fp-cloud-cli/fp_cli/orgs.py +30 -0
  197. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +6593 -0
  198. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +208 -0
  199. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +290 -0
  200. package/.next/standalone/fp-cloud-cli/fp_cli/py.typed +0 -0
  201. package/.next/standalone/fp-cloud-cli/fp_cli/select.py +322 -0
  202. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +53 -0
  203. package/.next/standalone/fp-cloud-cli/pyproject.toml +106 -0
  204. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +302 -0
  205. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +8 -0
  206. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +254 -0
  207. package/.next/standalone/fp-cloud-cli/tests/__init__.py +0 -0
  208. package/.next/standalone/fp-cloud-cli/tests/conftest.py +92 -0
  209. package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +644 -0
  210. package/.next/standalone/fp-cloud-cli/tests/test_analytics.py +385 -0
  211. package/.next/standalone/fp-cloud-cli/tests/test_audits.py +950 -0
  212. package/.next/standalone/fp-cloud-cli/tests/test_auth.py +98 -0
  213. package/.next/standalone/fp-cloud-cli/tests/test_auth_mode.py +272 -0
  214. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +118 -0
  215. package/.next/standalone/fp-cloud-cli/tests/test_client.py +299 -0
  216. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +814 -0
  217. package/.next/standalone/fp-cloud-cli/tests/test_config.py +99 -0
  218. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +340 -0
  219. package/.next/standalone/fp-cloud-cli/tests/test_dates.py +76 -0
  220. package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +481 -0
  221. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +170 -0
  222. package/.next/standalone/fp-cloud-cli/tests/test_failproofai_home.py +722 -0
  223. package/.next/standalone/fp-cloud-cli/tests/test_fp_home_contract.py +198 -0
  224. package/.next/standalone/fp-cloud-cli/tests/test_hardening.py +186 -0
  225. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +127 -0
  226. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +496 -0
  227. package/.next/standalone/fp-cloud-cli/tests/test_list.py +59 -0
  228. package/.next/standalone/fp-cloud-cli/tests/test_multivalue.py +183 -0
  229. package/.next/standalone/fp-cloud-cli/tests/test_no_customer_identifiers.py +168 -0
  230. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +492 -0
  231. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +692 -0
  232. package/.next/standalone/fp-cloud-cli/tests/test_output.py +1732 -0
  233. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +191 -0
  234. package/.next/standalone/fp-cloud-cli/tests/test_readme_matches_reality.py +125 -0
  235. package/.next/standalone/fp-cloud-cli/tests/test_review_fixes.py +161 -0
  236. package/.next/standalone/fp-cloud-cli/tests/test_telemetry_completeness.py +90 -0
  237. package/.next/standalone/fp-cloud-cli/tests/test_usage.py +115 -0
  238. package/.next/standalone/fp-cloud-cli/tests/test_v1_origin_diagnostic.py +70 -0
  239. package/.next/standalone/fp-cloud-cli/tests/test_v1_routing.py +214 -0
  240. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +104 -0
  241. package/.next/standalone/fp-cloud-cli/uv.lock +552 -0
  242. package/.next/standalone/package.json +27 -21
  243. package/.next/standalone/sdk/python/CHANGELOG.md +70 -0
  244. package/.next/standalone/sdk/python/LICENSE +42 -0
  245. package/.next/standalone/sdk/python/MANIFEST.in +21 -0
  246. package/.next/standalone/sdk/python/README.md +466 -0
  247. package/.next/standalone/sdk/python/docs/README.md +249 -0
  248. package/.next/standalone/sdk/python/docs/_shared/README.md +43 -0
  249. package/.next/standalone/sdk/python/docs/_shared/__init__.py +195 -0
  250. package/.next/standalone/sdk/python/docs/crewai/README.md +328 -0
  251. package/.next/standalone/sdk/python/docs/crewai/examples/quickstart.py +61 -0
  252. package/.next/standalone/sdk/python/docs/crewai/examples/research_crew.py +92 -0
  253. package/.next/standalone/sdk/python/docs/langgraph/README.md +481 -0
  254. package/.next/standalone/sdk/python/docs/langgraph/examples/quickstart.py +51 -0
  255. package/.next/standalone/sdk/python/docs/langgraph/examples/research_agent.py +86 -0
  256. package/.next/standalone/sdk/python/docs/langgraph/examples/supervisor_handoff.py +83 -0
  257. package/.next/standalone/sdk/python/docs/llama_index/README.md +332 -0
  258. package/.next/standalone/sdk/python/docs/llama_index/examples/quickstart.py +62 -0
  259. package/.next/standalone/sdk/python/docs/llama_index/examples/research_agent.py +93 -0
  260. package/.next/standalone/sdk/python/docs/manual/README.md +491 -0
  261. package/.next/standalone/sdk/python/docs/manual/examples/quickstart.py +57 -0
  262. package/.next/standalone/sdk/python/docs/manual/examples/research_agent.py +133 -0
  263. package/.next/standalone/sdk/python/docs/pydantic_ai/README.md +323 -0
  264. package/.next/standalone/sdk/python/docs/pydantic_ai/examples/quickstart.py +48 -0
  265. package/.next/standalone/sdk/python/docs/pydantic_ai/examples/research_agent.py +94 -0
  266. package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +131 -0
  267. package/.next/standalone/sdk/python/failproofai_sdk/_context.py +218 -0
  268. package/.next/standalone/sdk/python/failproofai_sdk/_environment.py +78 -0
  269. package/.next/standalone/sdk/python/failproofai_sdk/_events.py +752 -0
  270. package/.next/standalone/sdk/python/failproofai_sdk/_resolver.py +118 -0
  271. package/.next/standalone/sdk/python/failproofai_sdk/_runtime.py +21 -0
  272. package/.next/standalone/sdk/python/failproofai_sdk/_schema.py +317 -0
  273. package/.next/standalone/sdk/python/failproofai_sdk/_scopes.py +422 -0
  274. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -0
  275. package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +750 -0
  276. package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +274 -0
  277. package/.next/standalone/sdk/python/failproofai_sdk/integrations/_compat.py +247 -0
  278. package/.next/standalone/sdk/python/failproofai_sdk/integrations/_core.py +1076 -0
  279. package/.next/standalone/sdk/python/failproofai_sdk/integrations/crewai.py +1669 -0
  280. package/.next/standalone/sdk/python/failproofai_sdk/integrations/langchain.py +2151 -0
  281. package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +1588 -0
  282. package/.next/standalone/sdk/python/failproofai_sdk/integrations/pydantic_ai.py +977 -0
  283. package/.next/standalone/sdk/python/failproofai_sdk/py.typed +0 -0
  284. package/.next/standalone/sdk/python/pyproject.toml +92 -0
  285. package/.next/standalone/sdk/python/skill/SKILL.md +399 -0
  286. package/.next/standalone/sdk/python/skill/agents/openai.yaml +8 -0
  287. package/.next/standalone/sdk/python/skill/references/events.md +234 -0
  288. package/.next/standalone/sdk/python/skill/references/frameworks.md +314 -0
  289. package/.next/standalone/sdk/python/skill/references/install.md +100 -0
  290. package/.next/standalone/sdk/python/skill/references/integration.md +352 -0
  291. package/.next/standalone/sdk/python/tests/__init__.py +0 -0
  292. package/.next/standalone/sdk/python/tests/conftest.py +169 -0
  293. package/.next/standalone/sdk/python/tests/integrations/__init__.py +6 -0
  294. package/.next/standalone/sdk/python/tests/integrations/test_crewai.py +1299 -0
  295. package/.next/standalone/sdk/python/tests/integrations/test_langchain.py +2338 -0
  296. package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +1576 -0
  297. package/.next/standalone/sdk/python/tests/integrations/test_pydantic_ai.py +1102 -0
  298. package/.next/standalone/sdk/python/tests/test_context.py +410 -0
  299. package/.next/standalone/sdk/python/tests/test_docs.py +411 -0
  300. package/.next/standalone/sdk/python/tests/test_durability.py +1352 -0
  301. package/.next/standalone/sdk/python/tests/test_encoding.py +571 -0
  302. package/.next/standalone/sdk/python/tests/test_integrations.py +1127 -0
  303. package/.next/standalone/sdk/python/tests/test_no_customer_identifiers.py +214 -0
  304. package/.next/standalone/sdk/python/tests/test_packaging.py +185 -0
  305. package/.next/standalone/sdk/python/tests/test_resolver_umbrella.py +178 -0
  306. package/.next/standalone/sdk/python/tests/test_scopes.py +487 -0
  307. package/.next/standalone/sdk/python/tests/test_sdk.py +923 -0
  308. package/.next/standalone/sdk/python/tests/test_server_contract.py +650 -0
  309. package/.next/standalone/sdk/python/tests/test_site_docs.py +371 -0
  310. package/.next/standalone/sdk/python/tests/test_skill_snippets.py +165 -0
  311. package/.next/standalone/sdk/python/tests/test_spool_contract.py +287 -0
  312. package/.next/standalone/sdk/python/tests/test_spool_creation.py +360 -0
  313. package/.next/standalone/sdk/python/tests/test_wire_format.py +279 -0
  314. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +378 -0
  315. package/.next/standalone/sdk/python/uv.lock +6005 -0
  316. package/.next/standalone/server.js +1 -1
  317. package/README.md +77 -21
  318. package/bin/failproofai.mjs +579 -306
  319. package/dist/cli.mjs +6499 -3662
  320. package/dist/index.js +5 -5
  321. package/dist/worker.mjs +2390 -1978
  322. package/package.json +27 -21
  323. package/scripts/build-policy-pack.mjs +121 -0
  324. package/scripts/changelog-section.py +77 -0
  325. package/scripts/prune-standalone.mjs +3 -0
  326. package/scripts/python-version.py +232 -0
  327. package/scripts/translate-docs/cli.ts +21 -0
  328. package/scripts/translate-docs/mintlify-nav.ts +85 -13
  329. package/scripts/translate-docs/translator.ts +66 -11
  330. package/src/audit/cache.ts +40 -3
  331. package/src/audit/cli.ts +1 -1
  332. package/src/audit/index.ts +1 -1
  333. package/src/audit/replay.ts +89 -3
  334. package/src/audit/report.ts +5 -5
  335. package/src/audit/schedule-cli.ts +49 -53
  336. package/src/hooks/builtin-policies.ts +217 -579
  337. package/src/hooks/cloud-enrollment-cli.ts +66 -27
  338. package/src/hooks/cloud-managed-policies.ts +6 -1
  339. package/src/hooks/configure-wizard.ts +56 -369
  340. package/src/hooks/custom-hooks-loader.ts +89 -6
  341. package/src/hooks/fp-home.ts +92 -4
  342. package/src/hooks/fp-reset.ts +34 -2
  343. package/src/hooks/handler.ts +181 -11
  344. package/src/hooks/harness-cli.ts +79 -25
  345. package/src/hooks/hook-activity-store.ts +13 -3
  346. package/src/hooks/install-prompt.ts +25 -3
  347. package/src/hooks/manager.ts +424 -172
  348. package/src/hooks/migrations.ts +2 -2
  349. package/src/hooks/pack-cli.ts +885 -0
  350. package/src/hooks/pack-failclosed.ts +189 -0
  351. package/src/hooks/pack-manifest.ts +352 -0
  352. package/src/hooks/pack-store.ts +891 -0
  353. package/src/hooks/policy-catalog.ts +535 -0
  354. package/src/hooks/policy-evaluator.ts +24 -16
  355. package/src/hooks/policy-registry.ts +8 -2
  356. package/src/hooks/policy-types.ts +26 -0
  357. package/src/hooks/session-pause-cli.ts +33 -9
  358. package/src/hooks/tui.ts +695 -39
  359. package/src/hooks/types.ts +15 -1
  360. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
  361. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
  362. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +0 -3
  363. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +0 -3
  364. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
  365. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
  366. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +0 -3
  367. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
  368. package/.next/standalone/.next/server/chunks/_1ger0a2._.js +0 -3
  369. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
  370. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1iiwm5e._.js +0 -3
  371. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07tbbf3._.js +0 -3
  372. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kc8_v5._.js +0 -4
  373. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +0 -4
  374. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mhqk5a._.js +0 -4
  375. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pltpn8._.js +0 -4
  376. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qkcyf-._.js +0 -4
  377. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w0x_ok._.js +0 -4
  378. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +0 -4
  379. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bfex62._.js +0 -5
  380. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +0 -6
  381. package/.next/standalone/.next/server/chunks/ssr/_0lvyuv4._.js +0 -23
  382. package/.next/standalone/.next/static/chunks/2_2lhuojq2xop.js +0 -1
  383. package/.next/standalone/.next/static/chunks/36fw6yk1s6x_f.js +0 -1
  384. package/.next/standalone/.next/static/chunks/3k7mxbp9hiom0.js +0 -1
  385. package/.next/standalone/.next/static/chunks/3tzq0260n17ae.js +0 -6
  386. package/.next/standalone/.next/static/chunks/3wq9x0c-7ld5d.css +0 -2
  387. package/src/hooks/policy-presets.ts +0 -135
  388. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_buildManifest.js +0 -0
  389. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_clientMiddlewareManifest.js +0 -0
  390. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_ssgManifest.js +0 -0
@@ -0,0 +1,891 @@
1
+ /**
2
+ * Fetching and installing policy packs.
3
+ *
4
+ * ## The trust this does and does not give you
5
+ *
6
+ * A pack is verified against a `SHA256SUMS` published in the same release, so a
7
+ * truncated, cached or substituted download is caught. That manifest is NOT a
8
+ * signature: whoever controls the release controls both files, so this proves
9
+ * nothing about the publisher's identity. What it does buy is real and worth
10
+ * being precise about — the digest is RECORDED in `installed.json` at add time,
11
+ * and `pack-manifest.ts` re-verifies it immediately before every import. So a
12
+ * pack cannot change under a machine after it was installed, and a repository
13
+ * that retags or force-pushes an asset stops loading rather than silently
14
+ * running something else. Signing was considered and deliberately deferred.
15
+ *
16
+ * ## Never discovery
17
+ *
18
+ * Asset URLs are CONSTRUCTED from an owner, repo and concrete tag. When the
19
+ * user omits a tag, one `releases/latest` redirect resolves it before any asset
20
+ * is fetched or written. There is no API lookup or rate-limit dependency, and
21
+ * the installed record always names the pinned result — which has to AGREE with
22
+ * the version that pack's manifest declares, or nothing is installed at all
23
+ * (`packTagMatchesVersion`).
24
+ *
25
+ * ## Never on the hook path
26
+ *
27
+ * Everything here touches the network. A hook must never wait on it. This module
28
+ * is reachable only from the `pack` CLI command.
29
+ */
30
+ import { createHash } from "node:crypto";
31
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
32
+ import { dirname, resolve } from "node:path";
33
+ import { packsRoot, parsePackIdentity, parsePackPolicy, readInstalledPacks } from "./pack-manifest";
34
+ import type { ResolvedPack } from "./pack-manifest";
35
+ import type { InstalledPackRecord } from "./pack-manifest";
36
+ import type { PolicyCatalogEntry } from "./policy-types";
37
+ import type { PolicyEffect } from "./cloud-managed-policies";
38
+
39
+ const DEFAULT_BASE_URL = "https://github.com";
40
+ const FETCH_TIMEOUT_MS = 30_000;
41
+ /** Cap on a pack artifact. A policy file is kilobytes; this is a sanity bound. */
42
+ const MAX_ARTIFACT_BYTES = 8 * 1024 * 1024;
43
+
44
+ /** Asset names a publisher must use. Fixed, so the URL stays constructible. */
45
+ export const PACK_MANIFEST_ASSET = "failproofai-pack.json";
46
+ export const PACK_ENTRY_ASSET = "failproofai-pack.mjs";
47
+ export const PACK_CHECKSUMS_ASSET = "SHA256SUMS";
48
+
49
+ export interface PackSpec {
50
+ owner: string;
51
+ repo: string;
52
+ /** null when the user did not name one — resolved to a concrete tag at add time. */
53
+ tag: string | null;
54
+ }
55
+
56
+ /** A spec whose tag is known. Everything that builds a URL requires this. */
57
+ export interface PinnedPackSpec extends PackSpec {
58
+ tag: string;
59
+ }
60
+
61
+ export interface AddPackResult {
62
+ id: string;
63
+ version: string;
64
+ source: string;
65
+ /** Ids this install absorbed — the same artifact, previously under another
66
+ * name. Reported so a rename is never a silent deletion of someone's pack. */
67
+ replaced?: string[];
68
+ /** The concrete tag installed, whether typed or resolved. */
69
+ tag: string;
70
+ /**
71
+ * True when the user named no tag and this was the newest release at that
72
+ * moment. Surfaced so the pin is VISIBLE — re-running the same command later
73
+ * can install something different, and that should not be a silent surprise.
74
+ */
75
+ resolvedFromLatest: boolean;
76
+ /** Names actually registered — the whole pack, or the selected subset. */
77
+ enabled: string[];
78
+ /** Every policy the pack contains, whether taken or not. */
79
+ available: string[];
80
+ /** How the selection was arrived at, so the CLI can explain itself. */
81
+ selection: SelectionReason;
82
+ /** Category slugs the pack offers, for `--category`. */
83
+ categories: string[];
84
+ artifact: string;
85
+ }
86
+
87
+ function baseUrl(): string {
88
+ return (process.env.FAILPROOFAI_PACK_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
89
+ }
90
+
91
+ const OWNER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
92
+ const TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
93
+
94
+ /**
95
+ * Parse a pack source into owner, repo and an optional tag.
96
+ *
97
+ * Accepts what a person actually has to hand: the shorthand, and the URL they
98
+ * copied out of the browser.
99
+ *
100
+ * github:acme/finance@v1.2.0
101
+ * acme/finance@v1.2.0
102
+ * acme/finance (tag resolved at add time)
103
+ * https://github.com/acme/finance/releases/tag/v1.2.0
104
+ * https://github.com/acme/finance/releases/download/v1.2.0/failproofai-pack.mjs
105
+ * https://github.com/acme/finance
106
+ *
107
+ * A missing tag is resolved to a CONCRETE one before anything is written, and
108
+ * that concrete tag is what `installed.json` records. So "no moving sources"
109
+ * still holds where it matters: what a machine reinstalls is pinned, even when
110
+ * the person who typed it did not know the version.
111
+ */
112
+ export function parsePackSpec(source: string): PackSpec {
113
+ const raw = source.trim();
114
+ if (raw.length === 0) throw new Error("pack source is empty");
115
+
116
+ const url = raw.match(/^(?:https?:\/\/)?(?:www\.)?github\.com\/(.+)$/i);
117
+ if (url) {
118
+ const parts = url[1].replace(/\/+$/, "").split("/");
119
+ const [owner, repo, ...rest] = parts;
120
+ // `/releases/tag/<tag>` and `/releases/download/<tag>/<asset>` both carry the
121
+ // tag in the slot after their keyword; a tag may itself contain slashes
122
+ // (`release/2.1`), so take everything up to the asset rather than one segment.
123
+ let tag: string | null = null;
124
+ if (rest[0] === "releases" && (rest[1] === "tag" || rest[1] === "download")) {
125
+ const tail = rest.slice(2);
126
+ tag = (rest[1] === "download" ? tail.slice(0, -1) : tail).join("/") || null;
127
+ } else if (rest[0] === "releases" && rest[1] === "latest") {
128
+ tag = null;
129
+ }
130
+ return validated(owner, repo, tag);
131
+ }
132
+
133
+ const withoutScheme = raw.replace(/^github:/i, "");
134
+ const at = withoutScheme.lastIndexOf("@");
135
+ const repoPart = at > 0 ? withoutScheme.slice(0, at) : withoutScheme;
136
+ const tag = at > 0 ? withoutScheme.slice(at + 1) : null;
137
+ const slash = repoPart.indexOf("/");
138
+ if (slash <= 0 || slash === repoPart.length - 1) {
139
+ throw new Error(
140
+ `pack source must be owner/repo, owner/repo@tag, or a github.com URL — got ${JSON.stringify(source)}`,
141
+ );
142
+ }
143
+ return validated(repoPart.slice(0, slash), repoPart.slice(slash + 1), tag);
144
+ }
145
+
146
+ function validated(owner: string, repo: string, tag: string | null): PackSpec {
147
+ if (!owner || !OWNER_RE.test(owner)) throw new Error(`unsafe owner ${JSON.stringify(owner)}`);
148
+ if (!repo || !OWNER_RE.test(repo)) throw new Error(`unsafe repo ${JSON.stringify(repo)}`);
149
+ if (tag !== null && !TAG_RE.test(tag)) throw new Error(`unsafe tag ${JSON.stringify(tag)}`);
150
+ return { owner, repo, tag };
151
+ }
152
+
153
+ /**
154
+ * The concrete tag of a repository's newest release, read from the redirect
155
+ * `releases/latest` issues rather than from the API.
156
+ *
157
+ * Deliberately not `api.github.com`: the redirect is on the host we already
158
+ * fetch the assets from, so it needs no second origin, carries no 60-per-hour
159
+ * unauthenticated rate limit, and stays pointed at whatever
160
+ * `FAILPROOFAI_PACK_BASE_URL` names — which is how a mirror, and the tests,
161
+ * work at all.
162
+ *
163
+ * The price of that, and it is a real one: the redirect is issued only for a
164
+ * PUBLISHED, non-prerelease release. A repository whose newest release is a
165
+ * prerelease or a draft therefore either redirects to an OLDER stable tag —
166
+ * pinning something the publisher has already superseded — or issues no
167
+ * redirect at all. Only the second is detectable from here, so the error names
168
+ * the prerelease case explicitly; the first is why naming a tag is the answer
169
+ * whenever a publisher's newest release is not their newest stable one.
170
+ */
171
+ export async function resolveLatestTag(spec: PackSpec): Promise<string> {
172
+ const url = `${baseUrl()}/${spec.owner}/${spec.repo}/releases/latest`;
173
+ const response = await fetch(url, {
174
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
175
+ redirect: "manual",
176
+ });
177
+ const location = response.headers.get("location");
178
+ if (!location) {
179
+ throw new Error(
180
+ `could not resolve the newest release of ${spec.owner}/${spec.repo} ` +
181
+ `(GET ${url} returned ${response.status} with no redirect). That redirect is issued only ` +
182
+ `for a published, non-prerelease release, so either the repository has no releases at all ` +
183
+ `or every release it has is a prerelease or a draft. Name a tag explicitly.`,
184
+ );
185
+ }
186
+ const match = location.match(/\/releases\/tag\/(.+)$/);
187
+ if (!match) throw new Error(`unexpected redirect from ${url}: ${location}`);
188
+ const tag = decodeURIComponent(match[1]).replace(/\/+$/, "");
189
+ if (!TAG_RE.test(tag)) throw new Error(`unsafe tag ${JSON.stringify(tag)} from ${url}`);
190
+ return tag;
191
+ }
192
+
193
+ /**
194
+ * The canonical spelling recorded in `installed.json`.
195
+ *
196
+ * Always carries the CONCRETE tag, never the tagless form the user may have
197
+ * typed — otherwise re-adding a pack later would resolve to a different release
198
+ * and the recorded source would not describe what is installed.
199
+ */
200
+ export function formatPackSpec(spec: PinnedPackSpec): string {
201
+ return `github:${spec.owner}/${spec.repo}@${spec.tag}`;
202
+ }
203
+
204
+ export function packAssetUrl(spec: PinnedPackSpec, asset: string): string {
205
+ return `${baseUrl()}/${spec.owner}/${spec.repo}/releases/download/${spec.tag}/${asset}`;
206
+ }
207
+
208
+ /**
209
+ * Does a release tag describe the version the pack's own manifest declares?
210
+ *
211
+ * These are two independently-typed strings that everything downstream treats as
212
+ * one fact: the TAG builds every asset URL and is what `installed.json` records
213
+ * as the source, while the VERSION is read out of the manifest and is what
214
+ * `pack list` prints and `pack:<id>@<version>:<name>` keys are built from. Left
215
+ * uncompared, a pack tagged `v2` whose manifest still said `1.0.0` installed
216
+ * cleanly and recorded a version no release of that repository answers to.
217
+ *
218
+ * A leading `v` is accepted because both spellings are in live use for the same
219
+ * release — `pack build` tells publishers to tag `<version>` while this repo's
220
+ * own releases are tagged `v<version>` — and refusing that would break packs
221
+ * that are perfectly coherent.
222
+ *
223
+ * Compared on the tag's LAST `/`-delimited segment, so the monorepo shape
224
+ * `parsePackSpec` deliberately supports (`release/2.1`) still installs:
225
+ * `PACK_VERSION_RE` forbids `/`, so such a tag could never equal a version and a
226
+ * whole-string comparison would make the shape uninstallable rather than merely
227
+ * unusual. Everything else is a genuine disagreement — the release was tagged
228
+ * without rebuilding the manifest, or the wrong tag was typed.
229
+ */
230
+ export function packTagMatchesVersion(tag: string, version: string): boolean {
231
+ const tail = tag.slice(tag.lastIndexOf("/") + 1);
232
+ return tail === version || tail === `v${version}`;
233
+ }
234
+
235
+ async function fetchBytes(url: string): Promise<Buffer> {
236
+ const response = await fetch(url, {
237
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
238
+ redirect: "follow",
239
+ });
240
+ if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`);
241
+ const contentLength = response.headers.get("content-length");
242
+ if (contentLength !== null) {
243
+ const declared = Number(contentLength);
244
+ if (Number.isFinite(declared) && declared > MAX_ARTIFACT_BYTES) {
245
+ await response.body?.cancel();
246
+ throw new Error(`${url} declares ${declared} bytes, over the ${MAX_ARTIFACT_BYTES} limit`);
247
+ }
248
+ }
249
+ if (!response.body) return Buffer.alloc(0);
250
+
251
+ const chunks: Buffer[] = [];
252
+ const reader = response.body.getReader();
253
+ let total = 0;
254
+ try {
255
+ while (true) {
256
+ const { done, value } = await reader.read();
257
+ if (done) break;
258
+ total += value.byteLength;
259
+ if (total > MAX_ARTIFACT_BYTES) {
260
+ await reader.cancel();
261
+ throw new Error(`${url} is over the ${MAX_ARTIFACT_BYTES} byte limit`);
262
+ }
263
+ chunks.push(Buffer.from(value));
264
+ }
265
+ } finally {
266
+ reader.releaseLock();
267
+ }
268
+ return Buffer.concat(chunks, total);
269
+ }
270
+
271
+ /**
272
+ * The digest a `sha256sum`-format manifest records for one asset.
273
+ *
274
+ * A missing line is a refusal, never a skipped check — the manifest is the only
275
+ * thing between a substituted download and a file this process is about to
276
+ * install as policy.
277
+ */
278
+ export function digestFor(manifest: string, assetName: string): string | null {
279
+ for (const line of manifest.split("\n")) {
280
+ const match = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/i);
281
+ if (match && match[2].trim() === assetName) return match[1].toLowerCase();
282
+ }
283
+ return null;
284
+ }
285
+
286
+ function sha256(bytes: Buffer): string {
287
+ return createHash("sha256").update(bytes).digest("hex");
288
+ }
289
+
290
+ function writeAtomic(target: string, bytes: Buffer | string): void {
291
+ const tempPath = `${target}.${process.pid}.tmp`;
292
+ try {
293
+ mkdirSync(dirname(target), { recursive: true });
294
+ writeFileSync(tempPath, bytes);
295
+ renameSync(tempPath, target);
296
+ } catch (err) {
297
+ rmSync(tempPath, { force: true });
298
+ throw err;
299
+ }
300
+ }
301
+
302
+ interface FetchedPack {
303
+ id: string;
304
+ version: string;
305
+ policies: PolicyCatalogEntry[];
306
+ effect?: PolicyEffect;
307
+ artifact: Buffer;
308
+ artifactDigest: string;
309
+ }
310
+
311
+ /** Fetch and fully validate a pack, without writing anything. */
312
+ /**
313
+ * The short names for the pack Failproof AI publishes.
314
+ *
315
+ * Lives here, not in the CLI, because the dashboard takes the same typed source
316
+ * and has to resolve it the same way — `core` worked in the terminal and failed
317
+ * in the browser for exactly as long as this list sat in `pack-cli.ts`.
318
+ */
319
+ /**
320
+ * Where our own policies actually live.
321
+ *
322
+ * They are a GitHub release like anybody else's, and the package carries no
323
+ * copy of them. That is the point: a pack that ships inside the binary is a
324
+ * policy set we chose for you and put on your disk before you asked, and it
325
+ * gives our own policies a delivery path nobody else's pack can use. One lane,
326
+ * the same for everyone — ours are just the pack whose short name we spell.
327
+ */
328
+ export const CORE_SOURCE = "FailproofAI/policies";
329
+ export const CORE_ALIASES = new Set(["core", "failproofai", "official"]);
330
+
331
+ export interface PackAddOptions {
332
+ only?: string[];
333
+ categories?: string[];
334
+ all?: boolean;
335
+ }
336
+
337
+ export interface AddedPack {
338
+ id: string;
339
+ version: string;
340
+ enabled: string[];
341
+ available: string[];
342
+ /** True when it came from the copy inside this package rather than a release. */
343
+ bundled: boolean;
344
+ source?: string;
345
+ tag?: string;
346
+ resolvedFromLatest?: boolean;
347
+ selection?: string;
348
+ categories?: string[];
349
+ }
350
+
351
+ /**
352
+ * Install a pack from whatever a person typed — the one entry point for both the
353
+ * CLI and the dashboard, so a source that works in one cannot fail in the other.
354
+ */
355
+ export async function addPackFromSource(
356
+ source: string,
357
+ opts: PackAddOptions = {},
358
+ ): Promise<AddedPack> {
359
+ // `core` is a spelling of CORE_SOURCE, not a second delivery path. Resolved
360
+ // HERE rather than in the CLI so the dashboard and the CLI cannot disagree
361
+ // about what the short name means — they did once, and a name the CLI could
362
+ // resolve failed in the browser.
363
+ const resolved = CORE_ALIASES.has(source.trim().toLowerCase()) ? CORE_SOURCE : source;
364
+ const result = await addPack(resolved, opts);
365
+ return {
366
+ id: result.id,
367
+ version: result.version,
368
+ enabled: result.enabled,
369
+ available: result.available,
370
+ bundled: false,
371
+ source: result.source,
372
+ tag: result.tag,
373
+ resolvedFromLatest: result.resolvedFromLatest,
374
+ selection: result.selection,
375
+ categories: result.categories,
376
+ };
377
+ }
378
+
379
+ export interface PackPreview {
380
+ id: string;
381
+ version: string;
382
+ effect: PolicyEffect;
383
+ policies: PolicyCatalogEntry[];
384
+ /** The exact source the preview was read from, tag resolved and pinned. */
385
+ source: string;
386
+ /** True when the tag was resolved rather than typed. */
387
+ resolvedFromLatest: boolean;
388
+ }
389
+
390
+ /**
391
+ * Read what a pack CONTAINS, without installing it and without running it.
392
+ *
393
+ * The only way to see a stranger's policies was to install them, which is the
394
+ * wrong order: deciding whether to trust a pack should not require already
395
+ * having trusted it. This fetches the MANIFEST and nothing else — the entry
396
+ * artifact is never downloaded and never imported, so a preview cannot execute
397
+ * a line of somebody else's code.
398
+ *
399
+ * The manifest is still verified against the release's own SHA256SUMS and parsed
400
+ * with the loader's rules, so what is shown is what would install — and a pack
401
+ * that could never load says so here rather than after the download.
402
+ */
403
+ export async function fetchPackPreview(source: string): Promise<PackPreview> {
404
+ if (process.env.FAILPROOFAI_NO_DOWNLOAD === "1") {
405
+ throw new Error("FAILPROOFAI_NO_DOWNLOAD is set, so nothing can be fetched to preview");
406
+ }
407
+ const parsed = parsePackSpec(source);
408
+ const resolvedFromLatest = parsed.tag === null;
409
+ const spec: PinnedPackSpec =
410
+ parsed.tag !== null
411
+ ? { ...parsed, tag: parsed.tag }
412
+ : { ...parsed, tag: await resolveLatestTag(parsed) };
413
+
414
+ const checksums = (await fetchBytes(packAssetUrl(spec, PACK_CHECKSUMS_ASSET))).toString("utf8");
415
+ const manifestBytes = await fetchBytes(packAssetUrl(spec, PACK_MANIFEST_ASSET));
416
+ const manifestDigest = digestFor(checksums, PACK_MANIFEST_ASSET);
417
+ if (!manifestDigest) {
418
+ throw new Error(`${PACK_CHECKSUMS_ASSET} has no entry for ${PACK_MANIFEST_ASSET}`);
419
+ }
420
+ if (sha256(manifestBytes) !== manifestDigest) {
421
+ throw new Error(`${PACK_MANIFEST_ASSET} failed integrity verification`);
422
+ }
423
+
424
+ let raw: unknown;
425
+ try {
426
+ raw = JSON.parse(manifestBytes.toString("utf8"));
427
+ } catch (err) {
428
+ throw new Error(`${PACK_MANIFEST_ASSET} is not valid JSON: ${errText(err)}`);
429
+ }
430
+ if (!raw || typeof raw !== "object") throw new Error(`${PACK_MANIFEST_ASSET} is not an object`);
431
+ const value = raw as { id?: unknown; version?: unknown; policies?: unknown; effect?: unknown };
432
+ const identity = parsePackIdentity(value);
433
+ if (!Array.isArray(value.policies) || value.policies.length === 0) {
434
+ throw new Error("pack manifest declares no policies");
435
+ }
436
+ return {
437
+ id: identity.id,
438
+ version: identity.version,
439
+ effect: identity.effect,
440
+ policies: value.policies.map((policy, i) => parsePackPolicy(identity.id, policy, i)),
441
+ source: formatPackSpec(spec),
442
+ resolvedFromLatest,
443
+ };
444
+ }
445
+
446
+ /**
447
+ * The record this install is really an update OF — matched by id, or failing
448
+ * that by artifact digest.
449
+ *
450
+ * The digest half matters because an id can be renamed under a user: the
451
+ * published release calls this set `failproofai/builtins` and the vendored copy
452
+ * calls it `failproofai/core`. Looking up by id alone found nothing, so the
453
+ * install fell back to the pack's DEFAULTS and quietly discarded whatever the
454
+ * user had chosen — while reporting that their selection was kept.
455
+ */
456
+ function priorRecordFor(id: string, sha256: string): ResolvedPack | undefined {
457
+ const installed = readInstalledPacks().packs;
458
+ return installed.find((p) => p.id === id) ?? installed.find((p) => p.sha256 === sha256);
459
+ }
460
+
461
+ async function fetchPack(spec: PinnedPackSpec): Promise<FetchedPack> {
462
+ const checksums = (await fetchBytes(packAssetUrl(spec, PACK_CHECKSUMS_ASSET))).toString("utf8");
463
+
464
+ const manifestBytes = await fetchBytes(packAssetUrl(spec, PACK_MANIFEST_ASSET));
465
+ const manifestDigest = digestFor(checksums, PACK_MANIFEST_ASSET);
466
+ if (!manifestDigest) throw new Error(`${PACK_CHECKSUMS_ASSET} has no entry for ${PACK_MANIFEST_ASSET}`);
467
+ if (sha256(manifestBytes) !== manifestDigest) {
468
+ throw new Error(`${PACK_MANIFEST_ASSET} failed integrity verification`);
469
+ }
470
+
471
+ const artifact = await fetchBytes(packAssetUrl(spec, PACK_ENTRY_ASSET));
472
+ const entryDigest = digestFor(checksums, PACK_ENTRY_ASSET);
473
+ if (!entryDigest) throw new Error(`${PACK_CHECKSUMS_ASSET} has no entry for ${PACK_ENTRY_ASSET}`);
474
+ const artifactDigest = sha256(artifact);
475
+ if (artifactDigest !== entryDigest) {
476
+ throw new Error(`${PACK_ENTRY_ASSET} failed integrity verification`);
477
+ }
478
+
479
+ let parsed: unknown;
480
+ try {
481
+ parsed = JSON.parse(manifestBytes.toString("utf8"));
482
+ } catch (err) {
483
+ throw new Error(`${PACK_MANIFEST_ASSET} is not valid JSON: ${errText(err)}`);
484
+ }
485
+ if (!parsed || typeof parsed !== "object") throw new Error(`${PACK_MANIFEST_ASSET} is not an object`);
486
+ const raw = parsed as { id?: unknown; version?: unknown; policies?: unknown; effect?: unknown };
487
+ const identity = parsePackIdentity(raw);
488
+ if (!Array.isArray(raw.policies) || raw.policies.length === 0) {
489
+ throw new Error("pack manifest declares no policies");
490
+ }
491
+ // Validated with the SAME rules the loader applies, so a pack that could never
492
+ // load is refused here — while nothing has been written — rather than
493
+ // installing cleanly and failing silently on the next tool call.
494
+ const policies = raw.policies.map((p, i) => parsePackPolicy(identity.id, p, i));
495
+
496
+ return {
497
+ id: identity.id,
498
+ version: identity.version,
499
+ policies,
500
+ ...(raw.effect !== undefined ? { effect: identity.effect } : {}),
501
+ artifact,
502
+ artifactDigest,
503
+ };
504
+ }
505
+
506
+ /**
507
+ * Fetch, verify and install a pack, then activate it.
508
+ *
509
+ * `only` takes a subset of the pack's policies. Omitted, the whole pack is
510
+ * taken — and re-adding a pack that already had a selection CARRIES IT FORWARD,
511
+ * because upgrading a pack should not quietly switch on the policies someone
512
+ * chose to leave off.
513
+ */
514
+ export function slugifyCategory(category: string): string {
515
+ return category.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
516
+ }
517
+
518
+ /**
519
+ * Which of a pack's policies to take.
520
+ *
521
+ * The default is the pack's OWN `defaultEnabled` set, not everything. A pack
522
+ * carries an opinion about which of its policies are safe to switch on
523
+ * unattended — for the builtins that is 10 of 38 — and installing the whole
524
+ * thing overrode that opinion with one nobody held. `block-kubectl`,
525
+ * `block-terraform` and `require-ci-green-before-stop` are off by default in the
526
+ * npm package precisely because they interrupt legitimate work.
527
+ */
528
+ function resolveSelection(
529
+ policies: PolicyCatalogEntry[],
530
+ opts: { only?: string[]; categories?: string[]; all?: boolean } | undefined,
531
+ previous: string[] | null | undefined,
532
+ previouslyInstalled: boolean,
533
+ ): { enabled: string[] | null; reason: SelectionReason } {
534
+ const available = policies.map((p) => p.name);
535
+ if (opts?.all) return { enabled: null, reason: "all" };
536
+
537
+ const picked = new Set<string>();
538
+ if (opts?.categories?.length) {
539
+ const wanted = new Set(opts.categories.map(slugifyCategory));
540
+ const known = new Map(policies.map((p) => [slugifyCategory(p.category), p.category]));
541
+ const unknown = [...wanted].filter((c) => !known.has(c));
542
+ if (unknown.length > 0) {
543
+ throw new Error(
544
+ `no such categor${unknown.length === 1 ? "y" : "ies"}: ${unknown.join(", ")} ` +
545
+ `(has: ${[...new Set(policies.map((p) => slugifyCategory(p.category)))].join(", ")})`,
546
+ );
547
+ }
548
+ for (const p of policies) if (wanted.has(slugifyCategory(p.category))) picked.add(p.name);
549
+ }
550
+ if (opts?.only?.length) {
551
+ const unknown = opts.only.filter((n) => !available.includes(n));
552
+ if (unknown.length > 0) {
553
+ throw new Error(`pack does not contain ${unknown.join(", ")}`);
554
+ }
555
+ for (const n of opts.only) picked.add(n);
556
+ }
557
+ if (picked.size > 0) {
558
+ // Kept in the pack's declared order, which is the order everything else
559
+ // presents them in.
560
+ return { enabled: available.filter((n) => picked.has(n)), reason: "selected" };
561
+ }
562
+
563
+ // No flags. An upgrade keeps whatever the machine already had — switching a
564
+ // policy back on because the user did not repeat themselves is not an upgrade.
565
+ if (previouslyInstalled) {
566
+ return { enabled: previous === null ? null : (previous ?? null), reason: "carried" };
567
+ }
568
+ return { enabled: policies.filter((p) => p.defaultEnabled).map((p) => p.name), reason: "defaults" };
569
+ }
570
+
571
+ export type SelectionReason = "all" | "selected" | "carried" | "defaults";
572
+
573
+ /**
574
+ * Import the artifact and check it registers what the manifest promised.
575
+ *
576
+ * `pack add` used to verify digests and manifest SHAPE and then install without
577
+ * ever executing the file. Two failures got through, both reproduced: an
578
+ * artifact that does not parse installed at exit 0 and bricked every later tool
579
+ * call, and a one-name typo between manifest and artifact — the exact slip a
580
+ * publisher hand-maintaining two files makes — installed reporting "2/2
581
+ * enabled" and then converted into a MACHINE-WIDE DENY, because a declared
582
+ * policy that never registers is precisely what the fail-closed guard denies
583
+ * for.
584
+ *
585
+ * The right place to catch that is here, where nothing has been activated yet
586
+ * and the publisher's own mistake can be named. This runs only on the CLI path;
587
+ * the hook path never imports anything it has not already recorded.
588
+ */
589
+ async function verifyArtifactRegisters(
590
+ artifactPath: string,
591
+ declared: readonly string[],
592
+ ): Promise<void> {
593
+ const { loadCustomHooks } = await import("./custom-hooks-loader");
594
+ let registered: string[];
595
+ try {
596
+ registered = (await loadCustomHooks(artifactPath, { strict: true })).map((h) => h.name);
597
+ } catch (err) {
598
+ throw new Error(
599
+ `its artifact could not be loaded: ${err instanceof Error ? err.message : String(err)}`,
600
+ );
601
+ }
602
+ const missing = declared.filter((name) => !registered.includes(name));
603
+ const extra = registered.filter((name) => !declared.includes(name));
604
+ if (missing.length > 0) {
605
+ throw new Error(
606
+ `its manifest declares ${missing.join(", ")} but the artifact does not register ` +
607
+ `${missing.length === 1 ? "it" : "them"} — that would install as protection that never runs`,
608
+ );
609
+ }
610
+ if (extra.length > 0) {
611
+ throw new Error(
612
+ `its artifact registers undeclared ${extra.join(", ")} — a policy that enforces but ` +
613
+ `appears in no listing cannot be reviewed or switched off`,
614
+ );
615
+ }
616
+ }
617
+
618
+ /**
619
+ * The same check, as a question rather than a refusal — for `pack list`, which
620
+ * reported a pack whose artifact does not load as fully healthy, on the very
621
+ * command the fail-closed deny message tells the human to run.
622
+ */
623
+ export async function checkPackArtifact(
624
+ artifactPath: string,
625
+ declared: readonly string[],
626
+ ): Promise<string | null> {
627
+ try {
628
+ await verifyArtifactRegisters(artifactPath, declared);
629
+ return null;
630
+ } catch (err) {
631
+ return err instanceof Error ? err.message : String(err);
632
+ }
633
+ }
634
+
635
+ export async function addPack(
636
+ source: string,
637
+ opts?: { only?: string[]; categories?: string[]; all?: boolean },
638
+ ): Promise<AddPackResult> {
639
+ if (process.env.FAILPROOFAI_NO_DOWNLOAD) {
640
+ throw new Error(
641
+ "pack downloads are disabled (FAILPROOFAI_NO_DOWNLOAD). Already-installed packs keep enforcing.",
642
+ );
643
+ }
644
+ const parsed = parsePackSpec(source);
645
+ // Resolve BEFORE anything is written, and pin the concrete result. A tagless
646
+ // source is a convenience for the person typing; what the machine records must
647
+ // always name one release.
648
+ const spec: PinnedPackSpec = parsed.tag
649
+ ? { ...parsed, tag: parsed.tag }
650
+ : { ...parsed, tag: await resolveLatestTag(parsed) };
651
+ const resolvedFromLatest = parsed.tag === null;
652
+ const fetched = await fetchPack(spec);
653
+ // Checked here, before a byte is written and before the artifact is imported,
654
+ // so a refusal leaves the machine exactly as it was — and on the RESOLVED tag
655
+ // as well as a typed one, because a tagless add pins whatever
656
+ // `releases/latest` pointed at and lands the same incoherent pair in
657
+ // `installed.json` with nobody having typed it. Deliberately NOT in
658
+ // `fetchPackPreview`: looking at a pack has to stay possible precisely so a
659
+ // publisher can SEE the two values disagree.
660
+ if (!packTagMatchesVersion(spec.tag, fetched.version)) {
661
+ const fromTag = spec.tag.slice(spec.tag.lastIndexOf("/") + 1).replace(/^v/, "");
662
+ const fix =
663
+ `re-tag the release ${fetched.version} (a leading v is fine), or rebuild the pack with ` +
664
+ `--version ${fromTag}`;
665
+ throw new Error(
666
+ `${fetched.id} was not installed: release tag ${spec.tag} does not match the version its ` +
667
+ `manifest declares (${fetched.version}). ` +
668
+ (resolvedFromLatest
669
+ ? `That is the newest release of ${spec.owner}/${spec.repo}, so the release and the pack ` +
670
+ `disagree at the source — ask the publisher to ${fix}.`
671
+ : `Name the tag whose manifest says ${fetched.version}, or fix the pack: ${fix}.`),
672
+ );
673
+ }
674
+ const available = fetched.policies.map((p) => p.name);
675
+
676
+ const prior = priorRecordFor(fetched.id, fetched.artifactDigest);
677
+ const { enabled, reason } = resolveSelection(
678
+ fetched.policies,
679
+ opts,
680
+ prior?.enabled ? prior.enabled.filter((n) => available.includes(n)) : prior?.enabled,
681
+ Boolean(prior),
682
+ );
683
+
684
+ const root = packsRoot();
685
+ const artifactRel = `artifacts/${fetched.artifactDigest}.mjs`;
686
+ const artifactAbs = resolve(root, artifactRel);
687
+ // Content-addressed, so a file that already exists has these exact bytes and
688
+ // rewriting it could only ever disturb a pack currently importing it.
689
+ if (!existsSync(artifactAbs)) writeAtomic(artifactAbs, fetched.artifact);
690
+
691
+ // Before `installed.json` — the file that activates it — is written. The
692
+ // artifact on disk is inert until something points at it, so a refusal here
693
+ // leaves the machine exactly as it was.
694
+ try {
695
+ await verifyArtifactRegisters(artifactAbs, available);
696
+ } catch (err) {
697
+ throw new Error(
698
+ `${fetched.id}@${fetched.version} was not installed: ${err instanceof Error ? err.message : String(err)}`,
699
+ );
700
+ }
701
+
702
+ // An id is self-declared and nothing owns it, while `upsertInstalled` replaces
703
+ // by id — so a pack served from any repository could declare
704
+ // `failproofai/builtins` and silently take the place of the one already
705
+ // installed. Binding the id to the source it first came from is the cheap half
706
+ // of the fix: a second source for the same id has to be an explicit removal.
707
+ // Compared on the REPOSITORY, not the whole source: `github:acme/pack@v1.2.0`
708
+ // and `@v1.3.0` are the same publisher shipping an upgrade, which is the
709
+ // ordinary case this must not break.
710
+ const repoOf = (source: string): string => {
711
+ const at = source.lastIndexOf("@");
712
+ return at > source.indexOf(":") ? source.slice(0, at) : source;
713
+ };
714
+ if (
715
+ prior &&
716
+ !prior.source.startsWith("bundled:") &&
717
+ repoOf(prior.source) !== repoOf(formatPackSpec(spec))
718
+ ) {
719
+ throw new Error(
720
+ `pack id ${fetched.id} is already installed from ${prior.source}. ` +
721
+ `Refusing to replace it with ${formatPackSpec(spec)} — remove it first if that is what you mean.`,
722
+ );
723
+ }
724
+
725
+ const record: InstalledPackRecord = {
726
+ id: fetched.id,
727
+ version: fetched.version,
728
+ source: formatPackSpec(spec),
729
+ entry: artifactRel,
730
+ sha256: fetched.artifactDigest,
731
+ policies: fetched.policies,
732
+ ...(fetched.effect ? { effect: fetched.effect } : {}),
733
+ ...(enabled ? { enabled } : {}),
734
+ };
735
+
736
+ const absorbed = upsertInstalled(record);
737
+ return {
738
+ replaced: absorbed,
739
+ id: fetched.id,
740
+ version: fetched.version,
741
+ source: record.source,
742
+ tag: spec.tag,
743
+ resolvedFromLatest,
744
+ enabled: enabled ?? available,
745
+ available,
746
+ selection: reason,
747
+ categories: [...new Set(fetched.policies.map((p) => slugifyCategory(p.category)))],
748
+ artifact: artifactAbs,
749
+ };
750
+ }
751
+
752
+ /**
753
+ * The vendored pack directory, when a working tree happens to have one.
754
+ *
755
+ * The published package does NOT carry `policy-pack/` and there is no install
756
+ * path through here. Our policies are fetched from their GitHub release like
757
+ * anybody else's — a pack that ships inside the binary is a policy set chosen
758
+ * for the user and written to their disk before they asked, and it gave our own
759
+ * policies a delivery route no third-party pack could use.
760
+ *
761
+ * The one remaining caller is `audit/replay.ts`, which prefers the pack's own
762
+ * functions when a dev tree has run `build:pack` and falls back to the compiled
763
+ * implementations when it returns null. Both produce identical text, so the two
764
+ * paths score the same; the fallback is the normal case now.
765
+ *
766
+ * Anchored at `FAILPROOFAI_PACKAGE_ROOT` for the reason `npmPlatformBinaryPath`
767
+ * documents: `import.meta.url` does not survive the CJS bundle.
768
+ */
769
+ export function bundledPackDir(): string | null {
770
+ const root = process.env.FAILPROOFAI_PACKAGE_ROOT;
771
+ if (!root) return null;
772
+ const dir = resolve(root, "policy-pack");
773
+ return existsSync(resolve(dir, PACK_MANIFEST_ASSET)) ? dir : null;
774
+ }
775
+
776
+ export function setPackPolicyEnabled(
777
+ packId: string,
778
+ name: string,
779
+ on: boolean,
780
+ ): { ok: boolean; reason?: string } {
781
+ const manifestPath = resolve(packsRoot(), "installed.json");
782
+ if (!existsSync(manifestPath)) return { ok: false, reason: "no packs are installed" };
783
+ let raw: { schemaVersion?: number; packs?: InstalledPackRecord[] };
784
+ try {
785
+ raw = JSON.parse(readFileSync(manifestPath, "utf8"));
786
+ } catch {
787
+ return { ok: false, reason: `pack manifest at ${manifestPath} is unreadable` };
788
+ }
789
+ const packs = Array.isArray(raw.packs) ? raw.packs : [];
790
+ const pack = packs.find((p) => p?.id === packId);
791
+ if (!pack) return { ok: false, reason: `no installed pack with id ${packId}` };
792
+ // `policies` is `unknown` on the record — it is validated by the loader, not
793
+ // here — so read the names defensively rather than trusting the shape.
794
+ const declared = Array.isArray(pack.policies) ? (pack.policies as Array<{ name?: unknown }>) : [];
795
+ const available = declared
796
+ .map((p) => (typeof p?.name === "string" ? p.name : null))
797
+ .filter((n): n is string => n !== null);
798
+ if (!available.includes(name)) {
799
+ return { ok: false, reason: `pack ${packId} declares no policy named ${name}` };
800
+ }
801
+ // `enabled` absent means "the whole pack", which is what `--all` records so a
802
+ // later version's new policies are included. Turning one off has to make that
803
+ // implicit set explicit, or there is nothing to subtract from.
804
+ const current = pack.enabled ?? available;
805
+ const next: string[] = on
806
+ ? [...new Set([...current, name])]
807
+ : current.filter((n: string) => n !== name);
808
+ // Kept in the pack's declared order rather than the order things were toggled.
809
+ pack.enabled = available.filter((n) => next.includes(n));
810
+ writeAtomic(manifestPath, JSON.stringify({ schemaVersion: 1, packs }, null, 2) + "\n");
811
+ return { ok: true };
812
+ }
813
+
814
+ export function removePack(id: string): boolean {
815
+ const manifestPath = resolve(packsRoot(), "installed.json");
816
+ if (!existsSync(manifestPath)) return false;
817
+ let raw: { schemaVersion?: number; packs?: InstalledPackRecord[] };
818
+ try {
819
+ raw = JSON.parse(readFileSync(manifestPath, "utf8"));
820
+ } catch {
821
+ return false;
822
+ }
823
+ const packs = Array.isArray(raw.packs) ? raw.packs : [];
824
+ const remaining = packs.filter((p) => p?.id !== id);
825
+ if (remaining.length === packs.length) return false;
826
+ // The artifact is left on disk deliberately: it is content-addressed and inert
827
+ // once nothing points at it, so keeping it makes a re-add offline-safe. Same
828
+ // choice the cloud reconciler makes.
829
+ writeAtomic(manifestPath, JSON.stringify({ schemaVersion: 1, packs: remaining }, null, 2) + "\n");
830
+ return true;
831
+ }
832
+
833
+ /**
834
+ * Replace or append one pack in `installed.json`, written LAST and atomically so
835
+ * activation is a single flip: until this rename lands, the artifact on disk is
836
+ * a file nothing points at.
837
+ */
838
+ /**
839
+ * Install or update one pack record, collapsing anything that is the SAME PACK
840
+ * under a different id.
841
+ *
842
+ * Two ids for one artifact is not hypothetical: the published release calls this
843
+ * set `failproofai/builtins` and the copy vendored in the package calls it
844
+ * `failproofai/core`, so following the help's own two examples installed both.
845
+ * The result claimed "2 packs · 76 policies" over ONE artifact, warned on stderr
846
+ * on every hook event, and left policies showing ON that did not block.
847
+ *
848
+ * Matched on the artifact DIGEST, which is what actually decides whether two
849
+ * records describe the same code. Returns the ids it absorbed so the caller can
850
+ * say so — an earlier version deleted the old row silently, taking the user's
851
+ * selection and the enforcement it gave them with it.
852
+ */
853
+ function upsertInstalled(record: InstalledPackRecord): string[] {
854
+ const manifestPath = resolve(packsRoot(), "installed.json");
855
+ let packs: InstalledPackRecord[] = [];
856
+ if (existsSync(manifestPath)) {
857
+ try {
858
+ const raw = JSON.parse(readFileSync(manifestPath, "utf8")) as { packs?: InstalledPackRecord[] };
859
+ if (Array.isArray(raw.packs)) packs = raw.packs;
860
+ } catch {
861
+ // A manifest we cannot parse is one we must not append to: merging into
862
+ // it would either drop the packs it held or write them back malformed.
863
+ throw new Error(
864
+ `existing pack manifest at ${manifestPath} is unreadable; move it aside and re-add`,
865
+ );
866
+ }
867
+ }
868
+ // Anything with these bytes under another name is this pack, renamed.
869
+ const absorbed = packs
870
+ .filter((p) => p?.sha256 === record.sha256 && p?.id !== record.id)
871
+ .map((p) => p.id);
872
+ // The selection the user had under the old id is theirs — carry it, rather
873
+ // than resetting them to defaults because a publisher renamed something.
874
+ if (!record.enabled) {
875
+ const prior = packs.find((p) => absorbed.includes(p?.id));
876
+ const carried = prior?.enabled?.filter((n) =>
877
+ (record.policies as Array<{ name?: unknown }> | undefined)?.some((rp) => rp?.name === n),
878
+ );
879
+ if (carried && carried.length > 0) record.enabled = carried;
880
+ }
881
+ const remaining = packs.filter((p) => !absorbed.includes(p?.id));
882
+ const idx = remaining.findIndex((p) => p?.id === record.id);
883
+ if (idx >= 0) remaining[idx] = record;
884
+ else remaining.push(record);
885
+ writeAtomic(manifestPath, JSON.stringify({ schemaVersion: 1, packs: remaining }, null, 2) + "\n");
886
+ return absorbed;
887
+ }
888
+
889
+ function errText(err: unknown): string {
890
+ return err instanceof Error ? err.message : String(err);
891
+ }