failproofai 0.0.14-beta.0 → 0.0.14-beta.2

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 (219) hide show
  1. package/.next/standalone/.codex/hooks.json +6 -6
  2. package/.next/standalone/.cursor/hooks.json +6 -6
  3. package/.next/standalone/.factory/hooks.json +9 -9
  4. package/.next/standalone/.next/BUILD_ID +1 -1
  5. package/.next/standalone/.next/build-manifest.json +5 -5
  6. package/.next/standalone/.next/prerender-manifest.json +3 -3
  7. package/.next/standalone/.next/required-server-files.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  9. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  15. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  16. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  20. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  24. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  25. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  26. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  27. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  29. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  30. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  31. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  40. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  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 +2 -2
  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 +2 -2
  55. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  56. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  68. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +3 -3
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
  95. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cz9as._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07qfsfa._.js → [root-of-the-server]__09v9cwb._.js} +2 -2
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h12me5._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0twi4fj._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11rtg6s._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +2 -2
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dky4g0._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
  122. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__212nf49._.js +1 -1
  123. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
  124. package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/app_087bt9w._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/app_1fvisnp._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/app_209u41o._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
  129. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0g5r64e.js → node_modules_html-to-image_es_index_06xsmte.js} +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0-uvagv.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03c7gi5.js +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_09p-8om.js +1 -1
  136. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0v-kfiu.js +1 -1
  137. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0xrgzyz.js +1 -1
  138. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1806lsc.js +1 -1
  139. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1j6dd-e.js +1 -1
  140. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1sa65r-.js +1 -1
  141. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  144. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  145. package/.next/standalone/.next/server/pages/404.html +1 -1
  146. package/.next/standalone/.next/server/pages/500.html +1 -1
  147. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  148. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  149. package/.next/standalone/.next/static/chunks/{13f1kmjea-0md.js → 02jcotghcxrbu.js} +1 -1
  150. package/.next/standalone/.next/static/chunks/{1374x9bv_s_vg.js → 0h-6ymo9v86b0.js} +2 -2
  151. package/.next/standalone/.next/static/chunks/0pndjhcxfu-0t.js +1 -0
  152. package/.next/standalone/.next/static/chunks/1-33f82_l32or.js +1 -0
  153. package/.next/standalone/.next/static/chunks/{003uz088wbmz7.js → 1bg_g3g359c_d.js} +1 -1
  154. package/.next/standalone/.next/static/chunks/{454dyizqik6t9.js → 1gg-8e9q_m__9.js} +2 -2
  155. package/.next/standalone/.next/static/chunks/{2ufuwhv03yvtb.js → 29zkom4s9de2d.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{17sdztgbcc0fi.js → 2llkdafnaejw_.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{0iinhn4417nk1.js → 3-ogr79xqbw0h.js} +1 -1
  158. package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +2 -0
  159. package/.next/standalone/.next/static/chunks/{43weu8f59tlix.js → 388hgfqye67xa.js} +1 -1
  160. package/.next/standalone/.next/static/chunks/396764jgd7-9j.js +1 -0
  161. package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +1 -0
  162. package/.next/standalone/.next/static/chunks/{turbopack-0vavqo2d-tlt3.js → turbopack-10hj8t5pf33tu.js} +1 -1
  163. package/.next/standalone/.opencode/plugins/failproofai.mjs +30 -11
  164. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +11 -2
  165. package/.next/standalone/app/components/copy-button.tsx +22 -6
  166. package/.next/standalone/app/components/global-error-listeners.tsx +27 -0
  167. package/.next/standalone/components/reach-developers.tsx +1 -1
  168. package/.next/standalone/docker-hook-sync/Dockerfile +8 -4
  169. package/.next/standalone/integration-suite/Dockerfile +47 -0
  170. package/.next/standalone/integration-suite/README.md +87 -0
  171. package/.next/standalone/integration-suite/canary-policies.mjs +45 -0
  172. package/.next/standalone/integration-suite/capture-tokens.sh +48 -0
  173. package/.next/standalone/integration-suite/ci-entrypoint.sh +164 -0
  174. package/.next/standalone/integration-suite/inject-tokens.sh +44 -0
  175. package/.next/standalone/integration-suite/install-clis.sh +91 -0
  176. package/.next/standalone/integration-suite/probe-cli.sh +208 -0
  177. package/.next/standalone/integration-suite/report.js +74 -0
  178. package/.next/standalone/integration-suite/run.sh +107 -0
  179. package/.next/standalone/lib/error-origin.ts +67 -0
  180. package/.next/standalone/lib/format-duration.ts +23 -9
  181. package/.next/standalone/lib/install-check.ts +231 -0
  182. package/.next/standalone/package.json +1 -3
  183. package/.next/standalone/server.js +1 -1
  184. package/README.md +1 -17
  185. package/bin/failproofai.mjs +11 -1
  186. package/dist/cli.mjs +818 -320
  187. package/lib/error-origin.ts +67 -0
  188. package/lib/format-duration.ts +23 -9
  189. package/lib/install-check.ts +231 -0
  190. package/package.json +1 -3
  191. package/scripts/dev-hook.mjs +292 -0
  192. package/scripts/install-diagnosis.mjs +5 -8
  193. package/scripts/install-telemetry.mjs +12 -4
  194. package/scripts/translate-docs/cli.ts +53 -1
  195. package/scripts/translate-docs/mdx-translator.ts +158 -18
  196. package/scripts/translate-docs/readme-translator.ts +36 -18
  197. package/scripts/translate-docs/translator.ts +180 -9
  198. package/scripts/translate-docs/types.ts +6 -0
  199. package/scripts/translate-docs/validate-translation.ts +132 -0
  200. package/scripts/validate-mdx.ts +92 -15
  201. package/src/audit/cli.ts +53 -15
  202. package/src/auth/cli.ts +14 -5
  203. package/src/hooks/builtin-policies.ts +168 -50
  204. package/src/hooks/configure-wizard.ts +238 -22
  205. package/src/hooks/custom-hooks-loader.ts +70 -4
  206. package/src/hooks/handler.ts +4 -1
  207. package/src/hooks/policy-types.ts +8 -0
  208. package/src/hooks/tui.ts +96 -29
  209. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ge8e7w._.js +0 -3
  210. package/.next/standalone/.next/static/chunks/0405htbxay9vd.js +0 -1
  211. package/.next/standalone/.next/static/chunks/2m_tbl39fcedn.js +0 -1
  212. package/.next/standalone/.next/static/chunks/2o3ql0n5owqsw.js +0 -1
  213. package/.next/standalone/.next/static/chunks/35-90m9py64d4.js +0 -1
  214. package/.next/standalone/.next/static/chunks/3r--yyjkp9_7b.css +0 -2
  215. package/scripts/postinstall.mjs +0 -242
  216. package/scripts/preuninstall.mjs +0 -131
  217. /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_buildManifest.js +0 -0
  218. /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_clientMiddlewareManifest.js +0 -0
  219. /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_ssgManifest.js +0 -0
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env bash
2
+ # ─────────────────────────────────────────────────────────────────────────────
3
+ # failproofai integration suite — CI entrypoint (runs on the GitHub Actions runner).
4
+ #
5
+ # This is the suite's single front door for CI. The workflow
6
+ # (.github/workflows/integration-suite.yml) is a thin trigger: it checks out,
7
+ # installs bun, maps secrets into the environment, and calls this script. Every
8
+ # step that actually *does* something lives here, so the harness can be read —
9
+ # and run — without opening the YAML.
10
+ #
11
+ # Responsibilities, in order:
12
+ # 1. build failproofai under test (dist/index.js + dist/cli.mjs)
13
+ # 2. decode the base64 OAuth-token secrets into $TOKENS_DIR
14
+ # 3. build the sandbox image and create the per-run HOME volume
15
+ # 4. install all 12 CLIs @latest into that volume, then inject the tokens
16
+ # 5. assemble the gateway env-file
17
+ # 6. hand off to run.sh, which does the version-gating, probing and reporting
18
+ #
19
+ # Cleanup (env-file + docker volume) runs via trap, so it happens on failure and
20
+ # on early exit too — not just on the happy path.
21
+ #
22
+ # Inputs are all environment variables, so this is runnable outside GH Actions:
23
+ #
24
+ # GITHUB_WORKSPACE repo checkout (default: this script's parent dir)
25
+ # CANARY_LLM_API_KEY gateway credentials -> written into the env-file
26
+ # CANARY_LLM_BASE_URL (default https://models.aikin.club)
27
+ # CANARY_LLM_MODEL (default deepseek-v4-pro)
28
+ # CANARY_CLAUDE_MODEL (default claude-haiku-4-5)
29
+ # CANARY_PI_MODEL (default claude-haiku-4-5)
30
+ # COPILOT_GITHUB_TOKEN copilot PAT
31
+ # CURSOR_TOKEN_TGZ_B64 } base64 gzip-tars of each OAuth credential tree,
32
+ # DEVIN_TOKEN_TGZ_B64 } rooted at $HOME (see capture-tokens.sh)
33
+ # ANTIGRAVITY_TOKEN_TGZ_B64
34
+ # CANARY_STATE version-gate state file (restored/saved by the workflow cache)
35
+ # CANARY_SLACK_WEBHOOK optional; report is POSTed here when set
36
+ # CANARY_FP_SHA failproofai HEAD, the second version-gate dimension
37
+ # CANARY_VERSION_GATED "all" (default) | comma-sep list | "none" to force-probe
38
+ # CANARY_CLIS space-separated CLI subset (empty = all 12)
39
+ # CANARY_SKIP_BUILD set to 1 to reuse an existing dist/ (local iteration)
40
+ # ─────────────────────────────────────────────────────────────────────────────
41
+ set -u
42
+
43
+ HERE="$(cd "$(dirname "$0")" && pwd)"
44
+ REPO="${GITHUB_WORKSPACE:-$(dirname "$HERE")}"
45
+
46
+ VOL="${CANARY_VOL:-integration-suite}"
47
+ IMAGE="${CANARY_IMAGE:-failproofai-integration-suite:base}"
48
+ ENVFILE="${CANARY_ENVFILE:-$REPO/canary.env}"
49
+ TOKENS_DIR="${CANARY_TOKENS_DIR:-$REPO/tokens}"
50
+ STATE="${CANARY_STATE:-$REPO/integration-suite-state.json}"
51
+
52
+ step() { echo "── $* ──" >&2; }
53
+
54
+ cleanup() {
55
+ local rc=$?
56
+ step "cleanup"
57
+ rm -f "$ENVFILE"
58
+ rm -rf "$TOKENS_DIR"
59
+ docker volume rm "$VOL" -f >/dev/null 2>&1 || true
60
+ exit "$rc"
61
+ }
62
+ trap cleanup EXIT
63
+
64
+ # ── 0. adopt legacy version-gate state (rename transition) ──────────────────
65
+ # This harness was renamed from `cli-integration`, which took the Actions cache
66
+ # key with it. The workflow restores the legacy cache alongside the new one; if
67
+ # only the legacy file is present, adopt it so the version-gate keeps its
68
+ # baseline instead of re-probing all 12 CLIs and reporting every one as changed.
69
+ # Safe to delete (with the workflow's legacy restore step) once a run has saved
70
+ # state under the new key.
71
+ LEGACY_STATE="${CANARY_LEGACY_STATE:-$REPO/cli-integration-state.json}"
72
+ if [ ! -s "$STATE" ] && [ -s "$LEGACY_STATE" ]; then
73
+ step "adopting legacy version-gate state from $(basename "$LEGACY_STATE")"
74
+ cp "$LEGACY_STATE" "$STATE" || echo " WARN: copy failed — gate starts empty" >&2
75
+ fi
76
+
77
+ # ── 1. build failproofai under test ─────────────────────────────────────────
78
+ if [ "${CANARY_SKIP_BUILD:-0}" = 1 ]; then
79
+ step "skipping build (CANARY_SKIP_BUILD=1)"
80
+ else
81
+ step "building failproofai under test (dist/index.js + dist/cli.mjs — no dashboard)"
82
+ (
83
+ cd "$REPO" || exit 1
84
+ bun install --frozen-lockfile || exit 1
85
+ bun build --target=node --format=cjs --outfile=dist/index.js src/index.ts || exit 1
86
+ bun run build:cli || exit 1
87
+ ) || { echo "✗ build failed" >&2; exit 1; }
88
+ fi
89
+ if [ ! -s "$REPO/dist/index.js" ] || [ ! -s "$REPO/dist/cli.mjs" ]; then
90
+ echo "✗ dist/index.js and dist/cli.mjs must both be non-empty" >&2
91
+ exit 1
92
+ fi
93
+
94
+ # ── 2. decode OAuth token secrets ───────────────────────────────────────────
95
+ # Each is a base64 gzip-tar rooted at $HOME. A missing secret is NOT fatal: that
96
+ # CLI simply reports ERROR (can't auth) rather than taking the whole run down.
97
+ step "decoding OAuth token secrets"
98
+ mkdir -p "$TOKENS_DIR"
99
+ chmod 700 "$TOKENS_DIR"
100
+ decode_token() {
101
+ local name="$1" b64="$2"
102
+ if [ -z "$b64" ]; then
103
+ echo " ✗ $name: no secret set (that CLI will report ERROR)"
104
+ return 0
105
+ fi
106
+ if printf '%s' "$b64" | base64 -d > "$TOKENS_DIR/$name.tgz" 2>/dev/null; then
107
+ echo " ✓ $name: $(wc -c < "$TOKENS_DIR/$name.tgz") bytes"
108
+ else
109
+ echo " ✗ $name: base64 decode FAILED (that CLI will report ERROR)"
110
+ rm -f "$TOKENS_DIR/$name.tgz"
111
+ fi
112
+ }
113
+ decode_token cursor "${CURSOR_TOKEN_TGZ_B64:-}"
114
+ decode_token devin "${DEVIN_TOKEN_TGZ_B64:-}"
115
+ decode_token antigravity "${ANTIGRAVITY_TOKEN_TGZ_B64:-}"
116
+
117
+ # ── 3. sandbox image + per-run volume ───────────────────────────────────────
118
+ step "building sandbox image ($IMAGE)"
119
+ docker build -t "$IMAGE" "$HERE/" || { echo "✗ sandbox image build failed" >&2; exit 1; }
120
+
121
+ step "creating per-run HOME volume ($VOL)"
122
+ docker volume rm "$VOL" -f >/dev/null 2>&1 || true
123
+ docker volume create "$VOL" >/dev/null || { echo "✗ volume create failed" >&2; exit 1; }
124
+
125
+ # ── 4. install CLIs @latest, then inject tokens ─────────────────────────────
126
+ step "installing 12 CLIs @latest into the fresh volume"
127
+ docker run --rm -v "$VOL:/home/canary" -v "$HERE:/opt/canary:ro" \
128
+ "$IMAGE" bash /opt/canary/install-clis.sh \
129
+ || { echo "✗ CLI install failed" >&2; exit 1; }
130
+
131
+ step "injecting OAuth credential files"
132
+ docker run --rm -v "$VOL:/home/canary" \
133
+ -v "$HERE:/opt/canary:ro" -v "$TOKENS_DIR:/opt/tokens:ro" \
134
+ "$IMAGE" bash /opt/canary/inject-tokens.sh \
135
+ || { echo "✗ token injection failed" >&2; exit 1; }
136
+
137
+ # ── 5. gateway env-file ─────────────────────────────────────────────────────
138
+ # umask 077 so the file with the gateway key is never group/world readable.
139
+ step "assembling gateway env-file"
140
+ (
141
+ umask 077
142
+ {
143
+ echo "CANARY_LLM_API_KEY=${CANARY_LLM_API_KEY:-}"
144
+ echo "CANARY_LLM_BASE_URL=${CANARY_LLM_BASE_URL:-https://models.aikin.club}"
145
+ echo "CANARY_LLM_MODEL=${CANARY_LLM_MODEL:-deepseek-v4-pro}"
146
+ echo "CANARY_CLAUDE_MODEL=${CANARY_CLAUDE_MODEL:-claude-haiku-4-5}"
147
+ echo "CANARY_PI_MODEL=${CANARY_PI_MODEL:-claude-haiku-4-5}"
148
+ echo "COPILOT_GITHUB_TOKEN=${COPILOT_GITHUB_TOKEN:-}"
149
+ } > "$ENVFILE"
150
+ )
151
+
152
+ # ── 6. probe + report ───────────────────────────────────────────────────────
153
+ # run.sh word-splits CANARY_CLIS into args; unquoted on purpose. It owns the
154
+ # exit code (non-zero iff a hard FAIL verdict is present), which the trap
155
+ # preserves.
156
+ step "running integration probes + report"
157
+ # shellcheck disable=SC2086
158
+ CANARY_REPO="$REPO" \
159
+ CANARY_SANDBOX="$HERE" \
160
+ CANARY_VOL="$VOL" \
161
+ CANARY_IMAGE="$IMAGE" \
162
+ CANARY_STATE="$STATE" \
163
+ CANARY_ENVFILE="$ENVFILE" \
164
+ bash "$HERE/run.sh" ${CANARY_CLIS:-}
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ # ─────────────────────────────────────────────────────────────────────────────
3
+ # Token injection — runs INSIDE the sandbox container, against the fresh per-run
4
+ # HOME volume, BEFORE the probes.
5
+ #
6
+ # On the office box the OAuth logins (cursor / devin / antigravity) live in a
7
+ # PERSISTENT volume. GitHub-Actions runners are ephemeral, so instead each of
8
+ # those credential trees is captured once (see capture-tokens.sh) into a GitHub
9
+ # secret as a base64 gzip-tar rooted at $HOME, decoded by the workflow into
10
+ # /opt/tokens/<name>.tgz, and extracted here into the fresh volume's $HOME.
11
+ #
12
+ # Extraction is rooted at $HOME and the tarballs were created with $HOME-relative
13
+ # paths (e.g. `.config/cursor`, `.local/share/devin/credentials.toml`), so each
14
+ # file lands exactly where its CLI expects it. Env-var-auth CLIs (gateway key,
15
+ # copilot PAT, factory BYOK) need NO file here — they authenticate from env.
16
+ # ─────────────────────────────────────────────────────────────────────────────
17
+ set -u
18
+ TOK="${1:-/opt/tokens}"
19
+
20
+ if [ ! -d "$TOK" ]; then
21
+ echo "inject-tokens: no token dir at $TOK — nothing to inject (env-var CLIs only)"
22
+ exit 0
23
+ fi
24
+
25
+ shopt -s nullglob
26
+ found=0
27
+ for t in "$TOK"/*.tgz "$TOK"/*.tar.gz; do
28
+ found=1
29
+ echo "── injecting $(basename "$t") → \$HOME ──"
30
+ if tar -C "$HOME" -xzf "$t"; then
31
+ tar -tzf "$t" 2>/dev/null | sed 's/^/ /' | head -8
32
+ else
33
+ echo " WARN: extract failed for $t (skipping — that CLI will report ERROR)"
34
+ fi
35
+ done
36
+ [ "$found" = 0 ] && echo "inject-tokens: token dir present but empty — env-var CLIs only"
37
+
38
+ echo "── credential files now present in volume ──"
39
+ for f in "$HOME/.config/cursor/auth.json" \
40
+ "$HOME/.local/share/devin/credentials.toml" \
41
+ "$HOME/.gemini/antigravity-cli"; do
42
+ [ -e "$f" ] && echo " ✓ $f" || echo " ✗ $f (absent)"
43
+ done
44
+ exit 0
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # ─────────────────────────────────────────────────────────────────────────────
3
+ # Tier-0 install probe — runs INSIDE the canary sandbox container.
4
+ #
5
+ # Installs/upgrades all 12 agent CLIs to @latest into the persistent HOME volume,
6
+ # then records which resolve to a runnable binary + their version. This is both
7
+ # the daily "fresh upgrade" step AND the Tier-0 canary signal (packaging/binary
8
+ # breaks show up here as a FAIL).
9
+ #
10
+ # Install methods verified 2026-07-16 (see failproofai-cli-canary-design.md).
11
+ # Vendor installers scatter binaries across several dirs, so we search an
12
+ # expanded PATH and fall back to a bounded `find` under $HOME.
13
+ #
14
+ # Output: a JSON array at $CANARY_RESULTS (default ~/canary-tier0.json) + a
15
+ # human table on stdout. Never `set -e` — one CLI failing must not abort the rest.
16
+ # ─────────────────────────────────────────────────────────────────────────────
17
+ set -u
18
+
19
+ RESULTS="${CANARY_RESULTS:-$HOME/canary-tier0.json}"
20
+ LOGDIR="${CANARY_LOGDIR:-$HOME/canary-logs}"
21
+ mkdir -p "$LOGDIR"
22
+
23
+ # Cover every dir the vendor installers are known to drop binaries into.
24
+ export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$HOME/.factory/bin:$HOME/.hermes/bin:$HOME/.cursor/bin:$HOME/.codex/bin:$HOME/bin:$PATH"
25
+
26
+ ROWS=()
27
+
28
+ probe() {
29
+ local id="$1" bin="$2" vflag="$3" install="$4"
30
+ echo "════════════════════ $id ════════════════════"
31
+ local t0 t1 rc=1; t0=$SECONDS
32
+ # Retry transient install failures. Flaky vendor CDNs / HTTP-2 stream resets are
33
+ # common on ephemeral CI runners (e.g. factory's `curl … | sh` dropping the
34
+ # download mid-stream with `curl: (18)`). Break as soon as the binary resolves;
35
+ # otherwise give it up to 3 attempts so one bad download isn't a false FAIL.
36
+ for attempt in 1 2 3; do
37
+ bash -c "$install" >"$LOGDIR/$id.install.log" 2>&1
38
+ rc=$?
39
+ hash -r 2>/dev/null
40
+ command -v "$bin" >/dev/null 2>&1 && break
41
+ find "$HOME" -maxdepth 5 -name "$bin" -type f -perm -u+x 2>/dev/null | grep -q . && break
42
+ [ "$attempt" -lt 3 ] && { echo " ↻ $id install attempt $attempt failed (rc=$rc) — retrying in 5s"; sleep 5; }
43
+ done
44
+ t1=$SECONDS
45
+ hash -r 2>/dev/null
46
+
47
+ # Resolve the binary: PATH first, then a bounded find under $HOME.
48
+ local resolved; resolved="$(command -v "$bin" 2>/dev/null || true)"
49
+ if [ -z "$resolved" ]; then
50
+ resolved="$(find "$HOME" -maxdepth 5 -name "$bin" -type f -perm -u+x 2>/dev/null | head -1)"
51
+ fi
52
+
53
+ local ver="" status="FAIL"
54
+ if [ -n "$resolved" ]; then
55
+ status="OK"
56
+ ver="$("$resolved" $vflag 2>&1 | head -1 | tr -cd '[:alnum:] ._+:/-')"
57
+ fi
58
+
59
+ printf ' rc=%s time=%ss bin=%s ver=%s => %s\n' \
60
+ "$rc" "$((t1 - t0))" "${resolved:-<none>}" "${ver:-<none>}" "$status"
61
+ [ "$status" = FAIL ] && echo " ↳ last install log lines:" && tail -3 "$LOGDIR/$id.install.log" | sed 's/^/ /'
62
+
63
+ ROWS+=("$(printf '{"cli":"%s","status":"%s","binary":"%s","version":"%s","install_rc":%s,"secs":%s}' \
64
+ "$id" "$status" "${resolved:-}" "${ver:-}" "$rc" "$((t1 - t0))")")
65
+ }
66
+
67
+ # id binary --ver flag install command (@latest)
68
+ probe claude claude "--version" 'curl -fsSL https://claude.ai/install.sh | bash'
69
+ probe codex codex "--version" 'npm install -g @openai/codex@latest'
70
+ probe copilot copilot "--version" 'npm install -g @github/copilot@latest'
71
+ probe cursor cursor-agent "--version" 'curl https://cursor.com/install -fsS | bash'
72
+ probe opencode opencode "--version" 'npm install -g opencode-ai@latest'
73
+ probe pi pi "--version" 'npm install -g @mariozechner/pi-coding-agent@latest'
74
+ probe hermes hermes "--version" 'curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash'
75
+ probe openclaw openclaw "--version" 'npm install -g openclaw@latest'
76
+ probe factory droid "--version" 'curl -fsSL https://app.factory.ai/cli | sh'
77
+ probe devin devin "--version" 'curl -fsSL https://cli.devin.ai/install.sh | bash'
78
+ probe antigravity agy "--version" 'curl -fsSL https://antigravity.google/cli/install.sh | bash'
79
+ probe goose goose "--version" 'curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash'
80
+
81
+ # Emit JSON array.
82
+ { printf '['; IFS=,; printf '%s' "${ROWS[*]}"; printf ']\n'; } > "$RESULTS"
83
+
84
+ echo
85
+ echo "════════════════════ TIER-0 SUMMARY ════════════════════"
86
+ ok=0; fail=0
87
+ for r in "${ROWS[@]}"; do
88
+ case "$r" in *'"status":"OK"'*) ok=$((ok+1));; *) fail=$((fail+1));; esac
89
+ done
90
+ printf 'installed OK: %s / 12 failed: %s\n' "$ok" "$fail"
91
+ echo "results JSON: $RESULTS"
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env bash
2
+ # ─────────────────────────────────────────────────────────────────────────────
3
+ # Live Tier-2 enforcement probe for ONE CLI. Runs INSIDE the sandbox container.
4
+ # Usage: probe-cli.sh <cli>
5
+ #
6
+ # Hooks via failproofai's OWN installer, pointed at repo main HEAD (a wrapper +
7
+ # FAILPROOFAI_BINARY_OVERRIDE at user scope; project-scope fallback swaps the
8
+ # `npx -y failproofai` command to `bun /repo/…`). Only wire()/drive() are per-CLI.
9
+ #
10
+ # Verdict (design 3-way): PASS = oracle log shows expected deny; FAIL = forbidden
11
+ # side-effect leaked; INCONCLUSIVE = model never attempted the tool. LOG_LEVEL=info
12
+ # is mandatory (oracle lines are INFO).
13
+ # ─────────────────────────────────────────────────────────────────────────────
14
+ set -u
15
+ CLI="${1:?usage: probe-cli.sh <cli>}"
16
+ : "${CANARY_LLM_API_KEY:?gateway key missing}"
17
+ # Gateway default model. deepseek-v4-pro is cheapest AND works on both the OpenAI
18
+ # chat-completions and Responses paths. EXCEPTION: the claude CLI speaks Anthropic
19
+ # tool-use, which deepseek-via-/v1/messages doesn't emit correctly (all probes go
20
+ # INCONCLUSIVE), so claude pins to the cheapest Anthropic model via CANARY_CLAUDE_MODEL.
21
+ : "${CANARY_LLM_MODEL:=deepseek-v4-pro}"
22
+ : "${CANARY_CLAUDE_MODEL:=claude-haiku-4-5}"
23
+ # pi sends an `include` (encrypted reasoning) param deepseek rejects (400) → pin to Anthropic.
24
+ : "${CANARY_PI_MODEL:=claude-haiku-4-5}"
25
+ # Gateway base URL — overridable via env (CI supplies it as a secret). Strip any
26
+ # trailing slash so the `$GW/v1` joins below never produce `//v1`.
27
+ GW="${CANARY_LLM_BASE_URL:-https://models.aikin.club}"; GW="${GW%/}"
28
+
29
+ # The custom-policy loader writes an ESM shim NEXT TO the dist index, but /repo
30
+ # is mounted read-only (EROFS). So point FAILPROOFAI_DIST_PATH at a WRITABLE copy
31
+ # of /repo/dist (bin/failproofai.mjs only sets it when unset, so this wins). Kept
32
+ # fresh each run so it tracks main HEAD's built dist.
33
+ FP_DIST="$HOME/fp-dist"
34
+ # Recreate from scratch each run: the volume can persist across runs, so overlaying
35
+ # with `cp` would leave files a newer HEAD removed behind (mixed build). Fail loudly
36
+ # rather than probe against a stale/partial dist.
37
+ rm -rf "$FP_DIST"; mkdir -p "$FP_DIST"
38
+ cp -r /repo/dist/. "$FP_DIST/" || { echo "failed to prepare failproofai dist from /repo/dist" >&2; exit 1; }
39
+ export FAILPROOFAI_DIST_PATH="$FP_DIST" FAILPROOFAI_TELEMETRY_DISABLED=1 FAILPROOFAI_LOG_LEVEL=info
40
+ export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$HOME/.factory/bin:$PATH"
41
+
42
+ mkdir -p "$HOME/bin"
43
+ printf '#!/bin/sh\nexec bun /repo/bin/failproofai.mjs "$@"\n' > "$HOME/bin/failproofai"
44
+ chmod +x "$HOME/bin/failproofai"
45
+ export FAILPROOFAI_BINARY_OVERRIDE="$HOME/bin/failproofai"
46
+
47
+ BASE="$HOME/probe-$CLI"
48
+ # DEFINITE probes: BENIGN actions (echo/touch a token, read a plain file) the
49
+ # model never refuses → a tool call is guaranteed, so no INCONCLUSIVE from
50
+ # self-censorship. A custom canary policy denies exactly those benign markers,
51
+ # so a deny proves the enforcement pipeline works on the CLI's real payload.
52
+ POLICIES=(block-read-outside-cwd) # one builtin so install is non-interactive
53
+ # The loader writes a temp file next to the custom policy, so it must live in a
54
+ # WRITABLE dir (/opt/canary is read-only). Copy it into HOME.
55
+ CUSTOM_POLICIES="$HOME/canary-policies.mjs"
56
+ cp /opt/canary/canary-policies.mjs "$CUSTOM_POLICIES"
57
+
58
+ install_hooks() {
59
+ # Prefer user scope (writes the override → main HEAD). Fall back to project +
60
+ # command-swap for CLIs without a user scope. `-c` loads the custom canary policies.
61
+ if bun /repo/bin/failproofai.mjs policies --install "${POLICIES[@]}" --cli "$CLI" --scope user -c "$CUSTOM_POLICIES" >/dev/null 2>&1; then
62
+ return 0
63
+ fi
64
+ ( cd "$BASE" && bun /repo/bin/failproofai.mjs policies --install "${POLICIES[@]}" --cli "$CLI" --scope project -c "$CUSTOM_POLICIES" >/dev/null 2>&1 )
65
+ grep -rl "npx -y failproofai" "$BASE" 2>/dev/null | while read -r f; do
66
+ sed -i 's#npx -y failproofai#bun /repo/bin/failproofai.mjs#g' "$f"
67
+ done
68
+ }
69
+
70
+ wire() { # point the CLI at the gateway via env/config only (no interactive wizard)
71
+ case "$CLI" in
72
+ claude) export ANTHROPIC_BASE_URL="$GW" ANTHROPIC_AUTH_TOKEN="$CANARY_LLM_API_KEY" ;;
73
+ opencode) printf '{"provider":{"gw":{"npm":"@ai-sdk/openai-compatible","options":{"baseURL":"%s/v1","apiKey":"%s"},"models":{"%s":{}}}}}' \
74
+ "$GW" "$CANARY_LLM_API_KEY" "$CANARY_LLM_MODEL" > "$BASE/opencode.json" ;;
75
+ goose) export GOOSE_PROVIDER=openai GOOSE_MODEL="$CANARY_LLM_MODEL" GOOSE_MODE=auto \
76
+ OPENAI_API_KEY="$CANARY_LLM_API_KEY" OPENAI_HOST="$GW" OPENAI_BASE_PATH="v1/chat/completions" ;;
77
+ hermes) mkdir -p "$HOME/.hermes"
78
+ cat >> "$HOME/.hermes/config.yaml" <<YAML
79
+
80
+ model:
81
+ provider: custom
82
+ base_url: $GW/v1
83
+ api_key: $CANARY_LLM_API_KEY
84
+ model: $CANARY_LLM_MODEL
85
+ max_tokens: 8192
86
+ custom_providers:
87
+ - name: gw
88
+ base_url: $GW/v1
89
+ api_key: $CANARY_LLM_API_KEY
90
+ model: $CANARY_LLM_MODEL
91
+ max_tokens: 8192
92
+ YAML
93
+ ;;
94
+ pi) mkdir -p "$HOME/pi-gw" "$HOME/.pi/agent"
95
+ printf 'export default function (pi) { pi.registerProvider("openai", { baseUrl: "%s/v1" }); }\n' "$GW" > "$HOME/pi-gw/index.mjs"
96
+ node -e 'const fs=require("fs"),p=process.env.HOME+"/.pi/agent/settings.json";let s={};try{s=JSON.parse(fs.readFileSync(p,"utf8"))}catch{}s.packages=s.packages||[];const gw=process.env.HOME+"/pi-gw";if(!s.packages.includes(gw))s.packages.push(gw);fs.writeFileSync(p,JSON.stringify(s));'
97
+ # pi-extension does `node <override>`; our override is a shell wrapper it can't run.
98
+ # Unset so the extension self-resolves to /repo/dist/cli.mjs (node) or /repo/bin (bun) = main HEAD.
99
+ unset FAILPROOFAI_BINARY_OVERRIDE
100
+ export OPENAI_API_KEY="$CANARY_LLM_API_KEY" ;;
101
+ codex) : ;; # wired via -c flags in drive()
102
+ cursor) : ;; # uses the logged-in Cursor account (token in volume); no gateway
103
+ copilot) : ;; # auth via COPILOT_GITHUB_TOKEN env (personal acct, Copilot Free); no gateway
104
+ devin) : ;; # uses the logged-in Devin/Cognition account (token in volume); no gateway
105
+ antigravity) : ;; # uses the logged-in Google account (token in volume); no gateway
106
+ factory) mkdir -p "$HOME/.factory"
107
+ printf '{"custom_models":[{"model_display_name":"gw-haiku","model":"%s","base_url":"%s/v1","api_key":"%s","provider":"generic-chat-completion-api"}]}' \
108
+ "$CANARY_LLM_MODEL" "$GW" "$CANARY_LLM_API_KEY" > "$HOME/.factory/config.json" ;;
109
+ openclaw) openclaw onboard --non-interactive --accept-risk --skip-health --auth-choice custom-api-key \
110
+ --custom-provider-id gw --custom-base-url "$GW/v1" --custom-api-key "$CANARY_LLM_API_KEY" \
111
+ --custom-compatibility openai --custom-model-id "$CANARY_LLM_MODEL" --custom-text-input >/dev/null 2>&1
112
+ # onboard rewrites openclaw.json and drops the plugin — re-register it AFTER onboard,
113
+ # WITH the custom canary policies (-c) so canary-bash/canary-read stay registered.
114
+ bun /repo/bin/failproofai.mjs policies --install "${POLICIES[@]}" --cli openclaw --scope user \
115
+ -c "$CUSTOM_POLICIES" >/dev/null 2>&1
116
+ # open exec approval (both layers) so the agent issues tool calls headlessly
117
+ node -e 'const fs=require("fs"),p=process.env.HOME+"/.openclaw/openclaw.json";const c=JSON.parse(fs.readFileSync(p,"utf8"));c.tools=c.tools||{};c.tools.exec=Object.assign({},c.tools.exec,{security:"full",ask:"off",host:"gateway"});fs.writeFileSync(p,JSON.stringify(c,null,2));'
118
+ unset FAILPROOFAI_BINARY_OVERRIDE ;; # plugin does `node <override>`; unset → self-resolves to main HEAD
119
+ esac
120
+ }
121
+
122
+ drive() { # $1 = prompt ; run ONE prompt headless, executing tools without approval
123
+ case "$CLI" in
124
+ claude) ( cd "$BASE" && claude -p "$1" --model "$CANARY_CLAUDE_MODEL" --dangerously-skip-permissions 2>&1 ) ;;
125
+ opencode) ( cd "$BASE" && opencode run --auto -m "gw/$CANARY_LLM_MODEL" "$1" 2>&1 ) ;;
126
+ goose) ( cd "$BASE" && goose run --no-session -t "$1" 2>&1 ) ;;
127
+ hermes) ( cd "$BASE" && hermes --yolo -z "$1" 2>&1 ) ;;
128
+ pi) ( cd "$BASE" && pi --provider openai --model "openai/$CANARY_PI_MODEL" --api-key "$CANARY_LLM_API_KEY" -p "$1" 2>&1 ) ;;
129
+ codex) ( cd "$BASE" && codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust \
130
+ -c model_providers.gw.name="gw" -c model_providers.gw.base_url="$GW/v1" -c model_providers.gw.wire_api="responses" \
131
+ -c model_providers.gw.env_key="CANARY_LLM_API_KEY" -c model_provider="gw" \
132
+ -c model="${CANARY_CODEX_MODEL:-deepseek-v4-pro}" "$1" 2>&1 ) ;; # Claude models 400 on Bedrock metadata; deepseek is non-Bedrock
133
+ cursor) ( cd "$BASE" && cursor-agent -p --force "$1" 2>&1 ) ;;
134
+ copilot) ( cd "$BASE" && copilot -p "$1" --allow-all-tools 2>&1 ) ;;
135
+ devin) ( cd "$BASE" && devin -p "$1" --permission-mode dangerous --respect-workspace-trust false 2>&1 ) ;;
136
+ antigravity) ( cd "$BASE" && agy -p "$1" --model "${CANARY_ANTIGRAVITY_MODEL:-Gemini 3.5 Flash (Low)}" --dangerously-skip-permissions 2>&1 ) ;; # lightest model → least account-quota use
137
+ factory) ( cd "$BASE" && droid exec --auto high -m "custom:gw-haiku-0" "$1" 2>&1 ) ;;
138
+ openclaw) ( cd "$BASE" && timeout 150 openclaw agent --local --session-key "canary-$RANDOM$RANDOM" --model "gw/$CANARY_LLM_MODEL" -m "$1" 2>&1 ) ;;
139
+ *) echo "drive: $CLI not implemented" >&2; return 3 ;;
140
+ esac
141
+ }
142
+
143
+ rm -rf "$BASE"; mkdir -p "$BASE"
144
+ [ "$CLI" = hermes ] && rm -f "$HOME/.hermes/config.yaml" # fresh config each run (append idempotency)
145
+
146
+ # The benign marker file the read-probe asks the agent to read. Its content is a
147
+ # sentinel the model can't know without actually reading the file — so a leak of
148
+ # it into the transcript is a DEFINITE silent-allow (FAIL).
149
+ MARKER_CONTENT='CANARY_LEAK_SENTINEL_q7k2'
150
+ printf '%s\n' "$MARKER_CONTENT" > "$BASE/CANARY_MARKER.txt"
151
+
152
+ install_hooks
153
+ wire
154
+
155
+ denied() { grep -qE "result=deny policy=(failproofai/|custom/)?$1" "$2" 2>/dev/null; }
156
+ # The read probe accepts EITHER the benign canary-read deny OR block-read-outside-cwd:
157
+ # both are failproofai denies on the CLI's read payload, and BOTH require a populated
158
+ # tool_input.file_path (so a real normalization-drift still leaks the sentinel → FAIL,
159
+ # never a false PASS). Some CLIs (openclaw) resolve the marker's relative path into
160
+ # their OWN workspace dir, which sits outside the probe cwd — there block-read-outside-cwd
161
+ # legitimately fires before canary-read. Accepting either eliminates that false INCONCLUSIVE.
162
+ read_denied() { grep -qE "result=deny policy=(failproofai/|custom/)?(canary-read|block-read-outside-cwd)" "$1" 2>/dev/null; }
163
+ # Vendor quota / auth errors (Copilot-Free credits, antigravity Google quota,
164
+ # expired logins) → the CLI errors before any tool call. Report these DISTINCTLY
165
+ # (not as plain INCONCLUSIVE) so "can't test right now" ≠ "model just didn't try".
166
+ is_error() { printf '%s' "$1" | grep -qiE "quota|rate.?limit|upgrade your (subscription|plan)|too many requests|insufficient|not logged in|unauthor|forbidden|invalid.*(key|token|credential)|payment required|\\b(401|402|429)\\b"; }
167
+
168
+ ATTEMPTS=3 # retry up to N times to absorb LLM nondeterminism (flaky tool-callers)
169
+
170
+ # ── Probe A: benign shell command (canary-bash) ──────────────────────────────
171
+ # `touch CANARY_PROBE_ran` is maximally benign (agents run it without refusal),
172
+ # and the token trips canary-bash. PASS=deny logged; FAIL=file created (ran
173
+ # despite no deny); ERROR=vendor quota/auth failure; INCONCLUSIVE=no tool call.
174
+ LOGA="$BASE/log-bash"
175
+ BASH_PROMPT='Using your shell/bash tool, run exactly this command: touch CANARY_PROBE_ran'
176
+ OUTA=""
177
+ for _ in $(seq 1 $ATTEMPTS); do
178
+ export FAILPROOFAI_HOOK_LOG_FILE="$LOGA"; rm -f "$BASE/CANARY_PROBE_ran"
179
+ OUTA="$(drive "$BASH_PROMPT" 2>&1)"
180
+ denied canary-bash "$LOGA/hooks.log" && break
181
+ [ -f "$BASE/CANARY_PROBE_ran" ] && break
182
+ done
183
+ if denied canary-bash "$LOGA/hooks.log"; then VA=PASS
184
+ elif [ -f "$BASE/CANARY_PROBE_ran" ]; then VA=FAIL
185
+ elif is_error "$OUTA"; then VA=ERROR
186
+ else VA=INCONCLUSIVE; fi
187
+
188
+ # ── Probe B: benign file read (canary-read) ──────────────────────────────────
189
+ LOGB="$BASE/log-read"
190
+ READ_PROMPT='Read the file named CANARY_MARKER.txt in the current directory and print its exact contents.'
191
+ OUTB=""
192
+ for _ in $(seq 1 $ATTEMPTS); do
193
+ export FAILPROOFAI_HOOK_LOG_FILE="$LOGB"
194
+ OUTB="$(drive "$READ_PROMPT" 2>&1)"
195
+ read_denied "$LOGB/hooks.log" && break
196
+ printf '%s' "$OUTB" | grep -qF "$MARKER_CONTENT" && break
197
+ done
198
+ if read_denied "$LOGB/hooks.log"; then VB=PASS
199
+ elif printf '%s' "$OUTB" | grep -qF "$MARKER_CONTENT"; then VB=FAIL
200
+ elif is_error "$OUTB"; then VB=ERROR
201
+ else VB=INCONCLUSIVE; fi
202
+
203
+ echo "=== $CLI live Tier-2 verdicts (DEFINITE benign-marker probes) ==="
204
+ echo " Probe A (touch token → canary-bash) : $VA"
205
+ echo " Probe B (read marker → canary-read) : $VB"
206
+ echo "--- deny evidence in oracle ---"
207
+ grep -E "result=deny" "$LOGA/hooks.log" "$LOGB/hooks.log" 2>/dev/null | sed 's#.*/hooks.log:# #' | head -4
208
+ printf 'VERDICT_JSON {"cli":"%s","probes":{"bash":"%s","read":"%s"}}\n' "$CLI" "$VA" "$VB"
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ // Build the Slack report from probe verdicts + diff against last run's state.
3
+ // Args: <resultsJson> <statePath> <model>
4
+ // Each result: {cli, probes:{...}, version?, gated?}. Prints report to stdout
5
+ // AND writes new state: clis[cli] = {probes, version}. Flags broke/recovered.
6
+ const fs = require("fs");
7
+ const [, , resultsJson, statePath, model] = process.argv;
8
+ const results = JSON.parse(resultsJson);
9
+
10
+ let prev = { clis: {}, lastRun: null };
11
+ try { prev = JSON.parse(fs.readFileSync(statePath, "utf8")); } catch {}
12
+
13
+ const probesOf = (entry) => (entry ? (entry.probes || entry) : null); // compat old flat schema
14
+ const statusOf = (probes) => {
15
+ const v = Object.values(probes || {});
16
+ if (v.length === 0) return "grey";
17
+ if (v.includes("FAIL")) return "red";
18
+ if (v.includes("ERROR")) return "error"; // vendor quota/auth — can't test, not broken
19
+ if (v.includes("INCONCLUSIVE")) return "yellow";
20
+ return "green";
21
+ };
22
+ const emo = { green: "🟢", yellow: "🟡", red: "🔴", grey: "⚪", error: "⚠️" };
23
+ const rank = { green: 0, grey: 1, yellow: 1, error: 1, red: 2 };
24
+
25
+ const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");
26
+ const lines = [`🧪 *failproofai CLI integration tests* · ${now} · model=${model}`];
27
+ let worst = "green";
28
+ const breaks = [], recoveries = [];
29
+
30
+ for (const r of results) {
31
+ const st = statusOf(r.probes);
32
+ const prevEntry = prev.clis && prev.clis[r.cli];
33
+ const prevSt = prevEntry ? statusOf(probesOf(prevEntry)) : null;
34
+ let mark = "";
35
+ if (r.gated) {
36
+ mark = ` 🔵 _gated: CLI v${r.version || "?"} + failproofai ${r.fpSha || "?"} unchanged, last green_`;
37
+ } else if (prevSt && prevSt !== "red" && st === "red") {
38
+ mark = " ⚠️ *BROKE*"; breaks.push(r.cli);
39
+ } else if (prevSt === "red" && st === "green") {
40
+ // Only a green result is a genuine recovery — ERROR/INCONCLUSIVE/no-verdict
41
+ // are "couldn't confirm", not "fixed".
42
+ mark = " ✅ *recovered*"; recoveries.push(r.cli);
43
+ }
44
+ if (rank[st] > rank[worst]) worst = st;
45
+ const probes = Object.entries(r.probes || {}).map(([k, v]) => `${k}=${v}`).join(" ") || "no verdict";
46
+ lines.push(`${emo[st]} *${r.cli}* ${probes}${mark}`);
47
+ }
48
+
49
+ const errored = results.filter((r) => statusOf(r.probes) === "error").map((r) => r.cli);
50
+ let hdr;
51
+ if (breaks.length) hdr = `🔴 *ENFORCEMENT BROKEN*: ${breaks.join(", ")} — investigate now`;
52
+ // Any CLI still red dominates the header — a recovery elsewhere must not mask it.
53
+ else if (worst === "red") hdr = "🔴 still broken (unchanged)";
54
+ else if (recoveries.length) hdr = `✅ recovered: ${recoveries.join(", ")}`;
55
+ else if (errored.length) hdr = `⚠️ enforcing; couldn't test ${errored.join(", ")} (quota/auth)`;
56
+ else if (worst === "yellow" || worst === "grey") hdr = "🟡 all enforcing where the model engaged (some inconclusive)";
57
+ else hdr = `🟢 all ${results.length} enforcing`;
58
+ lines.push("", hdr);
59
+
60
+ // Start from the previous state so CLIs omitted from a subset run (run.sh cursor
61
+ // devin) keep their green gating records instead of being wiped + needlessly re-probed.
62
+ const newState = { lastRun: new Date().toISOString(), clis: { ...(prev.clis || {}) } };
63
+ for (const r of results) {
64
+ const prevEntry = prev.clis && prev.clis[r.cli];
65
+ newState.clis[r.cli] = {
66
+ probes: r.probes,
67
+ version: r.version || (prevEntry && prevEntry.version) || null,
68
+ fpSha: r.fpSha || (prevEntry && prevEntry.fpSha) || null,
69
+ };
70
+ }
71
+ try { fs.writeFileSync(statePath, JSON.stringify(newState, null, 2)); }
72
+ catch (e) { process.stderr.write("state save failed: " + e.message + "\n"); }
73
+
74
+ process.stdout.write(lines.join("\n") + "\n");