failproofai 0.0.13-beta.2 → 0.0.14-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.factory/hooks.json +112 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -3
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +2 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +1 -1
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -2
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -3
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jllheg._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d9qen3._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +3 -0
- package/.next/standalone/.next/server/chunks/{_1z062h5._.js → _1vslkoe._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +24 -12
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cz9as._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1rlmypf._.js → [root-of-the-server]__07qfsfa._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09u1pcy._.js → [root-of-the-server]__0ge8e7w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h12me5._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11rtg6s._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +12 -9
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dky4g0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__212nf49._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0_9_ytf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0ul5rf_._.js → _1_ozgif._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_antigravity-projects_ts_0_6zf11._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_devin-projects_ts_0a64qdy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_factory-projects_ts_1uyaahq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_goose-projects_ts_0zkscri._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_09fe3kb.js → node_modules_html-to-image_es_index_0g5r64e.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0-uvagv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03c7gi5.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_09p-8om.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0v-kfiu.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0xrgzyz.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1806lsc.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1j6dd-e.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1sa65r-.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/static/chunks/003uz088wbmz7.js +1 -0
- package/.next/standalone/.next/static/chunks/{0l2n5ftar65ox.js → 0405htbxay9vd.js} +1 -1
- package/.next/standalone/.next/static/chunks/0iinhn4417nk1.js +1 -0
- package/.next/standalone/.next/static/chunks/1374x9bv_s_vg.js +6 -0
- package/.next/standalone/.next/static/chunks/{06cw8nbn5c4_r.js → 17sdztgbcc0fi.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0i0joc040j999.js → 2m_tbl39fcedn.js} +1 -1
- package/.next/standalone/.next/static/chunks/{25qvspmcbb_5y.js → 2o3ql0n5owqsw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2a-k91nsa1lgf.js → 2ufuwhv03yvtb.js} +1 -1
- package/.next/standalone/.next/static/chunks/35-90m9py64d4.js +1 -0
- package/.next/standalone/.next/static/chunks/3r--yyjkp9_7b.css +2 -0
- package/.next/standalone/.next/static/chunks/{23t-i9s-q--nu.js → 43weu8f59tlix.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2oz8ok8fzxaj3.js → 454dyizqik6t9.js} +2 -2
- package/.next/standalone/app/api/audit/run/route.ts +2 -0
- package/.next/standalone/app/audit/_components/empty-state.tsx +1 -1
- package/.next/standalone/app/globals.css +25 -1
- package/.next/standalone/app/policies/hooks-client.tsx +2 -4
- package/.next/standalone/app/project/[name]/page.tsx +30 -10
- package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +71 -21
- package/.next/standalone/assets/logos/antigravity.svg +1 -0
- package/.next/standalone/assets/logos/devin.svg +1 -0
- package/.next/standalone/assets/logos/factory-dark.png +0 -0
- package/.next/standalone/assets/logos/factory-light.png +0 -0
- package/.next/standalone/assets/logos/goose-dark.svg +1 -0
- package/.next/standalone/assets/logos/goose-light.svg +1 -0
- package/.next/standalone/assets/logos/openclaw.svg +22 -0
- package/.next/standalone/components/navbar.tsx +5 -11
- package/.next/standalone/components/reach-developers.tsx +1 -1
- package/.next/standalone/lib/antigravity-projects.ts +224 -0
- package/.next/standalone/lib/antigravity-sessions.ts +283 -0
- package/.next/standalone/lib/cli-registry.ts +26 -6
- package/.next/standalone/lib/devin-projects.ts +141 -0
- package/.next/standalone/lib/devin-sessions.ts +343 -0
- package/.next/standalone/lib/download-session.ts +56 -5
- package/.next/standalone/lib/factory-projects.ts +109 -0
- package/.next/standalone/lib/factory-sessions.ts +312 -0
- package/.next/standalone/lib/goose-projects.ts +148 -0
- package/.next/standalone/lib/goose-sessions.ts +296 -0
- package/.next/standalone/lib/openclaw-projects.ts +182 -0
- package/.next/standalone/lib/openclaw-sessions.ts +284 -0
- package/.next/standalone/lib/projects.ts +33 -9
- package/.next/standalone/lib/telemetry-sanitize.ts +43 -0
- package/.next/standalone/openclaw-plugin/index.js +223 -0
- package/.next/standalone/openclaw-plugin/openclaw.plugin.json +6 -0
- package/.next/standalone/openclaw-plugin/package.json +12 -0
- package/.next/standalone/package.json +4 -3
- package/.next/standalone/server.js +1 -1
- package/README.md +105 -57
- package/bin/failproofai.mjs +87 -29
- package/dist/cli.mjs +12803 -9809
- package/lib/antigravity-projects.ts +224 -0
- package/lib/antigravity-sessions.ts +283 -0
- package/lib/cli-registry.ts +26 -6
- package/lib/devin-projects.ts +141 -0
- package/lib/devin-sessions.ts +343 -0
- package/lib/download-session.ts +56 -5
- package/lib/factory-projects.ts +109 -0
- package/lib/factory-sessions.ts +312 -0
- package/lib/goose-projects.ts +148 -0
- package/lib/goose-sessions.ts +296 -0
- package/lib/openclaw-projects.ts +182 -0
- package/lib/openclaw-sessions.ts +284 -0
- package/lib/projects.ts +33 -9
- package/lib/telemetry-sanitize.ts +43 -0
- package/openclaw-plugin/index.js +223 -0
- package/openclaw-plugin/openclaw.plugin.json +6 -0
- package/openclaw-plugin/package.json +12 -0
- package/package.json +4 -3
- package/scripts/launch.ts +4 -8
- package/scripts/postinstall.mjs +3 -4
- package/scripts/sync-agent-cli-harnesses-prompt.md +7 -10
- package/scripts/translate-docs/cli.ts +73 -52
- package/scripts/translate-docs/config.ts +57 -13
- package/scripts/translate-docs/mdx-translator.ts +51 -31
- package/scripts/translate-docs/mintlify-nav.ts +91 -10
- package/scripts/translate-docs/types.ts +3 -0
- package/src/audit/cli-adapters/antigravity.ts +87 -0
- package/src/audit/cli-adapters/devin.ts +53 -0
- package/src/audit/cli-adapters/factory.ts +75 -0
- package/src/audit/cli-adapters/goose.ts +53 -0
- package/src/audit/cli-adapters/index.ts +30 -6
- package/src/audit/cli-adapters/openclaw.ts +56 -0
- package/src/audit/cli.ts +44 -1
- package/src/audit/report.ts +1 -1
- package/src/audit/types.ts +1 -1
- package/src/hooks/builtin-policies.ts +5 -5
- package/src/hooks/configure-wizard.ts +356 -0
- package/src/hooks/handler.ts +283 -218
- package/src/hooks/hook-telemetry.ts +43 -3
- package/src/hooks/install-prompt.ts +92 -173
- package/src/hooks/integrations.ts +739 -138
- package/src/hooks/manager.ts +46 -2
- package/src/hooks/policy-evaluator.ts +301 -121
- package/src/hooks/policy-presets.ts +71 -0
- package/src/hooks/resolve-cwd.ts +1 -1
- package/src/hooks/resolve-permission-mode.ts +1 -5
- package/src/hooks/resolve-transcript-path.ts +25 -7
- package/src/hooks/tool-name-canonicalize.ts +36 -3
- package/src/hooks/tui.ts +578 -0
- package/src/hooks/types.ts +447 -109
- package/.next/standalone/.gemini/settings.json +0 -147
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cag8qd._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cefjd9._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ekj92q._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12v-lma._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__136y39g._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13h8pzr._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hgv_75._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04bqvos._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dqgs96._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0q5rfxg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qukut7._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ql-mch._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_11v0g3z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_1c7bgx-._.js +0 -3
- package/.next/standalone/.next/static/chunks/0fa7j157kg9_d.js +0 -1
- package/.next/standalone/.next/static/chunks/1c8i78p7t1i-5.js +0 -1
- package/.next/standalone/.next/static/chunks/1sg8kx5szx42y.js +0 -6
- package/.next/standalone/.next/static/chunks/25s52un3k_wht.css +0 -2
- package/.next/standalone/.next/static/chunks/2rkvn4znvuy4o.js +0 -1
- package/.next/standalone/assets/logos/gemini-dark.svg +0 -13
- package/.next/standalone/assets/logos/gemini-light.svg +0 -13
- package/.next/standalone/lib/gemini-projects.ts +0 -243
- package/.next/standalone/lib/gemini-sessions.ts +0 -365
- package/lib/gemini-projects.ts +0 -243
- package/lib/gemini-sessions.ts +0 -365
- package/src/audit/cli-adapters/gemini.ts +0 -51
- package/src/hooks/first-run-nudge.ts +0 -146
- /package/.next/standalone/.next/static/{HgG0mizCfOM3XyN1EW-sL → MNJWFb2jLRrxRKGFNK6Hy}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{HgG0mizCfOM3XyN1EW-sL → MNJWFb2jLRrxRKGFNK6Hy}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{HgG0mizCfOM3XyN1EW-sL → MNJWFb2jLRrxRKGFNK6Hy}/_ssgManifest.js +0 -0
package/src/hooks/handler.ts
CHANGED
|
@@ -12,15 +12,17 @@ import type {
|
|
|
12
12
|
CodexHookEventType,
|
|
13
13
|
CursorHookEventType,
|
|
14
14
|
PiHookEventType,
|
|
15
|
-
GeminiHookEventType,
|
|
16
15
|
HermesHookEventType,
|
|
16
|
+
OpenClawHookEventType,
|
|
17
|
+
AntigravityHookEventType,
|
|
17
18
|
} from "./types";
|
|
18
19
|
import {
|
|
19
20
|
CODEX_EVENT_MAP,
|
|
20
21
|
CURSOR_EVENT_MAP,
|
|
21
22
|
PI_EVENT_MAP,
|
|
22
|
-
GEMINI_EVENT_MAP,
|
|
23
23
|
HERMES_EVENT_MAP,
|
|
24
|
+
OPENCLAW_EVENT_MAP,
|
|
25
|
+
ANTIGRAVITY_EVENT_MAP,
|
|
24
26
|
} from "./types";
|
|
25
27
|
import { canonicalizeToolName, canonicalizeToolInput } from "./tool-name-canonicalize";
|
|
26
28
|
import type { PolicyFunction, PolicyResult } from "./policy-types";
|
|
@@ -31,7 +33,7 @@ import { clearPolicies, registerPolicy } from "./policy-registry";
|
|
|
31
33
|
import { loadAllCustomHooks } from "./custom-hooks-loader";
|
|
32
34
|
import type { CustomHook } from "./policy-types";
|
|
33
35
|
import { persistHookActivity } from "./hook-activity-store";
|
|
34
|
-
import { trackHookEvent } from "./hook-telemetry";
|
|
36
|
+
import { trackHookEvent, flushHookTelemetry } from "./hook-telemetry";
|
|
35
37
|
import { resolveCwd } from "./resolve-cwd";
|
|
36
38
|
import { resolvePermissionMode } from "./resolve-permission-mode";
|
|
37
39
|
import { resolveTranscriptPath } from "./resolve-transcript-path";
|
|
@@ -45,9 +47,7 @@ import { hookLogInfo, hookLogWarn } from "./hook-logger";
|
|
|
45
47
|
* `session_start`); Claude Code sends PascalCase. Copilot CLI is installed
|
|
46
48
|
* in "VS Code compatible" PascalCase mode (see integrations.ts), so its event
|
|
47
49
|
* NAMES arrive PascalCase already (note: tool names are a separate matter and
|
|
48
|
-
* are canonicalized in canonicalizeToolName below).
|
|
49
|
-
* PascalCase event names but with different spellings (`BeforeTool`,
|
|
50
|
-
* `BeforeAgent`, `AfterAgent`); we map via GEMINI_EVENT_MAP. The internal
|
|
50
|
+
* are canonicalized in canonicalizeToolName below). The internal
|
|
51
51
|
* registry, builtin policies, and policy.match.events all key on PascalCase.
|
|
52
52
|
*/
|
|
53
53
|
function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType {
|
|
@@ -63,16 +63,25 @@ function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType
|
|
|
63
63
|
const mapped = PI_EVENT_MAP[raw as PiHookEventType];
|
|
64
64
|
if (mapped) return mapped;
|
|
65
65
|
}
|
|
66
|
-
if (cli === "gemini") {
|
|
67
|
-
const mapped = GEMINI_EVENT_MAP[raw as GeminiHookEventType];
|
|
68
|
-
if (mapped) return mapped;
|
|
69
|
-
}
|
|
70
66
|
if (cli === "hermes") {
|
|
71
67
|
// Hermes sends snake_case event names (pre_tool_call, on_session_start, …);
|
|
72
68
|
// map to PascalCase. Has no turn-end Stop event, so no Stop mapping exists.
|
|
73
69
|
const mapped = HERMES_EVENT_MAP[raw as HermesHookEventType];
|
|
74
70
|
if (mapped) return mapped;
|
|
75
71
|
}
|
|
72
|
+
if (cli === "openclaw") {
|
|
73
|
+
// The openclaw-plugin shim forwards raw snake_case plugin-hook names
|
|
74
|
+
// (before_tool_call, before_agent_finalize, …); map to PascalCase.
|
|
75
|
+
// before_agent_finalize is the real turn-end gate → Stop.
|
|
76
|
+
const mapped = OPENCLAW_EVENT_MAP[raw as OpenClawHookEventType];
|
|
77
|
+
if (mapped) return mapped;
|
|
78
|
+
}
|
|
79
|
+
if (cli === "antigravity") {
|
|
80
|
+
// Antigravity's --hook args are PreToolUse|PostToolUse|PreInvocation|Stop.
|
|
81
|
+
// PreInvocation (before-model) → UserPromptSubmit. Verified agy v1.1.2.
|
|
82
|
+
const mapped = ANTIGRAVITY_EVENT_MAP[raw as AntigravityHookEventType];
|
|
83
|
+
if (mapped) return mapped;
|
|
84
|
+
}
|
|
76
85
|
// claude / copilot / unknown — already PascalCase, pass through.
|
|
77
86
|
// HOOK_EVENT_TYPES type-checks downstream.
|
|
78
87
|
return raw as HookEventType;
|
|
@@ -83,239 +92,295 @@ export async function handleHookEvent(
|
|
|
83
92
|
cli: IntegrationType = "claude",
|
|
84
93
|
): Promise<number> {
|
|
85
94
|
const startTime = performance.now();
|
|
86
|
-
|
|
87
|
-
// Read stdin payload (Claude passes JSON)
|
|
88
|
-
const MAX_STDIN_BYTES = 1_048_576; // 1 MB
|
|
89
|
-
let payload = "";
|
|
90
|
-
let stdinOversized = false;
|
|
91
95
|
try {
|
|
92
|
-
payload = await new Promise<string>((resolve, reject) => {
|
|
93
|
-
const chunks: string[] = [];
|
|
94
|
-
let totalBytes = 0;
|
|
95
|
-
process.stdin.setEncoding("utf8");
|
|
96
|
-
process.stdin.on("data", (chunk: string) => {
|
|
97
|
-
totalBytes += Buffer.byteLength(chunk);
|
|
98
|
-
if (totalBytes > MAX_STDIN_BYTES) {
|
|
99
|
-
hookLogWarn(`stdin payload exceeds 1 MB for ${eventType}, discarding`);
|
|
100
|
-
stdinOversized = true;
|
|
101
|
-
process.stdin.destroy();
|
|
102
|
-
resolve("");
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
chunks.push(chunk);
|
|
106
|
-
});
|
|
107
|
-
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
108
|
-
process.stdin.on("error", reject);
|
|
109
|
-
// If stdin is already closed or not piped, resolve immediately
|
|
110
|
-
if (process.stdin.readableEnded) resolve("");
|
|
111
|
-
});
|
|
112
|
-
} catch (err) {
|
|
113
|
-
hookLogWarn(`stdin read failed for ${eventType}`);
|
|
114
|
-
void trackHookEvent(getInstanceId(), "hook_stdin_error", {
|
|
115
|
-
event_type: eventType,
|
|
116
|
-
cli,
|
|
117
|
-
error_type: err instanceof Error ? err.name : "unknown",
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
if (stdinOversized) {
|
|
121
|
-
void trackHookEvent(getInstanceId(), "hook_stdin_error", {
|
|
122
|
-
event_type: eventType,
|
|
123
|
-
cli,
|
|
124
|
-
error_type: "oversized",
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
96
|
|
|
128
|
-
|
|
129
|
-
|
|
97
|
+
// Read stdin payload (Claude passes JSON)
|
|
98
|
+
const MAX_STDIN_BYTES = 1_048_576; // 1 MB
|
|
99
|
+
let payload = "";
|
|
100
|
+
let stdinOversized = false;
|
|
130
101
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
102
|
+
payload = await new Promise<string>((resolve, reject) => {
|
|
103
|
+
const chunks: string[] = [];
|
|
104
|
+
let totalBytes = 0;
|
|
105
|
+
process.stdin.setEncoding("utf8");
|
|
106
|
+
process.stdin.on("data", (chunk: string) => {
|
|
107
|
+
totalBytes += Buffer.byteLength(chunk);
|
|
108
|
+
if (totalBytes > MAX_STDIN_BYTES) {
|
|
109
|
+
hookLogWarn(`stdin payload exceeds 1 MB for ${eventType}, discarding`);
|
|
110
|
+
stdinOversized = true;
|
|
111
|
+
process.stdin.destroy();
|
|
112
|
+
resolve("");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
chunks.push(chunk);
|
|
116
|
+
});
|
|
117
|
+
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
118
|
+
process.stdin.on("error", reject);
|
|
119
|
+
// If stdin is already closed or not piped, resolve immediately
|
|
120
|
+
if (process.stdin.readableEnded) resolve("");
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
hookLogWarn(`stdin read failed for ${eventType}`);
|
|
124
|
+
void trackHookEvent(getInstanceId(), "hook_stdin_error", {
|
|
135
125
|
event_type: eventType,
|
|
136
126
|
cli,
|
|
137
|
-
|
|
127
|
+
error_type: err instanceof Error ? err.name : "unknown",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (stdinOversized) {
|
|
131
|
+
void trackHookEvent(getInstanceId(), "hook_stdin_error", {
|
|
132
|
+
event_type: eventType,
|
|
133
|
+
cli,
|
|
134
|
+
error_type: "oversized",
|
|
138
135
|
});
|
|
139
136
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Canonicalize event name (Codex sends snake_case; internals expect PascalCase)
|
|
143
|
-
const canonicalEventType = canonicalizeEventType(eventType, cli);
|
|
144
|
-
|
|
145
|
-
// Canonicalize tool name in place so both the policy-registry tool-name
|
|
146
|
-
// filter and policy bodies (`ctx.toolName === "Bash"`) see the canonical
|
|
147
|
-
// form. Mutating `parsed.tool_name` keeps the activity store + telemetry
|
|
148
|
-
// tagging consistent (they read from `parsed.tool_name`).
|
|
149
|
-
const rawToolName = parsed.tool_name as string | undefined;
|
|
150
|
-
const canonicalToolName = canonicalizeToolName(rawToolName, cli);
|
|
151
|
-
if (canonicalToolName !== rawToolName) {
|
|
152
|
-
parsed.tool_name = canonicalToolName;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Canonicalize tool-input keys for OpenCode + Pi (no-op for other CLIs).
|
|
156
|
-
// Defense-in-depth against stale shims that still pass camelCase /
|
|
157
|
-
// Pi-shape keys to the binary. The per-CLI shim ALSO canonicalizes; both
|
|
158
|
-
// passes are idempotent because the camelCase keys won't match a
|
|
159
|
-
// snake_case input.
|
|
160
|
-
const rawInput = parsed.tool_input;
|
|
161
|
-
const canonicalInput = canonicalizeToolInput(canonicalToolName, rawInput, cli);
|
|
162
|
-
if (canonicalInput !== rawInput) {
|
|
163
|
-
parsed.tool_input = canonicalInput;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Extract session metadata from payload
|
|
167
|
-
const sessionId = parsed.session_id as string | undefined;
|
|
168
|
-
const session: SessionMetadata = {
|
|
169
|
-
sessionId,
|
|
170
|
-
transcriptPath: resolveTranscriptPath(cli, parsed, sessionId),
|
|
171
|
-
cwd: resolveCwd(cli, parsed),
|
|
172
|
-
permissionMode: resolvePermissionMode(cli, parsed, sessionId),
|
|
173
|
-
hookEventName: parsed.hook_event_name as string | undefined,
|
|
174
|
-
// Preserve the raw CLI-side event name (eventType arg) before
|
|
175
|
-
// canonicalization. Response shapes that round-trip the agent-emitted
|
|
176
|
-
// event name (e.g. Gemini's `hookSpecificOutput.hookEventName`) prefer
|
|
177
|
-
// this over the canonicalized form when stdin omits hook_event_name.
|
|
178
|
-
rawHookEventName: eventType,
|
|
179
|
-
cli,
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// Load enabled policies (merge across project/local/global scopes)
|
|
183
|
-
const config = readMergedHooksConfig(session.cwd);
|
|
184
|
-
clearPolicies();
|
|
185
|
-
registerBuiltinPolicies(config.enabledPolicies);
|
|
186
|
-
|
|
187
|
-
// Load and register custom hooks (layer 2, after builtins)
|
|
188
|
-
const loadResult = await loadAllCustomHooks(config.customPoliciesPath, { sessionCwd: session.cwd });
|
|
189
|
-
const customHooksList = loadResult.hooks;
|
|
190
|
-
const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
|
|
191
137
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const conventionScope = (hook as CustomHook & { __conventionScope?: string }).__conventionScope;
|
|
195
|
-
const isConvention = !!conventionScope;
|
|
196
|
-
const prefix = isConvention ? `.failproofai-${conventionScope}` : "custom";
|
|
197
|
-
const fn: PolicyFunction = async (ctx): Promise<PolicyResult> => {
|
|
138
|
+
let parsed: Record<string, unknown> = {};
|
|
139
|
+
if (payload) {
|
|
198
140
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
),
|
|
204
|
-
]);
|
|
205
|
-
return result;
|
|
206
|
-
} catch (err) {
|
|
207
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
-
const isTimeout = msg === "timeout";
|
|
209
|
-
hookLogWarn(`${prefix} hook "${hookName}" failed: ${msg}`);
|
|
210
|
-
void trackHookEvent(getInstanceId(), "custom_hook_error", {
|
|
211
|
-
hook_name: hookName,
|
|
212
|
-
error_type: isTimeout ? "timeout" : "exception",
|
|
141
|
+
parsed = JSON.parse(payload) as Record<string, unknown>;
|
|
142
|
+
} catch {
|
|
143
|
+
hookLogWarn(`payload parse failed for ${eventType} (${payload.length} bytes)`);
|
|
144
|
+
void trackHookEvent(getInstanceId(), "hook_payload_parse_error", {
|
|
213
145
|
event_type: eventType,
|
|
214
146
|
cli,
|
|
215
|
-
|
|
216
|
-
convention_scope: conventionScope ?? null,
|
|
147
|
+
payload_size: payload.length,
|
|
217
148
|
});
|
|
218
|
-
return { decision: "allow" };
|
|
219
149
|
}
|
|
220
|
-
}
|
|
221
|
-
registerPolicy(
|
|
222
|
-
`${prefix}/${hookName}`,
|
|
223
|
-
hook.description ?? "",
|
|
224
|
-
fn,
|
|
225
|
-
hook.match ?? {},
|
|
226
|
-
-1, // Custom hooks run after builtins (priority 0)
|
|
227
|
-
);
|
|
228
|
-
}
|
|
150
|
+
}
|
|
229
151
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
152
|
+
// Antigravity (agy) pipes a camelCase protojson payload; normalize the fields
|
|
153
|
+
// the handler downstream reads to canonical snake_case BEFORE any
|
|
154
|
+
// canonicalization runs. `toolCall.{name,args}` → `tool_name`/`tool_input`,
|
|
155
|
+
// `conversationId` → `session_id`, `workspacePaths[0]` → `cwd`,
|
|
156
|
+
// `transcriptPath` → `transcript_path`. Verified against agy v1.1.2.
|
|
157
|
+
if (cli === "antigravity") {
|
|
158
|
+
const tc = parsed.toolCall as { name?: string; args?: unknown } | undefined;
|
|
159
|
+
if (tc && typeof tc === "object") {
|
|
160
|
+
if (tc.name !== undefined) parsed.tool_name = tc.name;
|
|
161
|
+
if (tc.args !== undefined) parsed.tool_input = tc.args;
|
|
162
|
+
}
|
|
163
|
+
if (typeof parsed.conversationId === "string") parsed.session_id = parsed.conversationId;
|
|
164
|
+
if (Array.isArray(parsed.workspacePaths) && typeof parsed.workspacePaths[0] === "string") {
|
|
165
|
+
parsed.cwd = parsed.workspacePaths[0];
|
|
166
|
+
}
|
|
167
|
+
if (typeof parsed.transcriptPath === "string") parsed.transcript_path = parsed.transcriptPath;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Copilot's snake_case events (PreToolUse/PostToolUse/Stop/…) are already
|
|
171
|
+
// Claude-shaped, but `permissionRequest` alone pipes a camelCase payload
|
|
172
|
+
// (`hookName`, `sessionId`, `toolName` in lowercase, `toolInput`) — verified
|
|
173
|
+
// live against Copilot CLI 1.0.71. Normalize the fields the handler reads so
|
|
174
|
+
// PermissionRequest-matched policies (e.g. block-sudo's Codex-escalation
|
|
175
|
+
// guard) fire instead of seeing a null tool name.
|
|
176
|
+
if (cli === "copilot") {
|
|
177
|
+
if (typeof parsed.toolName === "string" && parsed.tool_name === undefined) {
|
|
178
|
+
parsed.tool_name = parsed.toolName;
|
|
179
|
+
}
|
|
180
|
+
if (parsed.toolInput !== undefined && parsed.tool_input === undefined) {
|
|
181
|
+
parsed.tool_input = parsed.toolInput;
|
|
182
|
+
}
|
|
183
|
+
if (typeof parsed.sessionId === "string" && parsed.session_id === undefined) {
|
|
184
|
+
parsed.session_id = parsed.sessionId;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Goose pipes `event` / `working_dir` instead of Claude's `hook_event_name` /
|
|
189
|
+
// `cwd` (its `tool_name` / `tool_input` are already canonical field names).
|
|
190
|
+
// Normalize both so resolveCwd keeps its cwd (block-read-outside-cwd) and the
|
|
191
|
+
// round-tripped event name is available. The --hook arg is already PascalCase,
|
|
192
|
+
// so canonicalizeEventType needs no goose branch. Verified goose v1.43.0.
|
|
193
|
+
if (cli === "goose") {
|
|
194
|
+
if (typeof parsed.working_dir === "string") parsed.cwd = parsed.working_dir;
|
|
195
|
+
if (typeof parsed.event === "string" && parsed.hook_event_name === undefined) {
|
|
196
|
+
parsed.hook_event_name = parsed.event;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Canonicalize event name (Codex sends snake_case; internals expect PascalCase)
|
|
201
|
+
const canonicalEventType = canonicalizeEventType(eventType, cli);
|
|
202
|
+
|
|
203
|
+
// Canonicalize tool name in place so both the policy-registry tool-name
|
|
204
|
+
// filter and policy bodies (`ctx.toolName === "Bash"`) see the canonical
|
|
205
|
+
// form. Mutating `parsed.tool_name` keeps the activity store + telemetry
|
|
206
|
+
// tagging consistent (they read from `parsed.tool_name`).
|
|
207
|
+
const rawToolName = parsed.tool_name as string | undefined;
|
|
208
|
+
const canonicalToolName = canonicalizeToolName(rawToolName, cli);
|
|
209
|
+
if (canonicalToolName !== rawToolName) {
|
|
210
|
+
parsed.tool_name = canonicalToolName;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Canonicalize tool-input keys for OpenCode + Pi (no-op for other CLIs).
|
|
214
|
+
// Defense-in-depth against stale shims that still pass camelCase /
|
|
215
|
+
// Pi-shape keys to the binary. The per-CLI shim ALSO canonicalizes; both
|
|
216
|
+
// passes are idempotent because the camelCase keys won't match a
|
|
217
|
+
// snake_case input.
|
|
218
|
+
const rawInput = parsed.tool_input;
|
|
219
|
+
const canonicalInput = canonicalizeToolInput(canonicalToolName, rawInput, cli);
|
|
220
|
+
if (canonicalInput !== rawInput) {
|
|
221
|
+
parsed.tool_input = canonicalInput;
|
|
222
|
+
}
|
|
239
223
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
224
|
+
// Extract session metadata from payload
|
|
225
|
+
const sessionId = parsed.session_id as string | undefined;
|
|
226
|
+
const session: SessionMetadata = {
|
|
227
|
+
sessionId,
|
|
228
|
+
transcriptPath: resolveTranscriptPath(cli, parsed, sessionId),
|
|
229
|
+
cwd: resolveCwd(cli, parsed),
|
|
230
|
+
permissionMode: resolvePermissionMode(cli, parsed, sessionId),
|
|
231
|
+
hookEventName: parsed.hook_event_name as string | undefined,
|
|
232
|
+
// Preserve the raw CLI-side event name (eventType arg) before
|
|
233
|
+
// canonicalization. Response shapes that round-trip the agent-emitted
|
|
234
|
+
// event name prefer this over the canonicalized form when stdin omits
|
|
235
|
+
// hook_event_name.
|
|
236
|
+
rawHookEventName: eventType,
|
|
244
237
|
cli,
|
|
245
|
-
|
|
246
|
-
user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
|
|
247
|
-
convention_hook_count: conventionHookNames.size,
|
|
248
|
-
convention_hook_names: [...conventionHookNames],
|
|
249
|
-
});
|
|
250
|
-
}
|
|
238
|
+
};
|
|
251
239
|
|
|
252
|
-
|
|
240
|
+
// Load enabled policies (merge across project/local/global scopes)
|
|
241
|
+
const config = readMergedHooksConfig(session.cwd);
|
|
242
|
+
clearPolicies();
|
|
243
|
+
registerBuiltinPolicies(config.enabledPolicies);
|
|
253
244
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
245
|
+
// Load and register custom hooks (layer 2, after builtins)
|
|
246
|
+
const loadResult = await loadAllCustomHooks(config.customPoliciesPath, { sessionCwd: session.cwd });
|
|
247
|
+
const customHooksList = loadResult.hooks;
|
|
248
|
+
const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
|
|
258
249
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
250
|
+
for (const hook of customHooksList) {
|
|
251
|
+
const hookName = hook.name;
|
|
252
|
+
const conventionScope = (hook as CustomHook & { __conventionScope?: string }).__conventionScope;
|
|
253
|
+
const isConvention = !!conventionScope;
|
|
254
|
+
const prefix = isConvention ? `.failproofai-${conventionScope}` : "custom";
|
|
255
|
+
const fn: PolicyFunction = async (ctx): Promise<PolicyResult> => {
|
|
256
|
+
try {
|
|
257
|
+
const result = await Promise.race([
|
|
258
|
+
hook.fn(ctx),
|
|
259
|
+
new Promise<PolicyResult>((_, reject) =>
|
|
260
|
+
setTimeout(() => reject(new Error("timeout")), 10_000),
|
|
261
|
+
),
|
|
262
|
+
]);
|
|
263
|
+
return result;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
266
|
+
const isTimeout = msg === "timeout";
|
|
267
|
+
hookLogWarn(`${prefix} hook "${hookName}" failed: ${msg}`);
|
|
268
|
+
void trackHookEvent(getInstanceId(), "custom_hook_error", {
|
|
269
|
+
hook_name: hookName,
|
|
270
|
+
error_type: isTimeout ? "timeout" : "exception",
|
|
271
|
+
event_type: eventType,
|
|
272
|
+
cli,
|
|
273
|
+
is_convention_policy: isConvention,
|
|
274
|
+
convention_scope: conventionScope ?? null,
|
|
275
|
+
});
|
|
276
|
+
return { decision: "allow" };
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
registerPolicy(
|
|
280
|
+
`${prefix}/${hookName}`,
|
|
281
|
+
hook.description ?? "",
|
|
282
|
+
fn,
|
|
283
|
+
hook.match ?? {},
|
|
284
|
+
-1, // Custom hooks run after builtins (priority 0)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
265
287
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
reason: result.reason,
|
|
276
|
-
durationMs,
|
|
277
|
-
sessionId: session.sessionId,
|
|
278
|
-
transcriptPath: session.transcriptPath,
|
|
279
|
-
cwd: session.cwd,
|
|
280
|
-
permissionMode: session.permissionMode,
|
|
281
|
-
hookEventName: session.hookEventName,
|
|
282
|
-
};
|
|
283
|
-
try {
|
|
284
|
-
persistHookActivity(activityEntry);
|
|
285
|
-
} catch {
|
|
286
|
-
hookLogWarn("activity persistence failed");
|
|
287
|
-
}
|
|
288
|
+
// Fire telemetry once per invocation for custom hook loads
|
|
289
|
+
if (customHooksList.length > 0) {
|
|
290
|
+
void trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
|
|
291
|
+
cli,
|
|
292
|
+
custom_hooks_count: customHooksList.length,
|
|
293
|
+
custom_hook_names: customHooksList.map((h) => h.name),
|
|
294
|
+
event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
288
297
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
|
|
293
|
-
const isConventionPolicy = result.policyName?.startsWith(".failproofai-") ?? false;
|
|
294
|
-
const conventionScope = isConventionPolicy
|
|
295
|
-
? result.policyName!.match(/^\.failproofai-(project|user)\//)?.[1] ?? null
|
|
296
|
-
: null;
|
|
297
|
-
const hasCustomParams =
|
|
298
|
-
!isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
|
|
299
|
-
const paramKeysOverridden = hasCustomParams
|
|
300
|
-
? Object.keys(config.policyParams![result.policyName!])
|
|
301
|
-
: [];
|
|
302
|
-
const distinctId = getInstanceId();
|
|
303
|
-
await trackHookEvent(distinctId, "hook_policy_triggered", {
|
|
298
|
+
// Fire telemetry for convention-based policy discovery
|
|
299
|
+
if (loadResult.conventionSources.length > 0) {
|
|
300
|
+
void trackHookEvent(getInstanceId(), "convention_policies_loaded", {
|
|
304
301
|
event_type: canonicalEventType,
|
|
305
302
|
cli,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
is_convention_policy: isConventionPolicy,
|
|
311
|
-
convention_scope: conventionScope,
|
|
312
|
-
has_custom_params: hasCustomParams,
|
|
313
|
-
param_keys_overridden: paramKeysOverridden,
|
|
303
|
+
project_file_count: loadResult.conventionSources.filter((s) => s.scope === "project").length,
|
|
304
|
+
user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
|
|
305
|
+
convention_hook_count: conventionHookNames.size,
|
|
306
|
+
convention_hook_names: [...conventionHookNames],
|
|
314
307
|
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
hookLogInfo(`event=${canonicalEventType} cli=${cli} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
|
|
311
|
+
|
|
312
|
+
// Evaluate policies (use canonical PascalCase event type)
|
|
313
|
+
const result = await evaluatePolicies(canonicalEventType, parsed, session, config);
|
|
314
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
315
|
+
hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
|
|
316
|
+
|
|
317
|
+
if (result.stdout) {
|
|
318
|
+
process.stdout.write(result.stdout);
|
|
319
|
+
}
|
|
320
|
+
if (result.stderr) {
|
|
321
|
+
process.stderr.write(result.stderr);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Persist activity to disk (visible in /policies activity tab)
|
|
325
|
+
const activityEntry = {
|
|
326
|
+
timestamp: Date.now(),
|
|
327
|
+
eventType: canonicalEventType,
|
|
328
|
+
integration: cli,
|
|
329
|
+
toolName: (parsed.tool_name as string) ?? null,
|
|
330
|
+
policyName: result.policyName,
|
|
331
|
+
policyNames: result.policyNames,
|
|
332
|
+
decision: result.decision,
|
|
333
|
+
reason: result.reason,
|
|
334
|
+
durationMs,
|
|
335
|
+
sessionId: session.sessionId,
|
|
336
|
+
transcriptPath: session.transcriptPath,
|
|
337
|
+
cwd: session.cwd,
|
|
338
|
+
permissionMode: session.permissionMode,
|
|
339
|
+
hookEventName: session.hookEventName,
|
|
340
|
+
};
|
|
341
|
+
try {
|
|
342
|
+
persistHookActivity(activityEntry);
|
|
315
343
|
} catch {
|
|
316
|
-
|
|
344
|
+
hookLogWarn("activity persistence failed");
|
|
317
345
|
}
|
|
318
|
-
}
|
|
319
346
|
|
|
320
|
-
|
|
347
|
+
// Fire PostHog telemetry for decisions that affect Claude's behavior
|
|
348
|
+
if (result.decision === "deny" || result.decision === "instruct") {
|
|
349
|
+
try {
|
|
350
|
+
const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
|
|
351
|
+
const isConventionPolicy = result.policyName?.startsWith(".failproofai-") ?? false;
|
|
352
|
+
const conventionScope = isConventionPolicy
|
|
353
|
+
? result.policyName!.match(/^\.failproofai-(project|user)\//)?.[1] ?? null
|
|
354
|
+
: null;
|
|
355
|
+
const hasCustomParams =
|
|
356
|
+
!isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
|
|
357
|
+
const paramKeysOverridden = hasCustomParams
|
|
358
|
+
? Object.keys(config.policyParams![result.policyName!])
|
|
359
|
+
: [];
|
|
360
|
+
const distinctId = getInstanceId();
|
|
361
|
+
await trackHookEvent(distinctId, "hook_policy_triggered", {
|
|
362
|
+
event_type: canonicalEventType,
|
|
363
|
+
cli,
|
|
364
|
+
tool_name: (parsed.tool_name as string) ?? null,
|
|
365
|
+
policy_name: result.policyName,
|
|
366
|
+
decision: result.decision,
|
|
367
|
+
is_custom_hook: isCustomHook,
|
|
368
|
+
is_convention_policy: isConventionPolicy,
|
|
369
|
+
convention_scope: conventionScope,
|
|
370
|
+
has_custom_params: hasCustomParams,
|
|
371
|
+
param_keys_overridden: paramKeysOverridden,
|
|
372
|
+
});
|
|
373
|
+
} catch {
|
|
374
|
+
// Telemetry is best-effort — never block the hook
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return result.exitCode;
|
|
378
|
+
} finally {
|
|
379
|
+
// Await any un-awaited (`void trackHookEvent(...)`) events fired during
|
|
380
|
+
// this invocation. bin/failproofai.mjs calls process.exit() the moment we
|
|
381
|
+
// return OR throw, which would otherwise drop in-flight POSTs — notably on
|
|
382
|
+
// the allow path (no trailing awaited event) and on any early throw
|
|
383
|
+
// (custom-hook load / policy eval) before the happy path is reached.
|
|
384
|
+
await flushHookTelemetry();
|
|
385
|
+
}
|
|
321
386
|
}
|
|
@@ -11,13 +11,23 @@ import { POSTHOG_API_KEY, POSTHOG_PRODUCT } from "../posthog-key";
|
|
|
11
11
|
const API_KEY = POSTHOG_API_KEY;
|
|
12
12
|
const CAPTURE_URL = "https://us.i.posthog.com/capture/";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* In-flight telemetry POSTs. The hook binary is short-lived — `bin/failproofai.mjs`
|
|
16
|
+
* calls `process.exit()` the moment `handleHookEvent` returns, which kills any
|
|
17
|
+
* fetch that a caller fired with `void trackHookEvent(...)` (i.e. did not await).
|
|
18
|
+
* We track every send here so `flushHookTelemetry()` can await them all at the
|
|
19
|
+
* process-exit boundary, making delivery reliable regardless of whether the
|
|
20
|
+
* individual call site awaited. Without this, un-awaited events (custom_hooks_loaded,
|
|
21
|
+
* convention_policies_loaded, the *_error events) are dropped on the common
|
|
22
|
+
* allow path, since no trailing await holds the event loop open.
|
|
23
|
+
*/
|
|
24
|
+
const pending = new Set<Promise<void>>();
|
|
25
|
+
|
|
26
|
+
async function sendEvent(
|
|
15
27
|
distinctId: string,
|
|
16
28
|
event: string,
|
|
17
29
|
properties?: Record<string, unknown>,
|
|
18
30
|
): Promise<void> {
|
|
19
|
-
if (process.env.FAILPROOFAI_TELEMETRY_DISABLED === "1") return;
|
|
20
|
-
|
|
21
31
|
const body = JSON.stringify({
|
|
22
32
|
api_key: process.env.FAILPROOFAI_POSTHOG_KEY ?? API_KEY,
|
|
23
33
|
event,
|
|
@@ -41,3 +51,33 @@ export async function trackHookEvent(
|
|
|
41
51
|
// Telemetry is best-effort — never fail the hook
|
|
42
52
|
}
|
|
43
53
|
}
|
|
54
|
+
|
|
55
|
+
export async function trackHookEvent(
|
|
56
|
+
distinctId: string,
|
|
57
|
+
event: string,
|
|
58
|
+
properties?: Record<string, unknown>,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
if (process.env.FAILPROOFAI_TELEMETRY_DISABLED === "1") return;
|
|
61
|
+
|
|
62
|
+
const p = sendEvent(distinctId, event, properties);
|
|
63
|
+
pending.add(p);
|
|
64
|
+
// Deregister on settle so the set doesn't grow unbounded in long-lived
|
|
65
|
+
// callers (tests, the dashboard server). `sendEvent` never rejects, but
|
|
66
|
+
// `.finally` is defensive.
|
|
67
|
+
void p.finally(() => pending.delete(p));
|
|
68
|
+
await p;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Await every in-flight telemetry POST. Call this immediately before
|
|
73
|
+
* `process.exit()` on any short-lived path so `void trackHookEvent(...)` events
|
|
74
|
+
* are delivered instead of being cut off by the exit. No-op (resolves instantly)
|
|
75
|
+
* when nothing is pending. Never throws — `sendEvent` swallows its own errors.
|
|
76
|
+
*/
|
|
77
|
+
export async function flushHookTelemetry(): Promise<void> {
|
|
78
|
+
// Loop in case a settling promise's `.finally` races a newly-added send;
|
|
79
|
+
// in practice one pass suffices because no send spawns another.
|
|
80
|
+
while (pending.size > 0) {
|
|
81
|
+
await Promise.allSettled([...pending]);
|
|
82
|
+
}
|
|
83
|
+
}
|