failproofai 1.0.0-beta.2 → 1.0.0-beta.4

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 (234) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  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 +2 -2
  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/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  17. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/run/route.js +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  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/reminder/route.js.nft.json +1 -1
  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 +1 -1
  37. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  39. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  40. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  41. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/index.html +1 -1
  44. package/.next/standalone/.next/server/app/index.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  47. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  48. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  49. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  50. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  51. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  55. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
  56. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  57. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  60. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  67. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +1 -1
  68. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  71. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  72. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  73. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/chunks/[externals]__0h9_83k._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-dh5jy._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0f8nu8d._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0h3lfmp._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l4vuqm._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0longh-._.js → [root-of-the-server]__0szqzl8._.js} +2 -2
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10oa-v9._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__18d-9oi._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__1p7s8-d._.js} +2 -2
  88. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
  89. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
  90. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
  91. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0nb0syg._.js → [root-of-the-server]__0cmdyn_._.js} +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ath6v_._.js → [root-of-the-server]__0ls0gsr._.js} +3 -3
  98. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0uno39i._.js → [root-of-the-server]__0n_uvv0._.js} +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__0rag5dg._.js} +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__0rtdfof._.js} +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__0sdrz-j._.js} +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
  108. package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0oy_-36.js → node_modules_html-to-image_es_index_1q9xxca.js} +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -1
  114. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  116. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  117. package/.next/standalone/.next/server/pages/404.html +1 -1
  118. package/.next/standalone/.next/server/pages/500.html +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/server-reference-manifest.json +32 -20
  121. package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 06jakt_xhoolk.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/0oe2-hp0_2tzs.js +6 -0
  124. package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 0ue9ak_xexiqh.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
  127. package/.next/standalone/.next/static/chunks/1fw3pxobk_otl.js +1 -0
  128. package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
  129. package/.next/standalone/.next/static/chunks/2c90h1g6x8z8y.js +1 -0
  130. package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 2zb3m1tzq6h98.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
  132. package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 31cqiv_cpgj9m.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 3h5_3u46gu9jx.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
  135. package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 43a1ywqsg9bq9.js} +1 -1
  136. package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
  137. package/.next/standalone/Cargo.lock +1992 -198
  138. package/.next/standalone/Cargo.toml +1 -1
  139. package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
  140. package/.next/standalone/app/components/pause-notices.tsx +111 -0
  141. package/.next/standalone/app/policies/hooks-client.tsx +81 -6
  142. package/.next/standalone/crates/CLOUD_POLICIES.md +136 -0
  143. package/.next/standalone/crates/failproofaid/Cargo.toml +7 -0
  144. package/.next/standalone/crates/failproofaid/src/cloud_client.rs +484 -0
  145. package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +921 -0
  146. package/.next/standalone/crates/failproofaid/src/main.rs +452 -3
  147. package/.next/standalone/crates/failproofaid/src/paths.rs +29 -0
  148. package/.next/standalone/crates/fpai-collect/Cargo.toml +43 -0
  149. package/.next/standalone/crates/fpai-collect/src/config.rs +412 -0
  150. package/.next/standalone/crates/fpai-collect/src/cursor.rs +520 -0
  151. package/.next/standalone/crates/fpai-collect/src/delivery.rs +472 -0
  152. package/.next/standalone/crates/fpai-collect/src/filetail.rs +484 -0
  153. package/.next/standalone/crates/fpai-collect/src/health.rs +369 -0
  154. package/.next/standalone/crates/fpai-collect/src/lib.rs +34 -0
  155. package/.next/standalone/crates/fpai-collect/src/redact.rs +516 -0
  156. package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +279 -0
  157. package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +619 -0
  158. package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +156 -0
  159. package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +715 -0
  160. package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +158 -0
  161. package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +512 -0
  162. package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +511 -0
  163. package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +586 -0
  164. package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +318 -0
  165. package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +579 -0
  166. package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +310 -0
  167. package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +448 -0
  168. package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +23 -0
  169. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +176 -0
  170. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +500 -0
  171. package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +315 -0
  172. package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +493 -0
  173. package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +201 -0
  174. package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +533 -0
  175. package/.next/standalone/crates/fpai-collect/src/spool.rs +458 -0
  176. package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +387 -0
  177. package/.next/standalone/crates/fpai-collect/src/supervisor.rs +423 -0
  178. package/.next/standalone/crates/fpai-collect/src/uploader.rs +600 -0
  179. package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +738 -0
  180. package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +393 -0
  181. package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +975 -0
  182. package/.next/standalone/crates/fpai-collect/tests/config.rs +286 -0
  183. package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1019 -0
  184. package/.next/standalone/crates/fpai-collect/tests/delivery.rs +281 -0
  185. package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1008 -0
  186. package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1129 -0
  187. package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +621 -0
  188. package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +854 -0
  189. package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +1478 -0
  190. package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1191 -0
  191. package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +290 -0
  192. package/.next/standalone/crates/fpai-collect/tests/uploader.rs +506 -0
  193. package/.next/standalone/package.json +15 -9
  194. package/.next/standalone/server.js +1 -1
  195. package/bin/failproofai.mjs +129 -0
  196. package/dist/cli.mjs +2255 -779
  197. package/dist/worker.mjs +365 -52
  198. package/package.json +15 -9
  199. package/scripts/build-daemon-packages.mjs +210 -0
  200. package/scripts/daemon-platforms.mjs +42 -0
  201. package/scripts/prune-standalone.mjs +6 -0
  202. package/scripts/publish-aliases.mjs +62 -41
  203. package/src/hooks/builtin-policies.ts +139 -0
  204. package/src/hooks/cloud-connection.ts +205 -0
  205. package/src/hooks/cloud-enrollment-cli.ts +227 -0
  206. package/src/hooks/cloud-enrollment.ts +173 -0
  207. package/src/hooks/cloud-managed-policies.ts +138 -0
  208. package/src/hooks/collector-config.ts +226 -0
  209. package/src/hooks/configure-wizard.ts +221 -5
  210. package/src/hooks/custom-hooks-loader.ts +69 -25
  211. package/src/hooks/daemon-client.ts +18 -0
  212. package/src/hooks/daemon-download.ts +137 -27
  213. package/src/hooks/daemon-service.ts +23 -8
  214. package/src/hooks/handler.ts +144 -6
  215. package/src/hooks/hook-activity-store.ts +50 -0
  216. package/src/hooks/loader-utils.ts +6 -4
  217. package/src/hooks/policy-types.ts +35 -0
  218. package/src/hooks/session-pause-cli.ts +157 -0
  219. package/src/hooks/session-pause.ts +236 -0
  220. package/src/hooks/tui.ts +115 -0
  221. package/.next/standalone/.next/server/chunks/[externals]__14odj07._.js +0 -3
  222. package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
  223. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
  224. package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
  225. package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
  226. package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
  227. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
  228. package/.next/standalone/.next/static/chunks/31feojxhjyjpl.js +0 -1
  229. package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
  230. package/.next/standalone/.next/static/chunks/3s7myk_co0gbh.js +0 -1
  231. package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
  232. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_buildManifest.js +0 -0
  233. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_clientMiddlewareManifest.js +0 -0
  234. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.4",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -76,19 +76,19 @@
