failproofai 1.0.1 → 1.0.2-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 (389) 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]__0oa1lav._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07tbbf3._.js → [root-of-the-server]__0zig5sv._.js} +2 -2
  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/{_0lvyuv4._.js → _00e9tnf._.js} +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/{_0u5zauv._.js → _12l4a_k._.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/{37fcpc36x9gpe.js → 0eqky-7n7dgdm.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{1ide1wl2f7zi1.js → 0sa9v5mhc8861.js} +13 -13
  122. package/.next/standalone/.next/static/chunks/1mfzsja6ftim5.css +2 -0
  123. package/.next/standalone/.next/static/chunks/{3k7mxbp9hiom0.js → 1nuqytbta44af.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/1sl2xmn9inx3y.js +6 -0
  125. package/.next/standalone/.next/static/chunks/{2_2lhuojq2xop.js → 2aawsjld9ied_.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/2gn7ut4svofm2.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{31s1smp-g7gks.js → 2hs7ycvsav7v2.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{0k5050ezjvv49.js → 2i2yt3hzjredm.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/{3c808qrfcq0im.js → 35xg6retpe6rp.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/{2zpp1we2jv6tb.js → 3y54nw0i7dsjl.js} +1 -1
  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 +153 -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 -14
  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 +29 -22
  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 +145 -11
  319. package/dist/cli.mjs +4482 -2205
  320. package/dist/index.js +5 -5
  321. package/dist/worker.mjs +2399 -1978
  322. package/package.json +29 -22
  323. package/policy-pack/SHA256SUMS +2 -0
  324. package/policy-pack/failproofai-pack.json +732 -0
  325. package/policy-pack/failproofai-pack.mjs +2017 -0
  326. package/scripts/build-policy-pack.mjs +121 -0
  327. package/scripts/changelog-section.py +77 -0
  328. package/scripts/prune-standalone.mjs +3 -0
  329. package/scripts/python-version.py +232 -0
  330. package/scripts/translate-docs/cli.ts +21 -0
  331. package/scripts/translate-docs/mintlify-nav.ts +85 -13
  332. package/scripts/translate-docs/translator.ts +66 -11
  333. package/src/audit/cache.ts +40 -3
  334. package/src/audit/cli.ts +1 -1
  335. package/src/audit/index.ts +1 -1
  336. package/src/audit/replay.ts +89 -3
  337. package/src/audit/report.ts +5 -5
  338. package/src/audit/schedule-cli.ts +49 -53
  339. package/src/hooks/builtin-policies.ts +217 -579
  340. package/src/hooks/cloud-enrollment-cli.ts +66 -27
  341. package/src/hooks/cloud-managed-policies.ts +6 -1
  342. package/src/hooks/configure-wizard.ts +1 -1
  343. package/src/hooks/custom-hooks-loader.ts +89 -6
  344. package/src/hooks/fp-home.ts +92 -4
  345. package/src/hooks/fp-reset.ts +26 -2
  346. package/src/hooks/handler.ts +181 -11
  347. package/src/hooks/harness-cli.ts +79 -25
  348. package/src/hooks/hook-activity-store.ts +13 -3
  349. package/src/hooks/install-prompt.ts +25 -3
  350. package/src/hooks/manager.ts +418 -172
  351. package/src/hooks/migrations.ts +2 -2
  352. package/src/hooks/pack-cli.ts +505 -0
  353. package/src/hooks/pack-failclosed.ts +189 -0
  354. package/src/hooks/pack-manifest.ts +344 -0
  355. package/src/hooks/pack-store.ts +930 -0
  356. package/src/hooks/policy-catalog.ts +535 -0
  357. package/src/hooks/policy-evaluator.ts +24 -16
  358. package/src/hooks/policy-presets.ts +4 -2
  359. package/src/hooks/policy-registry.ts +8 -2
  360. package/src/hooks/policy-types.ts +26 -0
  361. package/src/hooks/session-pause-cli.ts +33 -9
  362. package/src/hooks/tui.ts +570 -0
  363. package/src/hooks/types.ts +15 -1
  364. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
  365. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
  366. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +0 -3
  367. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +0 -3
  368. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
  369. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
  370. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +0 -3
  371. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
  372. package/.next/standalone/.next/server/chunks/_1ger0a2._.js +0 -3
  373. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
  374. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1iiwm5e._.js +0 -3
  375. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kc8_v5._.js +0 -4
  376. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +0 -4
  377. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mhqk5a._.js +0 -4
  378. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pltpn8._.js +0 -4
  379. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qkcyf-._.js +0 -4
  380. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w0x_ok._.js +0 -4
  381. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +0 -4
  382. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bfex62._.js +0 -5
  383. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +0 -6
  384. package/.next/standalone/.next/static/chunks/36fw6yk1s6x_f.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/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_buildManifest.js +0 -0
  388. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_clientMiddlewareManifest.js +0 -0
  389. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_ssgManifest.js +0 -0
@@ -0,0 +1,930 @@
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.
22
+ *
23
+ * ## Never on the hook path
24
+ *
25
+ * Everything here touches the network. A hook must never wait on it. This module
26
+ * is reachable only from the `pack` CLI command.
27
+ */
28
+ import { createHash } from "node:crypto";
29
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
30
+ import { dirname, resolve } from "node:path";
31
+ import { packsRoot, parsePackIdentity, parsePackPolicy, readInstalledPacks } from "./pack-manifest";
32
+ import type { ResolvedPack } from "./pack-manifest";
33
+ import type { InstalledPackRecord } from "./pack-manifest";
34
+ import type { PolicyCatalogEntry } from "./policy-types";
35
+ import type { PolicyEffect } from "./cloud-managed-policies";
36
+
37
+ const DEFAULT_BASE_URL = "https://github.com";
38
+ const FETCH_TIMEOUT_MS = 30_000;
39
+ /** Cap on a pack artifact. A policy file is kilobytes; this is a sanity bound. */
40
+ const MAX_ARTIFACT_BYTES = 8 * 1024 * 1024;
41
+
42
+ /** Asset names a publisher must use. Fixed, so the URL stays constructible. */
43
+ export const PACK_MANIFEST_ASSET = "failproofai-pack.json";
44
+ export const PACK_ENTRY_ASSET = "failproofai-pack.mjs";
45
+ export const PACK_CHECKSUMS_ASSET = "SHA256SUMS";
46
+
47
+ export interface PackSpec {
48
+ owner: string;
49
+ repo: string;
50
+ /** null when the user did not name one — resolved to a concrete tag at add time. */
51
+ tag: string | null;
52
+ }
53
+
54
+ /** A spec whose tag is known. Everything that builds a URL requires this. */
55
+ export interface PinnedPackSpec extends PackSpec {
56
+ tag: string;
57
+ }
58
+
59
+ export interface AddPackResult {
60
+ id: string;
61
+ version: string;
62
+ source: string;
63
+ /** Ids this install absorbed — the same artifact, previously under another
64
+ * name. Reported so a rename is never a silent deletion of someone's pack. */
65
+ replaced?: string[];
66
+ /** The concrete tag installed, whether typed or resolved. */
67
+ tag: string;
68
+ /**
69
+ * True when the user named no tag and this was the newest release at that
70
+ * moment. Surfaced so the pin is VISIBLE — re-running the same command later
71
+ * can install something different, and that should not be a silent surprise.
72
+ */
73
+ resolvedFromLatest: boolean;
74
+ /** Names actually registered — the whole pack, or the selected subset. */
75
+ enabled: string[];
76
+ /** Every policy the pack contains, whether taken or not. */
77
+ available: string[];
78
+ /** How the selection was arrived at, so the CLI can explain itself. */
79
+ selection: SelectionReason;
80
+ /** Category slugs the pack offers, for `--category`. */
81
+ categories: string[];
82
+ artifact: string;
83
+ }
84
+
85
+ function baseUrl(): string {
86
+ return (process.env.FAILPROOFAI_PACK_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
87
+ }
88
+
89
+ const OWNER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
90
+ const TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
91
+
92
+ /**
93
+ * Parse a pack source into owner, repo and an optional tag.
94
+ *
95
+ * Accepts what a person actually has to hand: the shorthand, and the URL they
96
+ * copied out of the browser.
97
+ *
98
+ * github:acme/finance@v1.2.0
99
+ * acme/finance@v1.2.0
100
+ * acme/finance (tag resolved at add time)
101
+ * https://github.com/acme/finance/releases/tag/v1.2.0
102
+ * https://github.com/acme/finance/releases/download/v1.2.0/failproofai-pack.mjs
103
+ * https://github.com/acme/finance
104
+ *
105
+ * A missing tag is resolved to a CONCRETE one before anything is written, and
106
+ * that concrete tag is what `installed.json` records. So "no moving sources"
107
+ * still holds where it matters: what a machine reinstalls is pinned, even when
108
+ * the person who typed it did not know the version.
109
+ */
110
+ export function parsePackSpec(source: string): PackSpec {
111
+ const raw = source.trim();
112
+ if (raw.length === 0) throw new Error("pack source is empty");
113
+
114
+ const url = raw.match(/^(?:https?:\/\/)?(?:www\.)?github\.com\/(.+)$/i);
115
+ if (url) {
116
+ const parts = url[1].replace(/\/+$/, "").split("/");
117
+ const [owner, repo, ...rest] = parts;
118
+ // `/releases/tag/<tag>` and `/releases/download/<tag>/<asset>` both carry the
119
+ // tag in the slot after their keyword; a tag may itself contain slashes
120
+ // (`release/2.1`), so take everything up to the asset rather than one segment.
121
+ let tag: string | null = null;
122
+ if (rest[0] === "releases" && (rest[1] === "tag" || rest[1] === "download")) {
123
+ const tail = rest.slice(2);
124
+ tag = (rest[1] === "download" ? tail.slice(0, -1) : tail).join("/") || null;
125
+ } else if (rest[0] === "releases" && rest[1] === "latest") {
126
+ tag = null;
127
+ }
128
+ return validated(owner, repo, tag);
129
+ }
130
+
131
+ const withoutScheme = raw.replace(/^github:/i, "");
132
+ const at = withoutScheme.lastIndexOf("@");
133
+ const repoPart = at > 0 ? withoutScheme.slice(0, at) : withoutScheme;
134
+ const tag = at > 0 ? withoutScheme.slice(at + 1) : null;
135
+ const slash = repoPart.indexOf("/");
136
+ if (slash <= 0 || slash === repoPart.length - 1) {
137
+ throw new Error(
138
+ `pack source must be owner/repo, owner/repo@tag, or a github.com URL — got ${JSON.stringify(source)}`,
139
+ );
140
+ }
141
+ return validated(repoPart.slice(0, slash), repoPart.slice(slash + 1), tag);
142
+ }
143
+
144
+ function validated(owner: string, repo: string, tag: string | null): PackSpec {
145
+ if (!owner || !OWNER_RE.test(owner)) throw new Error(`unsafe owner ${JSON.stringify(owner)}`);
146
+ if (!repo || !OWNER_RE.test(repo)) throw new Error(`unsafe repo ${JSON.stringify(repo)}`);
147
+ if (tag !== null && !TAG_RE.test(tag)) throw new Error(`unsafe tag ${JSON.stringify(tag)}`);
148
+ return { owner, repo, tag };
149
+ }
150
+
151
+ /**
152
+ * The concrete tag of a repository's newest release, read from the redirect
153
+ * `releases/latest` issues rather than from the API.
154
+ *
155
+ * Deliberately not `api.github.com`: the redirect is on the host we already
156
+ * fetch the assets from, so it needs no second origin, carries no 60-per-hour
157
+ * unauthenticated rate limit, and stays pointed at whatever
158
+ * `FAILPROOFAI_PACK_BASE_URL` names — which is how a mirror, and the tests,
159
+ * work at all.
160
+ */
161
+ export async function resolveLatestTag(spec: PackSpec): Promise<string> {
162
+ const url = `${baseUrl()}/${spec.owner}/${spec.repo}/releases/latest`;
163
+ const response = await fetch(url, {
164
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
165
+ redirect: "manual",
166
+ });
167
+ const location = response.headers.get("location");
168
+ if (!location) {
169
+ throw new Error(
170
+ `could not resolve the newest release of ${spec.owner}/${spec.repo} ` +
171
+ `(GET ${url} returned ${response.status} with no redirect). Name a tag explicitly.`,
172
+ );
173
+ }
174
+ const match = location.match(/\/releases\/tag\/(.+)$/);
175
+ if (!match) throw new Error(`unexpected redirect from ${url}: ${location}`);
176
+ const tag = decodeURIComponent(match[1]).replace(/\/+$/, "");
177
+ if (!TAG_RE.test(tag)) throw new Error(`unsafe tag ${JSON.stringify(tag)} from ${url}`);
178
+ return tag;
179
+ }
180
+
181
+ /**
182
+ * The canonical spelling recorded in `installed.json`.
183
+ *
184
+ * Always carries the CONCRETE tag, never the tagless form the user may have
185
+ * typed — otherwise re-adding a pack later would resolve to a different release
186
+ * and the recorded source would not describe what is installed.
187
+ */
188
+ export function formatPackSpec(spec: PinnedPackSpec): string {
189
+ return `github:${spec.owner}/${spec.repo}@${spec.tag}`;
190
+ }
191
+
192
+ export function packAssetUrl(spec: PinnedPackSpec, asset: string): string {
193
+ return `${baseUrl()}/${spec.owner}/${spec.repo}/releases/download/${spec.tag}/${asset}`;
194
+ }
195
+
196
+ async function fetchBytes(url: string): Promise<Buffer> {
197
+ const response = await fetch(url, {
198
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
199
+ redirect: "follow",
200
+ });
201
+ if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`);
202
+ const contentLength = response.headers.get("content-length");
203
+ if (contentLength !== null) {
204
+ const declared = Number(contentLength);
205
+ if (Number.isFinite(declared) && declared > MAX_ARTIFACT_BYTES) {
206
+ await response.body?.cancel();
207
+ throw new Error(`${url} declares ${declared} bytes, over the ${MAX_ARTIFACT_BYTES} limit`);
208
+ }
209
+ }
210
+ if (!response.body) return Buffer.alloc(0);
211
+
212
+ const chunks: Buffer[] = [];
213
+ const reader = response.body.getReader();
214
+ let total = 0;
215
+ try {
216
+ while (true) {
217
+ const { done, value } = await reader.read();
218
+ if (done) break;
219
+ total += value.byteLength;
220
+ if (total > MAX_ARTIFACT_BYTES) {
221
+ await reader.cancel();
222
+ throw new Error(`${url} is over the ${MAX_ARTIFACT_BYTES} byte limit`);
223
+ }
224
+ chunks.push(Buffer.from(value));
225
+ }
226
+ } finally {
227
+ reader.releaseLock();
228
+ }
229
+ return Buffer.concat(chunks, total);
230
+ }
231
+
232
+ /**
233
+ * The digest a `sha256sum`-format manifest records for one asset.
234
+ *
235
+ * A missing line is a refusal, never a skipped check — the manifest is the only
236
+ * thing between a substituted download and a file this process is about to
237
+ * install as policy.
238
+ */
239
+ export function digestFor(manifest: string, assetName: string): string | null {
240
+ for (const line of manifest.split("\n")) {
241
+ const match = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/i);
242
+ if (match && match[2].trim() === assetName) return match[1].toLowerCase();
243
+ }
244
+ return null;
245
+ }
246
+
247
+ function sha256(bytes: Buffer): string {
248
+ return createHash("sha256").update(bytes).digest("hex");
249
+ }
250
+
251
+ function writeAtomic(target: string, bytes: Buffer | string): void {
252
+ const tempPath = `${target}.${process.pid}.tmp`;
253
+ try {
254
+ mkdirSync(dirname(target), { recursive: true });
255
+ writeFileSync(tempPath, bytes);
256
+ renameSync(tempPath, target);
257
+ } catch (err) {
258
+ rmSync(tempPath, { force: true });
259
+ throw err;
260
+ }
261
+ }
262
+
263
+ interface FetchedPack {
264
+ id: string;
265
+ version: string;
266
+ policies: PolicyCatalogEntry[];
267
+ effect?: PolicyEffect;
268
+ artifact: Buffer;
269
+ artifactDigest: string;
270
+ }
271
+
272
+ /** Fetch and fully validate a pack, without writing anything. */
273
+ /**
274
+ * The short names for the pack Failproof AI publishes.
275
+ *
276
+ * Lives here, not in the CLI, because the dashboard takes the same typed source
277
+ * and has to resolve it the same way — `core` worked in the terminal and failed
278
+ * in the browser for exactly as long as this list sat in `pack-cli.ts`.
279
+ */
280
+ export const CORE_ALIASES = new Set(["core", "failproofai", "official"]);
281
+
282
+ export interface PackAddOptions {
283
+ only?: string[];
284
+ categories?: string[];
285
+ all?: boolean;
286
+ }
287
+
288
+ export interface AddedPack {
289
+ id: string;
290
+ version: string;
291
+ enabled: string[];
292
+ available: string[];
293
+ /** True when it came from the copy inside this package rather than a release. */
294
+ bundled: boolean;
295
+ source?: string;
296
+ tag?: string;
297
+ resolvedFromLatest?: boolean;
298
+ selection?: string;
299
+ categories?: string[];
300
+ }
301
+
302
+ /**
303
+ * Install a pack from whatever a person typed — the one entry point for both the
304
+ * CLI and the dashboard, so a source that works in one cannot fail in the other.
305
+ */
306
+ export async function addPackFromSource(
307
+ source: string,
308
+ opts: PackAddOptions = {},
309
+ ): Promise<AddedPack> {
310
+ if (CORE_ALIASES.has(source.trim().toLowerCase())) {
311
+ const result = installBundledPack(opts);
312
+ if (!result.installed) throw new Error(result.reason ?? "could not install");
313
+ return {
314
+ id: result.id!,
315
+ version: result.version!,
316
+ enabled: result.enabled ?? result.available ?? [],
317
+ available: result.available ?? [],
318
+ bundled: true,
319
+ };
320
+ }
321
+ const result = await addPack(source, opts);
322
+ return {
323
+ id: result.id,
324
+ version: result.version,
325
+ enabled: result.enabled,
326
+ available: result.available,
327
+ bundled: false,
328
+ source: result.source,
329
+ tag: result.tag,
330
+ resolvedFromLatest: result.resolvedFromLatest,
331
+ selection: result.selection,
332
+ categories: result.categories,
333
+ };
334
+ }
335
+
336
+ export interface PackPreview {
337
+ id: string;
338
+ version: string;
339
+ effect: PolicyEffect;
340
+ policies: PolicyCatalogEntry[];
341
+ /** The exact source the preview was read from, tag resolved and pinned. */
342
+ source: string;
343
+ /** True when the tag was resolved rather than typed. */
344
+ resolvedFromLatest: boolean;
345
+ }
346
+
347
+ /**
348
+ * Read what a pack CONTAINS, without installing it and without running it.
349
+ *
350
+ * The only way to see a stranger's policies was to install them, which is the
351
+ * wrong order: deciding whether to trust a pack should not require already
352
+ * having trusted it. This fetches the MANIFEST and nothing else — the entry
353
+ * artifact is never downloaded and never imported, so a preview cannot execute
354
+ * a line of somebody else's code.
355
+ *
356
+ * The manifest is still verified against the release's own SHA256SUMS and parsed
357
+ * with the loader's rules, so what is shown is what would install — and a pack
358
+ * that could never load says so here rather than after the download.
359
+ */
360
+ export async function fetchPackPreview(source: string): Promise<PackPreview> {
361
+ if (process.env.FAILPROOFAI_NO_DOWNLOAD === "1") {
362
+ throw new Error("FAILPROOFAI_NO_DOWNLOAD is set, so nothing can be fetched to preview");
363
+ }
364
+ const parsed = parsePackSpec(source);
365
+ const resolvedFromLatest = parsed.tag === null;
366
+ const spec: PinnedPackSpec =
367
+ parsed.tag !== null
368
+ ? { ...parsed, tag: parsed.tag }
369
+ : { ...parsed, tag: await resolveLatestTag(parsed) };
370
+
371
+ const checksums = (await fetchBytes(packAssetUrl(spec, PACK_CHECKSUMS_ASSET))).toString("utf8");
372
+ const manifestBytes = await fetchBytes(packAssetUrl(spec, PACK_MANIFEST_ASSET));
373
+ const manifestDigest = digestFor(checksums, PACK_MANIFEST_ASSET);
374
+ if (!manifestDigest) {
375
+ throw new Error(`${PACK_CHECKSUMS_ASSET} has no entry for ${PACK_MANIFEST_ASSET}`);
376
+ }
377
+ if (sha256(manifestBytes) !== manifestDigest) {
378
+ throw new Error(`${PACK_MANIFEST_ASSET} failed integrity verification`);
379
+ }
380
+
381
+ let raw: unknown;
382
+ try {
383
+ raw = JSON.parse(manifestBytes.toString("utf8"));
384
+ } catch (err) {
385
+ throw new Error(`${PACK_MANIFEST_ASSET} is not valid JSON: ${errText(err)}`);
386
+ }
387
+ if (!raw || typeof raw !== "object") throw new Error(`${PACK_MANIFEST_ASSET} is not an object`);
388
+ const value = raw as { id?: unknown; version?: unknown; policies?: unknown; effect?: unknown };
389
+ const identity = parsePackIdentity(value);
390
+ if (!Array.isArray(value.policies) || value.policies.length === 0) {
391
+ throw new Error("pack manifest declares no policies");
392
+ }
393
+ return {
394
+ id: identity.id,
395
+ version: identity.version,
396
+ effect: identity.effect,
397
+ policies: value.policies.map((policy, i) => parsePackPolicy(identity.id, policy, i)),
398
+ source: formatPackSpec(spec),
399
+ resolvedFromLatest,
400
+ };
401
+ }
402
+
403
+ /**
404
+ * The record this install is really an update OF — matched by id, or failing
405
+ * that by artifact digest.
406
+ *
407
+ * The digest half matters because an id can be renamed under a user: the
408
+ * published release calls this set `failproofai/builtins` and the vendored copy
409
+ * calls it `failproofai/core`. Looking up by id alone found nothing, so the
410
+ * install fell back to the pack's DEFAULTS and quietly discarded whatever the
411
+ * user had chosen — while reporting that their selection was kept.
412
+ */
413
+ function priorRecordFor(id: string, sha256: string): ResolvedPack | undefined {
414
+ const installed = readInstalledPacks().packs;
415
+ return installed.find((p) => p.id === id) ?? installed.find((p) => p.sha256 === sha256);
416
+ }
417
+
418
+ async function fetchPack(spec: PinnedPackSpec): Promise<FetchedPack> {
419
+ const checksums = (await fetchBytes(packAssetUrl(spec, PACK_CHECKSUMS_ASSET))).toString("utf8");
420
+
421
+ const manifestBytes = await fetchBytes(packAssetUrl(spec, PACK_MANIFEST_ASSET));
422
+ const manifestDigest = digestFor(checksums, PACK_MANIFEST_ASSET);
423
+ if (!manifestDigest) throw new Error(`${PACK_CHECKSUMS_ASSET} has no entry for ${PACK_MANIFEST_ASSET}`);
424
+ if (sha256(manifestBytes) !== manifestDigest) {
425
+ throw new Error(`${PACK_MANIFEST_ASSET} failed integrity verification`);
426
+ }
427
+
428
+ const artifact = await fetchBytes(packAssetUrl(spec, PACK_ENTRY_ASSET));
429
+ const entryDigest = digestFor(checksums, PACK_ENTRY_ASSET);
430
+ if (!entryDigest) throw new Error(`${PACK_CHECKSUMS_ASSET} has no entry for ${PACK_ENTRY_ASSET}`);
431
+ const artifactDigest = sha256(artifact);
432
+ if (artifactDigest !== entryDigest) {
433
+ throw new Error(`${PACK_ENTRY_ASSET} failed integrity verification`);
434
+ }
435
+
436
+ let parsed: unknown;
437
+ try {
438
+ parsed = JSON.parse(manifestBytes.toString("utf8"));
439
+ } catch (err) {
440
+ throw new Error(`${PACK_MANIFEST_ASSET} is not valid JSON: ${errText(err)}`);
441
+ }
442
+ if (!parsed || typeof parsed !== "object") throw new Error(`${PACK_MANIFEST_ASSET} is not an object`);
443
+ const raw = parsed as { id?: unknown; version?: unknown; policies?: unknown; effect?: unknown };
444
+ const identity = parsePackIdentity(raw);
445
+ if (!Array.isArray(raw.policies) || raw.policies.length === 0) {
446
+ throw new Error("pack manifest declares no policies");
447
+ }
448
+ // Validated with the SAME rules the loader applies, so a pack that could never
449
+ // load is refused here — while nothing has been written — rather than
450
+ // installing cleanly and failing silently on the next tool call.
451
+ const policies = raw.policies.map((p, i) => parsePackPolicy(identity.id, p, i));
452
+
453
+ return {
454
+ id: identity.id,
455
+ version: identity.version,
456
+ policies,
457
+ ...(raw.effect !== undefined ? { effect: identity.effect } : {}),
458
+ artifact,
459
+ artifactDigest,
460
+ };
461
+ }
462
+
463
+ /**
464
+ * Fetch, verify and install a pack, then activate it.
465
+ *
466
+ * `only` takes a subset of the pack's policies. Omitted, the whole pack is
467
+ * taken — and re-adding a pack that already had a selection CARRIES IT FORWARD,
468
+ * because upgrading a pack should not quietly switch on the policies someone
469
+ * chose to leave off.
470
+ */
471
+ export function slugifyCategory(category: string): string {
472
+ return category.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
473
+ }
474
+
475
+ /**
476
+ * Which of a pack's policies to take.
477
+ *
478
+ * The default is the pack's OWN `defaultEnabled` set, not everything. A pack
479
+ * carries an opinion about which of its policies are safe to switch on
480
+ * unattended — for the builtins that is 10 of 38 — and installing the whole
481
+ * thing overrode that opinion with one nobody held. `block-kubectl`,
482
+ * `block-terraform` and `require-ci-green-before-stop` are off by default in the
483
+ * npm package precisely because they interrupt legitimate work.
484
+ */
485
+ function resolveSelection(
486
+ policies: PolicyCatalogEntry[],
487
+ opts: { only?: string[]; categories?: string[]; all?: boolean } | undefined,
488
+ previous: string[] | null | undefined,
489
+ previouslyInstalled: boolean,
490
+ ): { enabled: string[] | null; reason: SelectionReason } {
491
+ const available = policies.map((p) => p.name);
492
+ if (opts?.all) return { enabled: null, reason: "all" };
493
+
494
+ const picked = new Set<string>();
495
+ if (opts?.categories?.length) {
496
+ const wanted = new Set(opts.categories.map(slugifyCategory));
497
+ const known = new Map(policies.map((p) => [slugifyCategory(p.category), p.category]));
498
+ const unknown = [...wanted].filter((c) => !known.has(c));
499
+ if (unknown.length > 0) {
500
+ throw new Error(
501
+ `no such categor${unknown.length === 1 ? "y" : "ies"}: ${unknown.join(", ")} ` +
502
+ `(has: ${[...new Set(policies.map((p) => slugifyCategory(p.category)))].join(", ")})`,
503
+ );
504
+ }
505
+ for (const p of policies) if (wanted.has(slugifyCategory(p.category))) picked.add(p.name);
506
+ }
507
+ if (opts?.only?.length) {
508
+ const unknown = opts.only.filter((n) => !available.includes(n));
509
+ if (unknown.length > 0) {
510
+ throw new Error(`pack does not contain ${unknown.join(", ")}`);
511
+ }
512
+ for (const n of opts.only) picked.add(n);
513
+ }
514
+ if (picked.size > 0) {
515
+ // Kept in the pack's declared order, which is the order everything else
516
+ // presents them in.
517
+ return { enabled: available.filter((n) => picked.has(n)), reason: "selected" };
518
+ }
519
+
520
+ // No flags. An upgrade keeps whatever the machine already had — switching a
521
+ // policy back on because the user did not repeat themselves is not an upgrade.
522
+ if (previouslyInstalled) {
523
+ return { enabled: previous === null ? null : (previous ?? null), reason: "carried" };
524
+ }
525
+ return { enabled: policies.filter((p) => p.defaultEnabled).map((p) => p.name), reason: "defaults" };
526
+ }
527
+
528
+ export type SelectionReason = "all" | "selected" | "carried" | "defaults";
529
+
530
+ /**
531
+ * Import the artifact and check it registers what the manifest promised.
532
+ *
533
+ * `pack add` used to verify digests and manifest SHAPE and then install without
534
+ * ever executing the file. Two failures got through, both reproduced: an
535
+ * artifact that does not parse installed at exit 0 and bricked every later tool
536
+ * call, and a one-name typo between manifest and artifact — the exact slip a
537
+ * publisher hand-maintaining two files makes — installed reporting "2/2
538
+ * enabled" and then converted into a MACHINE-WIDE DENY, because a declared
539
+ * policy that never registers is precisely what the fail-closed guard denies
540
+ * for.
541
+ *
542
+ * The right place to catch that is here, where nothing has been activated yet
543
+ * and the publisher's own mistake can be named. This runs only on the CLI path;
544
+ * the hook path never imports anything it has not already recorded.
545
+ */
546
+ async function verifyArtifactRegisters(
547
+ artifactPath: string,
548
+ declared: readonly string[],
549
+ ): Promise<void> {
550
+ const { loadCustomHooks } = await import("./custom-hooks-loader");
551
+ let registered: string[];
552
+ try {
553
+ registered = (await loadCustomHooks(artifactPath, { strict: true })).map((h) => h.name);
554
+ } catch (err) {
555
+ throw new Error(
556
+ `its artifact could not be loaded: ${err instanceof Error ? err.message : String(err)}`,
557
+ );
558
+ }
559
+ const missing = declared.filter((name) => !registered.includes(name));
560
+ const extra = registered.filter((name) => !declared.includes(name));
561
+ if (missing.length > 0) {
562
+ throw new Error(
563
+ `its manifest declares ${missing.join(", ")} but the artifact does not register ` +
564
+ `${missing.length === 1 ? "it" : "them"} — that would install as protection that never runs`,
565
+ );
566
+ }
567
+ if (extra.length > 0) {
568
+ throw new Error(
569
+ `its artifact registers undeclared ${extra.join(", ")} — a policy that enforces but ` +
570
+ `appears in no listing cannot be reviewed or switched off`,
571
+ );
572
+ }
573
+ }
574
+
575
+ /**
576
+ * The same check, as a question rather than a refusal — for `pack list`, which
577
+ * reported a pack whose artifact does not load as fully healthy, on the very
578
+ * command the fail-closed deny message tells the human to run.
579
+ */
580
+ export async function checkPackArtifact(
581
+ artifactPath: string,
582
+ declared: readonly string[],
583
+ ): Promise<string | null> {
584
+ try {
585
+ await verifyArtifactRegisters(artifactPath, declared);
586
+ return null;
587
+ } catch (err) {
588
+ return err instanceof Error ? err.message : String(err);
589
+ }
590
+ }
591
+
592
+ export async function addPack(
593
+ source: string,
594
+ opts?: { only?: string[]; categories?: string[]; all?: boolean },
595
+ ): Promise<AddPackResult> {
596
+ if (process.env.FAILPROOFAI_NO_DOWNLOAD) {
597
+ throw new Error(
598
+ "pack downloads are disabled (FAILPROOFAI_NO_DOWNLOAD). Already-installed packs keep enforcing.",
599
+ );
600
+ }
601
+ const parsed = parsePackSpec(source);
602
+ // Resolve BEFORE anything is written, and pin the concrete result. A tagless
603
+ // source is a convenience for the person typing; what the machine records must
604
+ // always name one release.
605
+ const spec: PinnedPackSpec = parsed.tag
606
+ ? { ...parsed, tag: parsed.tag }
607
+ : { ...parsed, tag: await resolveLatestTag(parsed) };
608
+ const resolvedFromLatest = parsed.tag === null;
609
+ const fetched = await fetchPack(spec);
610
+ const available = fetched.policies.map((p) => p.name);
611
+
612
+ const prior = priorRecordFor(fetched.id, fetched.artifactDigest);
613
+ const { enabled, reason } = resolveSelection(
614
+ fetched.policies,
615
+ opts,
616
+ prior?.enabled ? prior.enabled.filter((n) => available.includes(n)) : prior?.enabled,
617
+ Boolean(prior),
618
+ );
619
+
620
+ const root = packsRoot();
621
+ const artifactRel = `artifacts/${fetched.artifactDigest}.mjs`;
622
+ const artifactAbs = resolve(root, artifactRel);
623
+ // Content-addressed, so a file that already exists has these exact bytes and
624
+ // rewriting it could only ever disturb a pack currently importing it.
625
+ if (!existsSync(artifactAbs)) writeAtomic(artifactAbs, fetched.artifact);
626
+
627
+ // Before `installed.json` — the file that activates it — is written. The
628
+ // artifact on disk is inert until something points at it, so a refusal here
629
+ // leaves the machine exactly as it was.
630
+ try {
631
+ await verifyArtifactRegisters(artifactAbs, available);
632
+ } catch (err) {
633
+ throw new Error(
634
+ `${fetched.id}@${fetched.version} was not installed: ${err instanceof Error ? err.message : String(err)}`,
635
+ );
636
+ }
637
+
638
+ // An id is self-declared and nothing owns it, while `upsertInstalled` replaces
639
+ // by id — so a pack served from any repository could declare
640
+ // `failproofai/builtins` and silently take the place of the one already
641
+ // installed. Binding the id to the source it first came from is the cheap half
642
+ // of the fix: a second source for the same id has to be an explicit removal.
643
+ // Compared on the REPOSITORY, not the whole source: `github:acme/pack@v1.2.0`
644
+ // and `@v1.3.0` are the same publisher shipping an upgrade, which is the
645
+ // ordinary case this must not break.
646
+ const repoOf = (source: string): string => {
647
+ const at = source.lastIndexOf("@");
648
+ return at > source.indexOf(":") ? source.slice(0, at) : source;
649
+ };
650
+ if (
651
+ prior &&
652
+ !prior.source.startsWith("bundled:") &&
653
+ repoOf(prior.source) !== repoOf(formatPackSpec(spec))
654
+ ) {
655
+ throw new Error(
656
+ `pack id ${fetched.id} is already installed from ${prior.source}. ` +
657
+ `Refusing to replace it with ${formatPackSpec(spec)} — remove it first if that is what you mean.`,
658
+ );
659
+ }
660
+
661
+ const record: InstalledPackRecord = {
662
+ id: fetched.id,
663
+ version: fetched.version,
664
+ source: formatPackSpec(spec),
665
+ entry: artifactRel,
666
+ sha256: fetched.artifactDigest,
667
+ policies: fetched.policies,
668
+ ...(fetched.effect ? { effect: fetched.effect } : {}),
669
+ ...(enabled ? { enabled } : {}),
670
+ };
671
+
672
+ const absorbed = upsertInstalled(record);
673
+ return {
674
+ replaced: absorbed,
675
+ id: fetched.id,
676
+ version: fetched.version,
677
+ source: record.source,
678
+ tag: spec.tag,
679
+ resolvedFromLatest,
680
+ enabled: enabled ?? available,
681
+ available,
682
+ selection: reason,
683
+ categories: [...new Set(fetched.policies.map((p) => slugifyCategory(p.category)))],
684
+ artifact: artifactAbs,
685
+ };
686
+ }
687
+
688
+ /**
689
+ * Install the pack that ships inside the npm package, from disk.
690
+ *
691
+ * This is what makes removing the builtins from the bundle survivable. The
692
+ * tarball carries `policy-pack/` — the builtins as a real, digest-verified pack —
693
+ * so a machine that has just installed failproofai already HAS them and setup
694
+ * can enable them with no network at all. Without this, a fresh install with no
695
+ * connectivity would be a machine enforcing nothing.
696
+ *
697
+ * Anchored at `FAILPROOFAI_PACKAGE_ROOT` for the reason `npmPlatformBinaryPath`
698
+ * documents: `import.meta.url` does not survive the CJS bundle.
699
+ *
700
+ * The bytes are COPIED into `packsDir()` rather than loaded where they lie. A
701
+ * `sudo npm i -g` package directory is root-owned, and the loader writes its
702
+ * rewritten module tree BESIDE the source it loads — so loading in place gives a
703
+ * non-root hook EACCES, the pack never loads, and the hook exits 0. Copying is
704
+ * what avoids that, and it is the same trap CHANGELOG #694 fixed for the shim.
705
+ *
706
+ * Idempotent, and never throws: a package without the directory returns null,
707
+ * which is a normal state for a dev checkout that has not run `build:pack`.
708
+ */
709
+ export function bundledPackDir(): string | null {
710
+ const root = process.env.FAILPROOFAI_PACKAGE_ROOT;
711
+ if (!root) return null;
712
+ const dir = resolve(root, "policy-pack");
713
+ return existsSync(resolve(dir, PACK_MANIFEST_ASSET)) ? dir : null;
714
+ }
715
+
716
+ export interface BundledPackResult {
717
+ installed: boolean;
718
+ id?: string;
719
+ version?: string;
720
+ enabled?: string[];
721
+ available?: string[];
722
+ /** Ids this install absorbed — the same pack, previously under another name. */
723
+ replaced?: string[];
724
+ reason?: string;
725
+ }
726
+
727
+ export function installBundledPack(opts?: { only?: string[]; categories?: string[]; all?: boolean }): BundledPackResult {
728
+ const dir = bundledPackDir();
729
+ if (!dir) return { installed: false, reason: "this build ships no bundled pack" };
730
+
731
+ try {
732
+ const checksums = readFileSync(resolve(dir, PACK_CHECKSUMS_ASSET), "utf8");
733
+ const manifestBytes = readFileSync(resolve(dir, PACK_MANIFEST_ASSET));
734
+ const artifact = readFileSync(resolve(dir, PACK_ENTRY_ASSET));
735
+
736
+ // Verified even though it never crossed a network. The digest is what the
737
+ // hook path re-checks before every import, so it has to describe the bytes
738
+ // actually installed — and a tarball can be corrupted on disk like anything
739
+ // else.
740
+ const manifestDigest = digestFor(checksums, PACK_MANIFEST_ASSET);
741
+ const entryDigest = digestFor(checksums, PACK_ENTRY_ASSET);
742
+ const artifactDigest = sha256(artifact);
743
+ if (!manifestDigest || sha256(manifestBytes) !== manifestDigest) {
744
+ return { installed: false, reason: `${PACK_MANIFEST_ASSET} failed integrity verification` };
745
+ }
746
+ if (!entryDigest || artifactDigest !== entryDigest) {
747
+ return { installed: false, reason: `${PACK_ENTRY_ASSET} failed integrity verification` };
748
+ }
749
+
750
+ const parsed = JSON.parse(manifestBytes.toString("utf8")) as {
751
+ id?: unknown; version?: unknown; policies?: unknown; effect?: unknown;
752
+ };
753
+ if (!Array.isArray(parsed.policies)) {
754
+ return { installed: false, reason: "bundled pack manifest is malformed" };
755
+ }
756
+ const identity = parsePackIdentity(parsed);
757
+ // Same rules the loader applies, so a bundled pack that could never load
758
+ // fails the install rather than looking fine until the next tool call.
759
+ const policies = parsed.policies.map((pol, i) => parsePackPolicy(identity.id, pol, i));
760
+
761
+ const root = packsRoot();
762
+ const artifactRel = `artifacts/${artifactDigest}.mjs`;
763
+ const artifactAbs = resolve(root, artifactRel);
764
+ if (!existsSync(artifactAbs)) writeAtomic(artifactAbs, artifact);
765
+
766
+ const prior = priorRecordFor(identity.id, artifactDigest);
767
+ const available = policies.map((pol) => pol.name);
768
+ const { enabled } = resolveSelection(
769
+ policies,
770
+ opts,
771
+ prior?.enabled ? prior.enabled.filter((n) => available.includes(n)) : prior?.enabled,
772
+ Boolean(prior),
773
+ );
774
+
775
+ const absorbed = upsertInstalled({
776
+ id: identity.id,
777
+ version: identity.version,
778
+ // Recorded as `bundled:` rather than a github: source, because it did not
779
+ // come from one — and `pack add` on this id would otherwise look like a
780
+ // re-fetch of something that was never fetched.
781
+ source: `bundled:${identity.id}@${identity.version}`,
782
+ entry: artifactRel,
783
+ sha256: artifactDigest,
784
+ policies,
785
+ ...(parsed.effect !== undefined ? { effect: identity.effect } : {}),
786
+ ...(enabled ? { enabled } : {}),
787
+ });
788
+
789
+ return {
790
+ installed: true,
791
+ id: identity.id,
792
+ version: identity.version,
793
+ enabled: enabled ?? available,
794
+ available,
795
+ ...(absorbed.length > 0 ? { replaced: absorbed } : {}),
796
+ };
797
+ } catch (err) {
798
+ return { installed: false, reason: errText(err) };
799
+ }
800
+ }
801
+
802
+ /** Remove a pack from the activation pointer. Returns false if it was not installed. */
803
+ /**
804
+ * Turn ONE policy of an installed pack on or off.
805
+ *
806
+ * The selection in `installed.json` is the lever, not a `disabledCustomPolicies`
807
+ * entry: those are keyed by `pack:<id>@<version>:<name>`, so an upgrade stops
808
+ * matching them and silently switches back on everything the user had turned
809
+ * off. The selection is version-stable and is carried forward by `pack add`.
810
+ *
811
+ * Enabling also clears any matching disabled key, or a policy switched off from
812
+ * the dashboard could not be switched back on from the CLI — the two mechanisms
813
+ * would disagree and the more obscure one would win.
814
+ */
815
+ export function setPackPolicyEnabled(
816
+ packId: string,
817
+ name: string,
818
+ on: boolean,
819
+ ): { ok: boolean; reason?: string } {
820
+ const manifestPath = resolve(packsRoot(), "installed.json");
821
+ if (!existsSync(manifestPath)) return { ok: false, reason: "no packs are installed" };
822
+ let raw: { schemaVersion?: number; packs?: InstalledPackRecord[] };
823
+ try {
824
+ raw = JSON.parse(readFileSync(manifestPath, "utf8"));
825
+ } catch {
826
+ return { ok: false, reason: `pack manifest at ${manifestPath} is unreadable` };
827
+ }
828
+ const packs = Array.isArray(raw.packs) ? raw.packs : [];
829
+ const pack = packs.find((p) => p?.id === packId);
830
+ if (!pack) return { ok: false, reason: `no installed pack with id ${packId}` };
831
+ // `policies` is `unknown` on the record — it is validated by the loader, not
832
+ // here — so read the names defensively rather than trusting the shape.
833
+ const declared = Array.isArray(pack.policies) ? (pack.policies as Array<{ name?: unknown }>) : [];
834
+ const available = declared
835
+ .map((p) => (typeof p?.name === "string" ? p.name : null))
836
+ .filter((n): n is string => n !== null);
837
+ if (!available.includes(name)) {
838
+ return { ok: false, reason: `pack ${packId} declares no policy named ${name}` };
839
+ }
840
+ // `enabled` absent means "the whole pack", which is what `--all` records so a
841
+ // later version's new policies are included. Turning one off has to make that
842
+ // implicit set explicit, or there is nothing to subtract from.
843
+ const current = pack.enabled ?? available;
844
+ const next: string[] = on
845
+ ? [...new Set([...current, name])]
846
+ : current.filter((n: string) => n !== name);
847
+ // Kept in the pack's declared order rather than the order things were toggled.
848
+ pack.enabled = available.filter((n) => next.includes(n));
849
+ writeAtomic(manifestPath, JSON.stringify({ schemaVersion: 1, packs }, null, 2) + "\n");
850
+ return { ok: true };
851
+ }
852
+
853
+ export function removePack(id: string): boolean {
854
+ const manifestPath = resolve(packsRoot(), "installed.json");
855
+ if (!existsSync(manifestPath)) return false;
856
+ let raw: { schemaVersion?: number; packs?: InstalledPackRecord[] };
857
+ try {
858
+ raw = JSON.parse(readFileSync(manifestPath, "utf8"));
859
+ } catch {
860
+ return false;
861
+ }
862
+ const packs = Array.isArray(raw.packs) ? raw.packs : [];
863
+ const remaining = packs.filter((p) => p?.id !== id);
864
+ if (remaining.length === packs.length) return false;
865
+ // The artifact is left on disk deliberately: it is content-addressed and inert
866
+ // once nothing points at it, so keeping it makes a re-add offline-safe. Same
867
+ // choice the cloud reconciler makes.
868
+ writeAtomic(manifestPath, JSON.stringify({ schemaVersion: 1, packs: remaining }, null, 2) + "\n");
869
+ return true;
870
+ }
871
+
872
+ /**
873
+ * Replace or append one pack in `installed.json`, written LAST and atomically so
874
+ * activation is a single flip: until this rename lands, the artifact on disk is
875
+ * a file nothing points at.
876
+ */
877
+ /**
878
+ * Install or update one pack record, collapsing anything that is the SAME PACK
879
+ * under a different id.
880
+ *
881
+ * Two ids for one artifact is not hypothetical: the published release calls this
882
+ * set `failproofai/builtins` and the copy vendored in the package calls it
883
+ * `failproofai/core`, so following the help's own two examples installed both.
884
+ * The result claimed "2 packs · 76 policies" over ONE artifact, warned on stderr
885
+ * on every hook event, and left policies showing ON that did not block.
886
+ *
887
+ * Matched on the artifact DIGEST, which is what actually decides whether two
888
+ * records describe the same code. Returns the ids it absorbed so the caller can
889
+ * say so — an earlier version deleted the old row silently, taking the user's
890
+ * selection and the enforcement it gave them with it.
891
+ */
892
+ function upsertInstalled(record: InstalledPackRecord): string[] {
893
+ const manifestPath = resolve(packsRoot(), "installed.json");
894
+ let packs: InstalledPackRecord[] = [];
895
+ if (existsSync(manifestPath)) {
896
+ try {
897
+ const raw = JSON.parse(readFileSync(manifestPath, "utf8")) as { packs?: InstalledPackRecord[] };
898
+ if (Array.isArray(raw.packs)) packs = raw.packs;
899
+ } catch {
900
+ // A manifest we cannot parse is one we must not append to: merging into
901
+ // it would either drop the packs it held or write them back malformed.
902
+ throw new Error(
903
+ `existing pack manifest at ${manifestPath} is unreadable; move it aside and re-add`,
904
+ );
905
+ }
906
+ }
907
+ // Anything with these bytes under another name is this pack, renamed.
908
+ const absorbed = packs
909
+ .filter((p) => p?.sha256 === record.sha256 && p?.id !== record.id)
910
+ .map((p) => p.id);
911
+ // The selection the user had under the old id is theirs — carry it, rather
912
+ // than resetting them to defaults because a publisher renamed something.
913
+ if (!record.enabled) {
914
+ const prior = packs.find((p) => absorbed.includes(p?.id));
915
+ const carried = prior?.enabled?.filter((n) =>
916
+ (record.policies as Array<{ name?: unknown }> | undefined)?.some((rp) => rp?.name === n),
917
+ );
918
+ if (carried && carried.length > 0) record.enabled = carried;
919
+ }
920
+ const remaining = packs.filter((p) => !absorbed.includes(p?.id));
921
+ const idx = remaining.findIndex((p) => p?.id === record.id);
922
+ if (idx >= 0) remaining[idx] = record;
923
+ else remaining.push(record);
924
+ writeAtomic(manifestPath, JSON.stringify({ schemaVersion: 1, packs: remaining }, null, 2) + "\n");
925
+ return absorbed;
926
+ }
927
+
928
+ function errText(err: unknown): string {
929
+ return err instanceof Error ? err.message : String(err);
930
+ }