76
76
  "@anthropic-ai/sdk": "^0.115.0",
77
77
  "@mdx-js/mdx": "^3.1.1",
78
78
  "@tailwindcss/postcss": "^4.3.1",
79
- "@tanstack/react-virtual": "^3.14.3",
79
+ "@tanstack/react-virtual": "^3.14.9",
80
80
  "@testing-library/jest-dom": "^7.0.0",
81
81
  "@testing-library/react": "^16.3.2",
82
82
  "@testing-library/user-event": "^14.6.1",
83
- "@types/node": "26.1.1",
83
+ "@types/node": "26.1.2",
84
84
  "@types/react": "19.2.17",
85
85
  "@types/react-dom": "^19.2.3",
86
- "@vitejs/plugin-react": "^6.0.1",
86
+ "@vitejs/plugin-react": "^6.0.5",
87
87
  "clsx": "^2.1.1",
88
88
  "eslint": "^10.5.0",
89
89
  "eslint-config-next": "^16.2.9",
90
- "jsdom": "^30.0.0",
91
- "lucide-react": "^1.18.0",
90
+ "jsdom": "^30.0.1",
91
+ "lucide-react": "^1.28.0",
92
92
  "next": "^16.2.11",
93
93
  "react": "^19.2.4",
94
94
  "react-dom": "^19.2.4",
@@ -100,7 +100,7 @@
100
100
  "dependencies": {
101
101
  "html-to-image": "^1.11.13",
102
102
  "html2canvas": "^1.4.1",
103
- "posthog-node": "^5.37.1",
103
+ "posthog-node": "^5.47.7",
104
104
  "sql.js": "^1.14.1",
105
105
  "yaml": "^2.9.0"
106
106
  },
@@ -108,8 +108,14 @@
108
108
  "postcss": "8.5.23",
109
109
  "eslint-plugin-react-hooks": "7.0.1",
110
110
  "vite": "8.0.16",
111
- "undici": "7.28.0",
112
- "brace-expansion": "5.0.8",
111
+ "undici": "7.29.0",
112
+ "brace-expansion": "5.0.9",
113
113
  "sharp": "0.35.0"
114
+ },
115
+ "optionalDependencies": {
116
+ "@failproofai/failproofaid-linux-x64": "1.0.0-beta.4",
117
+ "@failproofai/failproofaid-linux-arm64": "1.0.0-beta.4",
118
+ "@failproofai/failproofaid-darwin-x64": "1.0.0-beta.4",
119
+ "@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.4"
114
120
  }
115
121
  }
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Publishes the four `failproofaid` binaries as npm platform packages, so a
4
+ * plain `npm install failproofai` already carries the daemon for the machine
5
+ * it landed on instead of fetching one from the GitHub Release later.
6
+ *
7
+ * The release assets stay exactly as they are — this is a second channel, not
8
+ * a replacement. `src/hooks/daemon-download.ts` prefers whichever binary is
9
+ * already on disk, then this npm package, then the release; the npm path is
10
+ * the only one that works with no network at `failproofai config` time.
11
+ *
12
+ * Two rules make this safe, and both come from the way the first attempt at
13
+ * npm-shipping the daemon failed (CHANGELOG 1.0.0-beta.3 — four
14
+ * `optionalDependencies` were declared and never published, so every install
15
+ * resolved four 404s):
16
+ *
17
+ * 1. These packages publish BEFORE the root package that pins them, so the
18
+ * names always exist by the time anything depends on them.
19
+ * 2. A publish failure here is fatal, not a warning (unlike the alias stubs
20
+ * in publish-aliases.mjs, where a missing name costs nothing). Failing
21
+ * the job leaves the registry untouched; publishing a root package whose
22
+ * optional dependency 404s would ship the broken state to every user.
23
+ *
24
+ * Usage:
25
+ * node scripts/build-daemon-packages.mjs --artifacts <dir> [--dist-tag <tag>]
26
+ * [--version <v>] [--pin-root] [--dry-run]
27
+ */
28
+ import { execFileSync } from "node:child_process";
29
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
30
+ import { tmpdir } from "node:os";
31
+ import { dirname, join, resolve } from "node:path";
32
+ import { fileURLToPath, pathToFileURL } from "node:url";
33
+ import { gunzipSync } from "node:zlib";
34
+ import {
35
+ DAEMON_PLATFORMS,
36
+ daemonAssetName,
37
+ daemonOptionalDependencies,
38
+ daemonPackageName,
39
+ } from "./daemon-platforms.mjs";
40
+
41
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
42
+
43
+ /**
44
+ * Where the generated packages are staged — **outside the repo**, and that is
45
+ * load-bearing rather than tidy.
46
+ *
47
+ * `npm publish` re-runs `prepare`, so the Next build happens again *after*
48
+ * this script has run, and Next's file tracing pulls the whole project root
49
+ * into `.next/standalone` (see the "over-traced project artifacts" prune list
50
+ * in scripts/prune-standalone.mjs). Anything staged inside the checkout
51
+ * therefore ends up inside the published CLI tarball — measured: a dry run
52
+ * shipped 16 MB of daemon `.gz` assets under
53
+ * `.next/standalone/release-assets/`. Staging in the temp dir cannot be swept
54
+ * up by a build. `--staging` overrides it for tests.
55
+ */
56
+ export const STAGING_DIR = resolve(tmpdir(), "failproofaid-packages");
57
+
58
+ /**
59
+ * The manifest for one platform package.
60
+ *
61
+ * Deliberately carries no `bin` (it would shadow the root package's own
62
+ * `failproofaid` shim, which is what a user invoking the daemon by hand
63
+ * should reach) and no `exports` (so `require.resolve("<pkg>/package.json")`
64
+ * — how the CLI finds this package — keeps working).
65
+ */
66
+ export function platformPackageManifest(platform, version, rootPkg = {}) {
67
+ return {
68
+ name: daemonPackageName(platform.key),
69
+ version,
70
+ description: `The failproofaid background daemon binary for ${platform.os} ${platform.cpu}. Installed automatically as an optional dependency of failproofai.`,
71
+ // npm's own platform filters: with both set, only the matching package is
72
+ // installed and the other three are skipped without a warning.
73
+ os: [platform.os],
74
+ cpu: [platform.cpu],
75
+ files: ["bin/"],
76
+ publishConfig: { access: "public" },
77
+ repository: rootPkg.repository,
78
+ homepage: rootPkg.homepage,
79
+ bugs: rootPkg.bugs,
80
+ license: rootPkg.license,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Stages one platform package on disk and returns its directory.
86
+ *
87
+ * The binary is written decompressed and mode 0755: npm records the executable
88
+ * bit in the tarball, so it survives publish → install, which is how esbuild
89
+ * and swc ship their binaries.
90
+ */
91
+ export function stagePlatformPackage(platform, version, rootPkg, artifactsDir, stagingRoot = STAGING_DIR) {
92
+ const asset = resolve(artifactsDir, daemonAssetName(platform.key));
93
+ if (!existsSync(asset)) {
94
+ throw new Error(`missing artifact ${asset} — the daemon build did not produce ${daemonAssetName(platform.key)}`);
95
+ }
96
+
97
+ const dir = join(stagingRoot, `failproofaid-${platform.key}`);
98
+ rmSync(dir, { recursive: true, force: true });
99
+ mkdirSync(join(dir, "bin"), { recursive: true });
100
+
101
+ const binary = gunzipSync(readFileSync(asset));
102
+ const binaryPath = join(dir, "bin", "failproofaid");
103
+ writeFileSync(binaryPath, binary);
104
+ chmodSync(binaryPath, 0o755);
105
+
106
+ writeFileSync(
107
+ join(dir, "package.json"),
108
+ JSON.stringify(platformPackageManifest(platform, version, rootPkg), null, 2) + "\n",
109
+ );
110
+ writeFileSync(
111
+ join(dir, "README.md"),
112
+ `# ${daemonPackageName(platform.key)}\n\n` +
113
+ `The \`failproofaid\` background daemon binary for ${platform.os} ${platform.cpu}.\n\n` +
114
+ `Installed automatically as an optional dependency of [failproofai](https://www.npmjs.com/package/failproofai) — ` +
115
+ `you do not need to install it yourself.\n`,
116
+ );
117
+
118
+ return dir;
119
+ }
120
+
121
+ /**
122
+ * Writes the four platform packages into the root manifest's
123
+ * `optionalDependencies`, pinned to `version`.
124
+ *
125
+ * Called at publish time rather than committed: a pin to a version that does
126
+ * not exist yet breaks `bun install --frozen-lockfile` in this repo's own CI,
127
+ * and keeping four pins in step with every version bump by hand is the drift
128
+ * this whole file exists to avoid.
129
+ */
130
+ export function pinRootManifest(version, packageJsonPath = resolve(REPO_ROOT, "package.json")) {
131
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
132
+ pkg.optionalDependencies = { ...pkg.optionalDependencies, ...daemonOptionalDependencies(version) };
133
+ writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
134
+ return pkg.optionalDependencies;
135
+ }
136
+
137
+ function parseArgs(argv) {
138
+ const valueOf = (flag, fallback) => {
139
+ const index = argv.indexOf(flag);
140
+ return index !== -1 && argv[index + 1] ? argv[index + 1] : fallback;
141
+ };
142
+ return {
143
+ artifacts: valueOf("--artifacts", "release-assets"),
144
+ staging: valueOf("--staging", STAGING_DIR),
145
+ distTag: valueOf("--dist-tag", null),
146
+ version: valueOf("--version", null),
147
+ pinRoot: argv.includes("--pin-root"),
148
+ dryRun: argv.includes("--dry-run"),
149
+ };
150
+ }
151
+
152
+ /**
153
+ * `npm publish`, with provenance when the registry will take it.
154
+ *
155
+ * Provenance needs a supported CI and a repository field npm can match to the
156
+ * building workflow; when it refuses, the publish itself is still correct, so
157
+ * this retries once without the flag rather than failing the release.
158
+ */
159
+ function publishPackage(dir, distTag, dryRun) {
160
+ const args = ["publish", "--tag", distTag];
161
+ if (dryRun) args.push("--dry-run");
162
+
163
+ const attempt = (extra) =>
164
+ execFileSync("npm", [...args, ...extra], { cwd: dir, stdio: "pipe", encoding: "utf8" });
165
+
166
+ const withProvenance = !dryRun && process.env.GITHUB_ACTIONS === "true";
167
+ try {
168
+ return attempt(withProvenance ? ["--provenance"] : []);
169
+ } catch (err) {
170
+ const output = (err.stdout ?? "") + (err.stderr ?? "");
171
+ if (output.includes("cannot publish over")) return "[skip] already published at this version";
172
+ if (withProvenance && /provenance/i.test(output)) {
173
+ console.log("::warning::provenance was rejected — retrying the publish without it");
174
+ return attempt([]);
175
+ }
176
+ // Fatal on purpose: see the header. The root package pins these names, so
177
+ // a half-published set must stop the release rather than ship 404s.
178
+ throw new Error(`npm publish failed for ${dir}:\n${output.trim()}`);
179
+ }
180
+ }
181
+
182
+ export function main(argv = process.argv.slice(2)) {
183
+ const options = parseArgs(argv);
184
+ const rootPkg = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8"));
185
+ const version = options.version ?? rootPkg.version;
186
+ const distTag = options.distTag ?? (version.includes("-") ? "beta" : "latest");
187
+ const artifactsDir = resolve(process.cwd(), options.artifacts);
188
+
189
+ console.log(
190
+ `Publishing ${DAEMON_PLATFORMS.length} failproofaid platform packages at ${version} ` +
191
+ `(tag: ${distTag}${options.dryRun ? ", dry run" : ""}) from ${artifactsDir}`,
192
+ );
193
+
194
+ for (const platform of DAEMON_PLATFORMS) {
195
+ const dir = stagePlatformPackage(platform, version, rootPkg, artifactsDir, options.staging);
196
+ const name = daemonPackageName(platform.key);
197
+ console.log(`\n${name}@${version}`);
198
+ console.log(publishPackage(dir, distTag, options.dryRun).trim());
199
+ }
200
+
201
+ if (options.pinRoot) {
202
+ const pins = pinRootManifest(version);
203
+ console.log("\nPinned in the root manifest's optionalDependencies:");
204
+ console.log(JSON.stringify(pins, null, 2));
205
+ }
206
+ }
207
+
208
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
209
+ main();
210
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The platforms `failproofaid` is cross-compiled for, and the npm packages
3
+ * those binaries publish under.
4
+ *
5
+ * One list, three consumers: `build-daemon-packages.mjs` (publishes them),
6
+ * `publish-aliases.mjs` (pins them in every typo-squat stub), and the root
7
+ * package's injected `optionalDependencies`. They have to agree exactly — a
8
+ * name that appears in a dependency but never gets published is a 404 in
9
+ * every install, which is precisely how the first attempt at npm-shipping the
10
+ * daemon failed (see CHANGELOG 1.0.0-beta.3).
11
+ *
12
+ * `os` / `cpu` are npm's own filters, spelled in `process.platform` /
13
+ * `process.arch` values: with both set, npm and bun install only the package
14
+ * matching the machine and silently skip the other three.
15
+ */
16
+
17
+ export const DAEMON_PLATFORMS = [
18
+ { key: "linux-x64", os: "linux", cpu: "x64" },
19
+ { key: "linux-arm64", os: "linux", cpu: "arm64" },
20
+ { key: "darwin-x64", os: "darwin", cpu: "x64" },
21
+ { key: "darwin-arm64", os: "darwin", cpu: "arm64" },
22
+ ];
23
+
24
+ /** The npm package name carrying the binary for one platform key. */
25
+ export function daemonPackageName(key) {
26
+ return `@failproofai/failproofaid-${key}`;
27
+ }
28
+
29
+ /** The release asset the binary for one platform key arrives in. */
30
+ export function daemonAssetName(key) {
31
+ return `failproofaid-${key}.gz`;
32
+ }
33
+
34
+ /**
35
+ * The `optionalDependencies` block that pins all four platform packages to one
36
+ * version — what the root package and every alias stub carry when published.
37
+ */
38
+ export function daemonOptionalDependencies(version) {
39
+ return Object.fromEntries(
40
+ DAEMON_PLATFORMS.map((platform) => [daemonPackageName(platform.key), version]),
41
+ );
42
+ }
@@ -101,6 +101,12 @@ const STANDALONE_ROOT_PRUNE = [
101
101
  ".claude", ".failproofai", ".github", ".vscode", ".idea",
102
102
  // Failproofai CLI artifacts — the dashboard never loads these
103
103
  "bin", "dist", "scripts", "src",
104
+ // Release-pipeline scratch: the daemon binaries downloaded from the build
105
+ // matrix and the staged platform packages. Both are written into the
106
+ // workspace only by a local/manual run (the publish workflow keeps them in
107
+ // RUNNER_TEMP precisely so this cannot happen), and 16 MB of `.gz` shipped
108
+ // inside the CLI tarball once already because nothing pruned them.
109
+ "release-assets", ".daemon-packages",
104
110
  ];
105
111
  const STANDALONE_ROOT_PRUNE_FILES = [
106
112
  // Top-level markdown / licenses / docs
@@ -2,7 +2,8 @@
2
2
  import { execSync } from 'child_process';
3
3
  import { readFileSync, writeFileSync, mkdirSync, rmSync, cpSync } from 'fs';
4
4
  import { join, dirname } from 'path';
5
- import { fileURLToPath } from 'url';
5
+ import { fileURLToPath, pathToFileURL } from 'url';
6
+ import { daemonOptionalDependencies } from './daemon-platforms.mjs';
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
  const rootPkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -11,7 +12,7 @@ const DRY_RUN = process.argv.includes('--dry-run');
11
12
  const distTagIdx = process.argv.indexOf('--dist-tag');
12
13
  const DIST_TAG = distTagIdx !== -1 ? process.argv[distTagIdx + 1] : (VERSION.includes('-') ? 'beta' : 'latest');
13
14
 
14
- const ALIASES = [
15
+ export const ALIASES = [
15
16
  // Formatting variants — no "ai", or hyphen/underscore separators
16
17
  'failproof',
17
18
  'failproof-ai',
@@ -31,57 +32,77 @@ const ALIASES = [
31
32
  'faliproofai',
32
33
  ];
33
34
 
34
- const warnings = [];
35
-
36
- for (const name of ALIASES) {
37
- const tmpDir = join('/tmp', `npm-alias-${name}-${Date.now()}`);
38
- const binDir = join(tmpDir, 'bin');
39
- mkdirSync(binDir, { recursive: true });
40
-
41
- const pkg = {
35
+ /**
36
+ * The manifest one alias stub publishes.
37
+ *
38
+ * The four `@failproofai/failproofaid-<platform>` pins are the same ones the
39
+ * root package carries. They arrive transitively through the `failproofai`
40
+ * dependency anyway, but naming them here means someone who installs a typo'd
41
+ * name gets the daemon binary for exactly the reason the root package does,
42
+ * rather than by accident of resolution — and a test can assert it.
43
+ */
44
+ export function aliasManifest(name, version = VERSION, pkg = rootPkg) {
45
+ return {
42
46
  name,
43
- version: VERSION,
47
+ version,
44
48
  description: `Alias for failproofai — installs if you typed '${name}' instead of 'failproofai'`,
45
49
  bin: { [name]: './bin/proxy.js' },
46
50
  files: ['bin/'],
47
- dependencies: { failproofai: VERSION },
51
+ dependencies: { failproofai: version },
52
+ optionalDependencies: daemonOptionalDependencies(version),
48
53
  publishConfig: { access: 'public' },
49
- repository: rootPkg.repository,
50
- homepage: rootPkg.homepage,
51
- bugs: rootPkg.bugs,
52
- license: rootPkg.license,
54
+ repository: pkg.repository,
55
+ homepage: pkg.homepage,
56
+ bugs: pkg.bugs,
57
+ license: pkg.license,
53
58
  };
59
+ }
54
60
 
55
- writeFileSync(join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
56
- cpSync(join(__dirname, 'alias-proxy.js'), join(binDir, 'proxy.js'));
61
+ export function main() {
62
+ const warnings = [];
57
63
 
58
- if (DRY_RUN) {
59
- console.log(`[dry-run] Would publish ${name}@${VERSION} (tag: ${DIST_TAG})`);
60
- console.log(JSON.stringify(pkg, null, 2));
61
- console.log('---');
62
- rmSync(tmpDir, { recursive: true, force: true });
63
- continue;
64
- }
64
+ for (const name of ALIASES) {
65
+ const tmpDir = join('/tmp', `npm-alias-${name}-${Date.now()}`);
66
+ const binDir = join(tmpDir, 'bin');
67
+ mkdirSync(binDir, { recursive: true });
68
+
69
+ const pkg = aliasManifest(name);
65
70
 
66
- console.log(`Publishing ${name}@${VERSION}...`);
67
- try {
68
- execSync(`npm publish --tag ${DIST_TAG}`, { cwd: tmpDir, stdio: 'pipe' });
69
- console.log(`Done: ${name}`);
70
- } catch (err) {
71
- const output = (err.stdout?.toString() ?? '') + (err.stderr?.toString() ?? '');
72
- if (output.includes('too similar')) {
73
- warnings.push(`${name}: blocked by npm similarity check — request via npm support`);
74
- } else if (output.includes('cannot publish over')) {
75
- console.log(`[skip] ${name}: already published at ${VERSION}`);
76
- } else {
77
- warnings.push(`${name}: ${output.trim().split('\n').find(l => l.includes('npm error')) ?? 'unknown error'}`);
71
+ writeFileSync(join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
72
+ cpSync(join(__dirname, 'alias-proxy.js'), join(binDir, 'proxy.js'));
73
+
74
+ if (DRY_RUN) {
75
+ console.log(`[dry-run] Would publish ${name}@${VERSION} (tag: ${DIST_TAG})`);
76
+ console.log(JSON.stringify(pkg, null, 2));
77
+ console.log('---');
78
+ rmSync(tmpDir, { recursive: true, force: true });
79
+ continue;
80
+ }
81
+
82
+ console.log(`Publishing ${name}@${VERSION}...`);
83
+ try {
84
+ execSync(`npm publish --tag ${DIST_TAG}`, { cwd: tmpDir, stdio: 'pipe' });
85
+ console.log(`Done: ${name}`);
86
+ } catch (err) {
87
+ const output = (err.stdout?.toString() ?? '') + (err.stderr?.toString() ?? '');
88
+ if (output.includes('too similar')) {
89
+ warnings.push(`${name}: blocked by npm similarity check — request via npm support`);
90
+ } else if (output.includes('cannot publish over')) {
91
+ console.log(`[skip] ${name}: already published at ${VERSION}`);
92
+ } else {
93
+ warnings.push(`${name}: ${output.trim().split('\n').find(l => l.includes('npm error')) ?? 'unknown error'}`);
94
+ }
78
95
  }
96
+
97
+ rmSync(tmpDir, { recursive: true, force: true });
79
98
  }
80
99
 
81
- rmSync(tmpDir, { recursive: true, force: true });
100
+ if (warnings.length > 0) {
101
+ console.log('\n::warning::Some alias packages were not published:');
102
+ for (const w of warnings) console.log(` - ${w}`);
103
+ }
82
104
  }
83
105
 
84
- if (warnings.length > 0) {
85
- console.log('\n::warning::Some alias packages were not published:');
86
- for (const w of warnings) console.log(` - ${w}`);
106
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
107
+ main();
87
108
  }
@@ -170,6 +170,99 @@ const RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
170
170
 
171
171
  // blockCurlPipeSh
172
172
  const CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
173
+ /**
174
+ * `failproofai config --pause` in any of its reachable spellings — direct, via
175
+ * `npx`/`bunx`/`pnpm dlx`, by absolute path, or through the `configure`/`setup`
176
+ * aliases the CLI normalizes. Only `--pause` matches: `--resume` and `--status`
177
+ * restore or merely report enforcement, so an agent running either is harmless.
178
+ *
179
+ * Two details carry the whole policy, and an earlier version got both wrong:
180
+ *
181
+ * `failproofai[^\s]*` rather than `\bfailproofai\b`. A word boundary stops at
182
+ * the character after the name, so every ordinary way of pinning or pathing the
183
+ * binary walked straight through: `npx failproofai@latest`, `npx -y
184
+ * failproofai@0.0.16`, `bunx failproofai@latest`, and
185
+ * `node /usr/lib/node_modules/failproofai/bin/failproofai.mjs`. The suffix has
186
+ * to be absorbed by the same token.
187
+ *
188
+ * `\s+--pause` rather than `\s--pause`. With a single `\s`, two spaces before
189
+ * the flag — which no shell cares about and any agent may emit — did not match.
190
+ *
191
+ * This still raises the bar rather than closing the class: base64, a wrapper
192
+ * script, an alias or a shell variable all still reach the same state. Closing
193
+ * it properly means a pause cannot originate from a tool call at all.
194
+ *
195
+ * Matched against BOTH the raw command and its shell-unescaped form (see
196
+ * `stripShellQuoting`), because a shell reconstructs the binary name from
197
+ * fragments a regex over the raw string cannot see.
198
+ */
199
+ const SELF_PAUSE_RE =
200
+ /failproofai[^\s]*(?:\s+\S+)*?\s+(?:config|configure|setup)\b(?:\s+\S+)*?\s+--pause\b/;
201
+
202
+ /**
203
+ * Decode one ANSI-C `$'...'` body (the text between the quotes) to the bytes a
204
+ * shell would produce. Covers the escape forms that can reconstruct an ASCII
205
+ * identifier: `\xHH`, octal `\NNN`, `\uHHHH`, `\UHHHHHHHH`, and the named
206
+ * controls. An out-of-range or malformed sequence is left as-is rather than
207
+ * throwing — this runs on adversarial input and must never crash the hook.
208
+ */
209
+ function decodeAnsiC(body: string): string {
210
+ return body.replace(
211
+ /\\(x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|[0-7]{1,3}|.)/g,
212
+ (_, seq: string) => {
213
+ try {
214
+ if (seq[0] === "x") return String.fromCharCode(parseInt(seq.slice(1), 16));
215
+ if (seq[0] === "u" || seq[0] === "U") return String.fromCodePoint(parseInt(seq.slice(1), 16));
216
+ if (/^[0-7]+$/.test(seq)) return String.fromCharCode(parseInt(seq, 8));
217
+ } catch {
218
+ return seq;
219
+ }
220
+ const named: Record<string, string> = {
221
+ a: "\x07", b: "\b", e: "\x1b", f: "\f", n: "\n",
222
+ r: "\r", t: "\t", v: "\v", "\\": "\\", "'": "'", '"': '"', "?": "?",
223
+ };
224
+ return named[seq] ?? seq;
225
+ },
226
+ );
227
+ }
228
+
229
+ /**
230
+ * Collapse the quoting a POSIX shell resolves LEXICALLY — before it execs a
231
+ * command — so a literal matcher sees roughly what will actually run.
232
+ *
233
+ * Two adversarial red-team rounds defeated the pattern above by reassembling
234
+ * the binary name from fragments the regex could not see: first backslash and
235
+ * quote splitting (`fail\proofai`, `fail"proof"ai`, `f\a\i\l\p\r\o\o\f\a\i`),
236
+ * then ANSI-C quoting (`$'fail\x70roofai'`, octal `\160`, unicode `p`).
237
+ * Each executes the real pause. This now normalizes all three lexical forms:
238
+ * ANSI-C `$'...'` bodies are decoded first (the generic backslash pass below
239
+ * would otherwise turn `\x70` into `x70`), then surrounding single/double
240
+ * quotes and lone backslash-escapes are removed.
241
+ *
242
+ * The boundary is principled, not arbitrary: everything handled here is pure
243
+ * shell LEXING. What remains — a variable (`f=failproofai; $f …`), command
244
+ * substitution (`$(printf failproofai)`), `eval`, an alias, a base64 pipe —
245
+ * requires the shell to EXECUTE something to reconstruct the name, which no
246
+ * PreToolUse hook inspecting a command string can follow. That class is the one
247
+ * the header comment above calls out, and the only real closure is to make the
248
+ * pause ACTION itself refuse to originate from a tool call — deferred with the
249
+ * rest of the daemon-side redesign.
250
+ */
251
+ function stripShellQuoting(command: string): string {
252
+ // Line continuation FIRST. A shell deletes a backslash-newline pair entirely
253
+ // and rejoins the fragments, so `fail\<newline>proofai` runs the real binary.
254
+ // The generic `\(.)` strip below cannot catch it — JS `.` excludes newline —
255
+ // so it has to be removed before that pass, or the fragments never join.
256
+ // (A third red-team round rode exactly this in.) `\r?\n` also covers a
257
+ // CRLF-joined line.
258
+ const joined = command.replace(/\\\r?\n/g, "");
259
+ // `$'...'` next, honoring `\'` inside the body, so its own escapes decode
260
+ // before the generic backslash-strip can mangle them.
261
+ const ansiDecoded = joined.replace(/\$'((?:[^'\\]|\\.)*)'/g, (_, body: string) =>
262
+ decodeAnsiC(body),
263
+ );
264
+ return ansiDecoded.replace(/\\(.)/g, "$1").replace(/['"]/g, "");
265
+ }
173
266
  const PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
174
267
 
175
268
  // blockForcePush
@@ -579,6 +672,42 @@ function blockSudo(ctx: PolicyContext): PolicyResult {
579
672
  return allow();
580
673
  }
581
674
 
675
+ /**
676
+ * A pause the agent can issue is not a guardrail.
677
+ *
678
+ * `failproofai config --pause` is a human affordance: you watched the agent get
679
+ * blocked, you judged the block wrong, you suspended enforcement for a bit. An
680
+ * agent that can run it turns every other policy advisory — it need only shell
681
+ * out once to switch them all off, and the pause even survives into later turns.
682
+ *
683
+ * This raises the bar rather than closing the class: an agent can still reach
684
+ * the same state obfuscated (base64, a wrapper script, an alias). Closing it
685
+ * properly means the pause cannot originate from a tool call at all — a
686
+ * dashboard action, or a daemon that accepts it only from a TTY. Until then,
687
+ * the honest description of this policy is "stops the obvious attempt".
688
+ *
689
+ * It is deliberately NOT redundant with `block-failproofai-commands`, which
690
+ * blocks the CLI far more broadly. That one anchors on a command boundary
691
+ * (`FAILPROOFAI_CLI_RE`), so `npx -y failproofai config --pause` does not match
692
+ * it — and being broad, it is a policy people plausibly switch off so agents
693
+ * can run `failproofai audit`. Neither gap should leave pausing reachable, so
694
+ * this stays narrow, matches the runner forms, and survives that one being off.
695
+ */
696
+ function blockSelfPause(ctx: PolicyContext): PolicyResult {
697
+ if (ctx.toolName !== "Bash") return allow();
698
+ const cmd = getCommand(ctx);
699
+ // The raw command AND its shell-unescaped form: a shell strips quotes and
700
+ // backslashes before running the binary, so `fail\proofai config --pause`
701
+ // reaches the pause CLI even though the literal name is broken.
702
+ if (SELF_PAUSE_RE.test(cmd) || SELF_PAUSE_RE.test(stripShellQuoting(cmd))) {
703
+ return deny(
704
+ "Pausing failproofai enforcement is a human action, not an agent one. " +
705
+ "If a policy is blocking legitimate work, say so and let the operator decide.",
706
+ );
707
+ }
708
+ return allow();
709
+ }
710
+
582
711
  function blockCurlPipeSh(ctx: PolicyContext): PolicyResult {
583
712
  if (ctx.toolName !== "Bash") return allow();
584
713
  const cmd = getCommand(ctx);
@@ -1752,6 +1881,16 @@ export const BUILTIN_POLICIES: BuiltinPolicyDefinition[] = [
1752
1881
  },
1753
1882
  } satisfies PolicyParamsSchema,
1754
1883
  },
1884
+ {
1885
+ name: "block-self-pause",
1886
+ displayTitle: "Tried to pause failproofai enforcement",
1887
+ impact: "An agent that can pause enforcement can switch off every other policy.",
1888
+ description: "Block agents from pausing failproofai enforcement",
1889
+ fn: blockSelfPause,
1890
+ match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
1891
+ defaultEnabled: true,
1892
+ category: "Dangerous Commands",
1893
+ },
1755
1894
  {
1756
1895
  name: "block-sudo",
1757
1896
  displayTitle: "Tried to run a command with sudo",