failproofai 1.0.7-beta.1 → 1.0.7
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/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- 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 +4 -4
- 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 +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- 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 +4 -4
- 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/_not-found/__PAGE__.segment.rsc +14 -14
- 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 +7 -8
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/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/status/route.js.nft.json +1 -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 +5 -7
- 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 +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- 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 +6 -6
- 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 +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +11 -13
- 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 +7 -8
- 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 +7 -7
- 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 +6 -7
- 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/app/settings/page/server-reference-manifest.json +8 -41
- package/.next/standalone/.next/server/app/settings/page.js +9 -12
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__1_bftcl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__19_pzeq._.js → [externals]__1msfs-h._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1bf34x4._.js → [root-of-the-server]__1_r2rbg._.js} +7 -5
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1c3k-8x._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
- 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/src_hooks_0iu54mz._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yxwl6j._.js → [root-of-the-server]__0bd3mje._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__0cxe_2_._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1w_5l7t._.js → _0h_douw._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_214wgrp._.js → _1-i_gzc._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0bn2oo8._.js → _166t73i._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1q46vxx._.js → _1_qswah._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifp._.js → _1es2j7i._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_18_d8l1._.js → node_modules_next_dist_0drixxt._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.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 +23 -56
- package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +1 -0
- package/.next/standalone/.next/static/chunks/0__8a7m868fvf.js +1 -0
- package/.next/standalone/.next/static/chunks/{12tvm75t5ffui.js → 0o6qlkgubtoex.js} +1 -1
- package/.next/standalone/.next/static/chunks/13i7-9is-vhys.js +1 -0
- package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +6 -0
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
- package/.next/standalone/.next/static/chunks/{0vmd180qfntfb.js → 2klitrtzpaoe0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{258668t68du6b.js → 2mdh397ghgnvv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +2 -0
- package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +1 -0
- package/.next/standalone/.next/static/chunks/{0qrbdkv9qmvli.js → 3rh5o7e16irrm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{29fql3nbnfc9q.js → 43ufqrz8qo3h-.js} +1 -1
- package/.next/standalone/SECURITY.md +53 -0
- package/.next/standalone/app/actions/pack-actions.ts +0 -12
- package/.next/standalone/app/policies/hooks-client.tsx +0 -9
- package/.next/standalone/app/settings/page.tsx +1 -20
- package/.next/standalone/app/settings/settings-client.tsx +1 -27
- package/.next/standalone/app/settings/settings.css +0 -79
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/sdk/python/skill/SKILL.md +60 -14
- package/.next/standalone/sdk/python/skill/agents/openai.yaml +2 -1
- package/.next/standalone/sdk/python/skill/references/evaluator.md +255 -0
- package/.next/standalone/sdk/python/skill/references/events.md +17 -8
- package/.next/standalone/sdk/python/skill/references/frameworks.md +3 -0
- package/.next/standalone/sdk/python/skill/references/install.md +3 -0
- package/.next/standalone/sdk/python/skill/references/integration.md +6 -2
- package/.next/standalone/sdk/python/skill/references/typescript.md +568 -0
- package/.next/standalone/sdk/typescript/CHANGELOG.md +133 -0
- package/.next/standalone/sdk/typescript/LICENSE +42 -0
- package/.next/standalone/sdk/typescript/README.md +552 -0
- package/.next/standalone/sdk/typescript/eslint.config.mjs +59 -0
- package/.next/standalone/sdk/typescript/examples/research-agent.ts +197 -0
- package/.next/standalone/sdk/typescript/integration/ai.test.ts +920 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/agent.ts +337 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +261 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +16 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/surfaces.ts +605 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/agent.ts +342 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +156 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +16 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/surfaces.ts +628 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/agent.ts +346 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +156 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +13 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/surfaces.ts +651 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/agent.ts +350 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +153 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +13 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/surfaces.ts +651 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/agent.ts +623 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +344 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent-v1.ts +99 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent.ts +623 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +336 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/agent.ts +96 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +441 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.cjs +53 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.mjs +55 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/package.json +18 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/agent.ts +659 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +635 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/agent.ts +659 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +553 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/agent.ts +877 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/mcp-server.mjs +66 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +6797 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +24 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/agent.ts +872 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/mcp-server.mjs +66 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +2540 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/actions.ts +18 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/ai/route.ts +42 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/edge/route.ts +29 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/langgraph/route.ts +21 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/llamaindex/route.ts +11 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/mastra/route.ts +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/status/route.ts +7 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/layout.tsx +9 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/page.tsx +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/instrumentation.ts +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/ai.ts +61 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/langgraph.ts +68 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/llamaindex.ts +98 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/mastra.ts +90 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/next.config.ts +52 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +4343 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +28 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/agent.ts +51 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/deno-npm.ts +76 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +484 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/agent.ts +79 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/package-lock.json +740 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/package.json +11 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/agent.ts +197 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package-lock.json +70 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/global-setup.ts +29 -0
- package/.next/standalone/sdk/typescript/integration/harness.ts +451 -0
- package/.next/standalone/sdk/typescript/integration/langchain.test.ts +682 -0
- package/.next/standalone/sdk/typescript/integration/llamaindex.test.ts +709 -0
- package/.next/standalone/sdk/typescript/integration/mastra-coverage.test.ts +386 -0
- package/.next/standalone/sdk/typescript/integration/mastra.test.ts +311 -0
- package/.next/standalone/sdk/typescript/integration/nextjs.test.ts +341 -0
- package/.next/standalone/sdk/typescript/integration/runtime-parity.ts +180 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.bun.test.ts +15 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.core.test.ts +113 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.deno.test.ts +19 -0
- package/.next/standalone/sdk/typescript/integration/types.test.ts +141 -0
- package/.next/standalone/sdk/typescript/integration/vanilla.test.ts +255 -0
- package/.next/standalone/sdk/typescript/package-lock.json +2640 -0
- package/.next/standalone/sdk/typescript/package.json +401 -0
- package/.next/standalone/sdk/typescript/scripts/finalize-build.mjs +123 -0
- package/.next/standalone/sdk/typescript/scripts/release.mjs +177 -0
- package/.next/standalone/sdk/typescript/src/clock.ts +58 -0
- package/.next/standalone/sdk/typescript/src/context.ts +214 -0
- package/.next/standalone/sdk/typescript/src/edge/adapter.ts +18 -0
- package/.next/standalone/sdk/typescript/src/edge/ai.ts +116 -0
- package/.next/standalone/sdk/typescript/src/edge/index.ts +238 -0
- package/.next/standalone/sdk/typescript/src/edge/langchain.ts +18 -0
- package/.next/standalone/sdk/typescript/src/edge/llamaindex.ts +12 -0
- package/.next/standalone/sdk/typescript/src/edge/mastra.ts +17 -0
- package/.next/standalone/sdk/typescript/src/edge/notice.ts +33 -0
- package/.next/standalone/sdk/typescript/src/environment.ts +75 -0
- package/.next/standalone/sdk/typescript/src/evaluator/authoring.ts +480 -0
- package/.next/standalone/sdk/typescript/src/evaluator/cli.ts +96 -0
- package/.next/standalone/sdk/typescript/src/evaluator/client.ts +421 -0
- package/.next/standalone/sdk/typescript/src/evaluator/expression.ts +1292 -0
- package/.next/standalone/sdk/typescript/src/evaluator/index.ts +144 -0
- package/.next/standalone/sdk/typescript/src/evaluator/protocol.ts +747 -0
- package/.next/standalone/sdk/typescript/src/evaluator/runtime.ts +930 -0
- package/.next/standalone/sdk/typescript/src/evaluator/sandbox-worker.ts +171 -0
- package/.next/standalone/sdk/typescript/src/evaluator/source-limits.ts +27 -0
- package/.next/standalone/sdk/typescript/src/evaluator/source.ts +509 -0
- package/.next/standalone/sdk/typescript/src/events.ts +879 -0
- package/.next/standalone/sdk/typescript/src/exit.ts +117 -0
- package/.next/standalone/sdk/typescript/src/index.ts +186 -0
- package/.next/standalone/sdk/typescript/src/integrations/ai.ts +1566 -0
- package/.next/standalone/sdk/typescript/src/integrations/compat.ts +322 -0
- package/.next/standalone/sdk/typescript/src/integrations/core.ts +1321 -0
- package/.next/standalone/sdk/typescript/src/integrations/index.ts +355 -0
- package/.next/standalone/sdk/typescript/src/integrations/langchain.ts +2340 -0
- package/.next/standalone/sdk/typescript/src/integrations/llamaindex.ts +2111 -0
- package/.next/standalone/sdk/typescript/src/integrations/mastra.ts +1802 -0
- package/.next/standalone/sdk/typescript/src/logger.ts +98 -0
- package/.next/standalone/sdk/typescript/src/next.ts +115 -0
- package/.next/standalone/sdk/typescript/src/node-require.ts +446 -0
- package/.next/standalone/sdk/typescript/src/redact.ts +305 -0
- package/.next/standalone/sdk/typescript/src/resolver.ts +120 -0
- package/.next/standalone/sdk/typescript/src/runtime.ts +29 -0
- package/.next/standalone/sdk/typescript/src/schema.ts +410 -0
- package/.next/standalone/sdk/typescript/src/scopes.ts +701 -0
- package/.next/standalone/sdk/typescript/src/shared.ts +33 -0
- package/.next/standalone/sdk/typescript/src/version.ts +5 -0
- package/.next/standalone/sdk/typescript/src/writer.ts +934 -0
- package/.next/standalone/sdk/typescript/test/adapters.test.ts +397 -0
- package/.next/standalone/sdk/typescript/test/ai.test.ts +1076 -0
- package/.next/standalone/sdk/typescript/test/copies.test.ts +204 -0
- package/.next/standalone/sdk/typescript/test/edge.test.ts +183 -0
- package/.next/standalone/sdk/typescript/test/evaluator-client.test.ts +234 -0
- package/.next/standalone/sdk/typescript/test/evaluator-protocol.test.ts +225 -0
- package/.next/standalone/sdk/typescript/test/events.test.ts +193 -0
- package/.next/standalone/sdk/typescript/test/expression.test.ts +181 -0
- package/.next/standalone/sdk/typescript/test/global-setup.ts +26 -0
- package/.next/standalone/sdk/typescript/test/helpers.ts +130 -0
- package/.next/standalone/sdk/typescript/test/integrations.test.ts +369 -0
- package/.next/standalone/sdk/typescript/test/langchain-copies.test.ts +204 -0
- package/.next/standalone/sdk/typescript/test/langchain.test.ts +999 -0
- package/.next/standalone/sdk/typescript/test/llamaindex.test.ts +1760 -0
- package/.next/standalone/sdk/typescript/test/mastra-coverage.test.ts +501 -0
- package/.next/standalone/sdk/typescript/test/mastra-lifecycle.test.ts +479 -0
- package/.next/standalone/sdk/typescript/test/mastra.test.ts +285 -0
- package/.next/standalone/sdk/typescript/test/next.test.ts +109 -0
- package/.next/standalone/sdk/typescript/test/packaging.test.ts +312 -0
- package/.next/standalone/sdk/typescript/test/redaction.test.ts +171 -0
- package/.next/standalone/sdk/typescript/test/runtimes.test.ts +101 -0
- package/.next/standalone/sdk/typescript/test/sandbox.test.ts +189 -0
- package/.next/standalone/sdk/typescript/test/scopes.test.ts +271 -0
- package/.next/standalone/sdk/typescript/test/setup.ts +19 -0
- package/.next/standalone/sdk/typescript/test/skill-snippets.test.ts +73 -0
- package/.next/standalone/sdk/typescript/test/spool-contract.test.ts +124 -0
- package/.next/standalone/sdk/typescript/test/tracker-bounds.test.ts +191 -0
- package/.next/standalone/sdk/typescript/test/wire-format.test.ts +214 -0
- package/.next/standalone/sdk/typescript/test/writer.test.ts +407 -0
- package/.next/standalone/sdk/typescript/tsconfig.build.json +15 -0
- package/.next/standalone/sdk/typescript/tsconfig.cjs.json +19 -0
- package/.next/standalone/sdk/typescript/tsconfig.json +28 -0
- package/.next/standalone/sdk/typescript/vitest.config.ts +33 -0
- package/.next/standalone/sdk/typescript/vitest.integration.config.ts +23 -0
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +2 -115
- package/dist/cli.mjs +6538 -13686
- package/dist/index.js +1 -19
- package/dist/worker.mjs +2085 -8012
- package/package.json +9 -9
- package/pi-extension/index.ts +0 -11
- package/scripts/build-policy-pack.mjs +2 -39
- package/src/hooks/builtin-policies.ts +3 -21
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-managed-policies.ts +0 -22
- package/src/hooks/custom-hooks-loader.ts +7 -45
- package/src/hooks/custom-hooks-registry.ts +1 -45
- package/src/hooks/first-run-gate.ts +0 -5
- package/src/hooks/fp-home.ts +0 -23
- package/src/hooks/handler.ts +6 -265
- package/src/hooks/hook-activity-store.ts +1 -105
- package/src/hooks/hook-telemetry.ts +0 -41
- package/src/hooks/loader-utils.ts +0 -6
- package/src/hooks/pack-cli.ts +17 -278
- package/src/hooks/pack-manifest.ts +7 -479
- package/src/hooks/pack-store.ts +10 -156
- package/src/hooks/policy-catalog.ts +0 -65
- package/src/hooks/policy-evaluator.ts +796 -940
- package/src/hooks/policy-registry.ts +0 -25
- package/src/hooks/policy-types.ts +0 -126
- package/src/hooks/worker-server.ts +26 -119
- package/src/index.ts +0 -6
- package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
- package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0w6mzq5._.js +0 -151
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
- package/.next/standalone/.next/static/chunks/0cd-_8-c-m1ea.js +0 -6
- package/.next/standalone/.next/static/chunks/0uldbut9y2-e8.js +0 -1
- package/.next/standalone/.next/static/chunks/1u5zsejmgrir_.js +0 -1
- package/.next/standalone/.next/static/chunks/285spx855h_3r.css +0 -2
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +0 -1
- package/.next/standalone/.next/static/chunks/2vkvu9-opa_1z.js +0 -1
- package/.next/standalone/.next/static/chunks/37lhv7wa3ywt6.js +0 -1
- package/.next/standalone/app/actions/get-jev-config.ts +0 -409
- package/.next/standalone/app/actions/update-jev-config.ts +0 -420
- package/.next/standalone/app/components/jev-notices.tsx +0 -96
- package/.next/standalone/app/settings/jev-panel.tsx +0 -469
- package/src/hooks/effective-reviewers.ts +0 -79
- package/src/hooks/jev-activity.ts +0 -385
- package/src/hooks/jev-cli.ts +0 -1193
- package/src/hooks/policy-authority.ts +0 -333
- package/src/hooks/policy-reviewability.ts +0 -229
- package/src/hooks/semantic/combine.ts +0 -541
- package/src/hooks/semantic/compile.ts +0 -176
- package/src/hooks/semantic/decide.ts +0 -392
- package/src/hooks/semantic/envelope.ts +0 -1296
- package/src/hooks/semantic/evaluator.ts +0 -547
- package/src/hooks/semantic/facts.ts +0 -292
- package/src/hooks/semantic/intent.ts +0 -1190
- package/src/hooks/semantic/jev-client.ts +0 -643
- package/src/hooks/semantic/jev-config.ts +0 -594
- package/src/hooks/semantic/jev-review.ts +0 -374
- package/src/hooks/semantic/jev-stats.ts +0 -289
- package/src/hooks/semantic/jev-throttle.ts +0 -421
- package/src/hooks/semantic/pack-policies.ts +0 -251
- package/src/hooks/semantic/policies.ts +0 -596
- package/src/hooks/semantic/precondition-names.ts +0 -60
- package/src/hooks/semantic/preconditions.ts +0 -58
- package/src/hooks/semantic/redact.ts +0 -2910
- package/src/hooks/semantic/types.ts +0 -145
- package/src/hooks/semver-precedence.ts +0 -128
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_ssgManifest.js +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Returns exit code, stdout, and stderr for the hook handler.
|
|
4
4
|
*/
|
|
5
5
|
import type { HookEventType, SessionMetadata } from "./types";
|
|
6
|
-
import type { PolicyContext, HooksConfig
|
|
6
|
+
import type { PolicyContext, HooksConfig } from "./policy-types";
|
|
7
7
|
// Read from the layer that owns it rather than restated here — restating a
|
|
8
8
|
// pack id is exactly how `failproofai/builtins` and `failproofai/core` both
|
|
9
9
|
// outlived their rename. This is on the hook path, so the cost was measured
|
|
@@ -15,13 +15,6 @@ import { CORE_SOURCE } from "./pack-store";
|
|
|
15
15
|
import { packPolicyParamKey, parsePackPolicyName } from "./pack-param-key";
|
|
16
16
|
export { packPolicyParamKey } from "./pack-param-key";
|
|
17
17
|
import { DEFAULT_POLICY_NAMESPACE, getPoliciesForEvent } from "./policy-registry";
|
|
18
|
-
// Type-only apart from the two pure functions: the semantic evaluator itself is
|
|
19
|
-
// loaded by handler.ts, lazily, and only when a Jev config exists.
|
|
20
|
-
import { combineTwoTier, regexOnly, type FinalVerdict, type JevActivityFields, type JevReview, type RegexVerdict, type TwoTierReview } from "./semantic/combine";
|
|
21
|
-
// The code, not a copy of the string: the log line below turns on it, and a
|
|
22
|
-
// rename should break the build here rather than quietly print "unavailable"
|
|
23
|
-
// for a call Jev actually answered.
|
|
24
|
-
import { JEV_REASON_REQUEST_CUT } from "./jev-activity";
|
|
25
18
|
import { hookLogInfo, hookLogWarn } from "./hook-logger";
|
|
26
19
|
import { trackHookEvent } from "./hook-telemetry";
|
|
27
20
|
import { getInstanceId } from "../../lib/telemetry-id";
|
|
@@ -42,16 +35,6 @@ export interface EvaluationResult {
|
|
|
42
35
|
policyNames?: string[];
|
|
43
36
|
reason: string | null;
|
|
44
37
|
decision: "allow" | "deny" | "instruct";
|
|
45
|
-
/**
|
|
46
|
-
* Present only when the two-tier (Jev) path ran — never on the regex-only
|
|
47
|
-
* path, so an unconfigured machine's results carry no extra key.
|
|
48
|
-
*/
|
|
49
|
-
twoTier?: {
|
|
50
|
-
/** The hook-activity fields to persist for this call. */
|
|
51
|
-
activity: JevActivityFields;
|
|
52
|
-
/** True when Jev's own verdict is what `policyName` names. */
|
|
53
|
-
decidedByJev: boolean;
|
|
54
|
-
};
|
|
55
38
|
}
|
|
56
39
|
|
|
57
40
|
|
|
@@ -142,117 +125,22 @@ function getConfigParamsFor(
|
|
|
142
125
|
return config.policyParams[canonicalName.slice(defaultPrefix.length)];
|
|
143
126
|
}
|
|
144
127
|
|
|
145
|
-
const HARD: { authority: "hard"; reviewedBy: string[] } = { authority: "hard", reviewedBy: [] };
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Evaluate every policy registered for this event and turn the outcome into
|
|
149
|
-
* the calling CLI's response.
|
|
150
|
-
*
|
|
151
|
-
* Three stages, so the two engines can meet in the middle:
|
|
152
|
-
*
|
|
153
|
-
* 1. **collect** runs the regex policies in priority order and records each
|
|
154
|
-
* verdict. A HARD deny short-circuits exactly as the evaluator always has.
|
|
155
|
-
* Without `twoTier` every deny is hard, so this is the old loop.
|
|
156
|
-
* 2. **combine** decides. Without `twoTier` that is `regexOnly` — first deny,
|
|
157
|
-
* else every instruct, else every note — which is the old evaluator's
|
|
158
|
-
* answer. With it, `combineTwoTier` applies the §4 rules against Jev's
|
|
159
|
-
* review (see `semantic/combine.ts`).
|
|
160
|
-
* 3. **format** turns the verdict into the per-CLI response shape. It is the
|
|
161
|
-
* single source of truth for those shapes and does not know which engine
|
|
162
|
-
* decided.
|
|
163
|
-
*
|
|
164
|
-
* `twoTier` is passed by the handler only when a Jev config (BYOK) exists and
|
|
165
|
-
* the event is a gate; the Jev request is already in flight by then.
|
|
166
|
-
*/
|
|
167
128
|
export async function evaluatePolicies(
|
|
168
129
|
eventType: HookEventType,
|
|
169
130
|
payload: Record<string, unknown>,
|
|
170
131
|
session?: SessionMetadata,
|
|
171
132
|
config?: HooksConfig,
|
|
172
|
-
twoTier?: TwoTierReview,
|
|
173
133
|
): Promise<EvaluationResult> {
|
|
174
134
|
const toolName = payload.tool_name as string | undefined;
|
|
135
|
+
const toolInput = payload.tool_input as Record<string, unknown> | undefined;
|
|
175
136
|
|
|
176
137
|
const policies = getPoliciesForEvent(eventType, toolName);
|
|
177
138
|
|
|
178
139
|
hookLogInfo(`evaluating ${policies.length} policies for ${eventType}`);
|
|
179
140
|
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
|
|
183
|
-
}
|
|
184
|
-
const { verdicts } = await collectVerdicts(eventType, payload, session, config, policies);
|
|
185
|
-
return formatVerdict(eventType, session, toolName, regexOnly(verdicts));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
let collected: Awaited<ReturnType<typeof collectVerdicts>>;
|
|
189
|
-
try {
|
|
190
|
-
collected = await collectVerdicts(eventType, payload, session, config, policies, twoTier);
|
|
191
|
-
} catch (err) {
|
|
192
|
-
twoTier.abort();
|
|
193
|
-
throw err;
|
|
194
|
-
}
|
|
195
|
-
let review: JevReview;
|
|
196
|
-
if (collected.hardDenied) {
|
|
197
|
-
// Final whatever Jev says, so stop paying for the answer.
|
|
198
|
-
twoTier.abort();
|
|
199
|
-
review = { kind: "not-consulted" };
|
|
200
|
-
} else {
|
|
201
|
-
// Nothing from here on reads the policy registry (the verdicts, their
|
|
202
|
-
// authority and every policy name are already captured), so the warm
|
|
203
|
-
// worker may start the next queued hook while this one waits on Jev.
|
|
204
|
-
twoTier.releaseRegistry?.();
|
|
205
|
-
review = await twoTier.review;
|
|
141
|
+
if (policies.length === 0) {
|
|
142
|
+
return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
|
|
206
143
|
}
|
|
207
|
-
const combined = combineTwoTier(collected.verdicts, review, twoTier.mode);
|
|
208
|
-
if (combined.cleared.length > 0) {
|
|
209
|
-
hookLogInfo(`jev ${twoTier.mode === "shadow" ? "would clear" : "cleared"}: ${combined.cleared.join(", ")}`);
|
|
210
|
-
}
|
|
211
|
-
if (combined.activity.evaluator === "jev-fallback") {
|
|
212
|
-
// Info, not warn: during an outage this is every gated call, and warn-level
|
|
213
|
-
// lines reach the hook's stderr — which is the deny text itself on some
|
|
214
|
-
// CLIs. The activity row (`evaluator: "jev-fallback"` + reason) and
|
|
215
|
-
// `failproofai jev status` are where a fallback is made visible.
|
|
216
|
-
//
|
|
217
|
-
// `request-cut` is the one reason here that is not an outage: Jev answered,
|
|
218
|
-
// its answer still counted toward the most-severe rule, it withdrew its
|
|
219
|
-
// clears, and it refused to let a call nobody could read in full come out
|
|
220
|
-
// as an allow (see `semantic/combine.ts`). Saying "unavailable" for it
|
|
221
|
-
// would send someone looking for a provider problem that is not there.
|
|
222
|
-
//
|
|
223
|
-
// `truncated` — the name a build before the rename wrote for that same
|
|
224
|
-
// reason — had a branch of its own here, and it was unreachable: this line
|
|
225
|
-
// only ever describes a row THIS process just produced, and `combine.ts`
|
|
226
|
-
// writes `JEV_REASON_REQUEST_CUT`. The readers that do meet old rows keep
|
|
227
|
-
// the old name (`ANSWERED_BUT_NOT_APPLIED_REASONS` and the code list in
|
|
228
|
-
// `jev-activity.ts`); a log line about a live call does not need it.
|
|
229
|
-
hookLogInfo(
|
|
230
|
-
combined.activity.jevFallbackReason === JEV_REASON_REQUEST_CUT
|
|
231
|
-
? "jev could not be shown the whole call; it cleared nothing, and the call was not allowed on a partial read"
|
|
232
|
-
: `jev unavailable (${combined.activity.jevFallbackReason}); the regex result decided this call`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
return {
|
|
236
|
-
...formatVerdict(eventType, session, toolName, combined.final),
|
|
237
|
-
twoTier: { activity: combined.activity, decidedByJev: combined.decidedByJev },
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Run the policies in order and record each verdict. Stops at the first HARD
|
|
243
|
-
* deny; a reviewable deny (two-tier only) is recorded and evaluation goes on,
|
|
244
|
-
* because Jev may yet clear it and a later hard deny must still be found.
|
|
245
|
-
*/
|
|
246
|
-
async function collectVerdicts(
|
|
247
|
-
eventType: HookEventType,
|
|
248
|
-
payload: Record<string, unknown>,
|
|
249
|
-
session: SessionMetadata | undefined,
|
|
250
|
-
config: HooksConfig | undefined,
|
|
251
|
-
policies: RegisteredPolicy[],
|
|
252
|
-
twoTier?: TwoTierReview,
|
|
253
|
-
): Promise<{ verdicts: RegexVerdict[]; hardDenied: boolean }> {
|
|
254
|
-
const toolName = payload.tool_name as string | undefined;
|
|
255
|
-
const toolInput = payload.tool_input as Record<string, unknown> | undefined;
|
|
256
144
|
|
|
257
145
|
const baseCtx: PolicyContext = {
|
|
258
146
|
eventType,
|
|
@@ -263,7 +151,11 @@ async function collectVerdicts(
|
|
|
263
151
|
cli: session?.cli,
|
|
264
152
|
};
|
|
265
153
|
|
|
266
|
-
|
|
154
|
+
// Track all instruct results (accumulated, does not short-circuit)
|
|
155
|
+
const instructEntries: Array<{ policyName: string; reason: string }> = [];
|
|
156
|
+
|
|
157
|
+
// Track informational messages from allow decisions (with policy attribution)
|
|
158
|
+
const allowEntries: Array<{ policyName: string; reason: string }> = [];
|
|
267
159
|
|
|
268
160
|
for (const policy of policies) {
|
|
269
161
|
// Inject params: merge policyParams[policy.name] over schema defaults.
|
|
@@ -324,534 +216,513 @@ async function collectVerdicts(
|
|
|
324
216
|
getConfigParamsFor(config, policy.name)?.hint,
|
|
325
217
|
);
|
|
326
218
|
hookLogInfo(`deny by "${policy.name}": ${reason}`);
|
|
327
|
-
const { authority, reviewedBy } = twoTier ? twoTier.authorityOf(policy) : HARD;
|
|
328
|
-
verdicts.push({ policyName: policy.name, decision: "deny", reason, authority, reviewedBy });
|
|
329
|
-
if (authority === "hard") return { verdicts, hardDenied: true };
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
219
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
220
|
+
// Pick a noun for the deny message that fits the event type. Tool events
|
|
221
|
+
// get the tool name; non-tool events (UserPromptSubmit, SessionStart,
|
|
222
|
+
// SessionEnd, Stop, …) use an event-appropriate label so we don't emit
|
|
223
|
+
// the misleading "Blocked unknown tool by failproofai because: ...".
|
|
224
|
+
let displayTool: string;
|
|
225
|
+
if (ctx.toolName) {
|
|
226
|
+
displayTool = ctx.toolName;
|
|
227
|
+
} else if (eventType === "UserPromptSubmit") {
|
|
228
|
+
displayTool = "prompt";
|
|
229
|
+
} else if (eventType === "SessionStart") {
|
|
230
|
+
displayTool = "session start";
|
|
231
|
+
} else if (eventType === "SessionEnd") {
|
|
232
|
+
displayTool = "session end";
|
|
233
|
+
} else if (eventType === "Stop") {
|
|
234
|
+
displayTool = "stop";
|
|
235
|
+
} else {
|
|
236
|
+
displayTool = "operation";
|
|
237
|
+
}
|
|
238
|
+
const blockedMessage = `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`;
|
|
343
239
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
240
|
+
// Cursor's hook protocol expects a flat `{permission, user_message,
|
|
241
|
+
// agent_message}` shape for any blocking decision, regardless of which
|
|
242
|
+
// event triggered it. Branch ahead of the per-event handlers below so
|
|
243
|
+
// PreToolUse / PostToolUse / PermissionRequest all flow through the
|
|
244
|
+
// Cursor-shaped response.
|
|
245
|
+
// Ref: https://cursor.com/docs/hooks (Stdout Response Format).
|
|
246
|
+
if (session?.cli === "cursor") {
|
|
247
|
+
// Cursor's `stop` / `subagentStop` hooks ignore `{permission: "deny"}`
|
|
248
|
+
// — that shape is only honored on tool events. The only force-retry
|
|
249
|
+
// channel for Stop/SubagentStop is `{followup_message}` on stdout
|
|
250
|
+
// (exit 0); Cursor auto-submits the text as the next user message
|
|
251
|
+
// (capped at `loop_limit`, default 5). Mirrors the Copilot Stop branch.
|
|
252
|
+
// Without this branch, the 5 `require-*-before-stop` builtins were
|
|
253
|
+
// observation-only on Cursor — the deny was logged but the agent
|
|
254
|
+
// stopped cleanly. Ref: https://cursor.com/docs/hooks
|
|
255
|
+
if (eventType === "Stop" || eventType === "SubagentStop") {
|
|
256
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
|
|
257
|
+
return {
|
|
258
|
+
exitCode: 0,
|
|
259
|
+
stdout: JSON.stringify({ followup_message: reasonText }),
|
|
260
|
+
stderr: "",
|
|
261
|
+
policyName: policy.name,
|
|
262
|
+
reason,
|
|
263
|
+
decision: "deny",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// `beforeSubmitPrompt` does NOT read `permission`. Its only block key
|
|
267
|
+
// is `continue: false` (+ an optional `user_message` shown to the
|
|
268
|
+
// user); an object carrying unknown keys validates and is dropped, so
|
|
269
|
+
// the flat deny below was inert and every prompt went through. Verified
|
|
270
|
+
// against cursor-agent 2026.07.16-899851b, 1931.index.js char 887883.
|
|
271
|
+
if (eventType === "UserPromptSubmit") {
|
|
272
|
+
return {
|
|
273
|
+
exitCode: 0,
|
|
274
|
+
stdout: JSON.stringify({ continue: false, user_message: blockedMessage }),
|
|
275
|
+
stderr: "",
|
|
276
|
+
policyName: policy.name,
|
|
277
|
+
reason,
|
|
278
|
+
decision: "deny",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const response = {
|
|
282
|
+
permission: "deny",
|
|
283
|
+
user_message: blockedMessage,
|
|
284
|
+
agent_message: blockedMessage,
|
|
285
|
+
};
|
|
286
|
+
return {
|
|
287
|
+
exitCode: 0,
|
|
288
|
+
stdout: JSON.stringify(response),
|
|
289
|
+
stderr: "",
|
|
290
|
+
policyName: policy.name,
|
|
291
|
+
reason,
|
|
292
|
+
decision: "deny",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
349
295
|
|
|
350
|
-
|
|
351
|
-
}
|
|
296
|
+
// Pi's shim parses a flat `{permission, reason}` JSON shape from stdout
|
|
297
|
+
// and translates `permission === "deny"` into a `{block: true, reason}`
|
|
298
|
+
// return value from its `pi.on("tool_call", ...)` handler. Pi has no
|
|
299
|
+
// event-specific decision wrappers, so all events flow through the
|
|
300
|
+
// same flat shape — except Stop, where we emit the MANDATORY ACTION
|
|
301
|
+
// wording so the shim can re-inject it as a system-prompt suffix on
|
|
302
|
+
// the next before_agent_start (Pi cannot veto agent_end directly).
|
|
303
|
+
// Mirrors the Cursor/Copilot/OpenCode Stop branches above.
|
|
304
|
+
if (session?.cli === "pi") {
|
|
305
|
+
if (eventType === "Stop") {
|
|
306
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
|
|
307
|
+
return {
|
|
308
|
+
exitCode: 0,
|
|
309
|
+
stdout: JSON.stringify({ permission: "deny", reason: reasonText }),
|
|
310
|
+
stderr: "",
|
|
311
|
+
policyName: policy.name,
|
|
312
|
+
reason,
|
|
313
|
+
decision: "deny",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const response = {
|
|
317
|
+
permission: "deny",
|
|
318
|
+
reason: blockedMessage,
|
|
319
|
+
};
|
|
320
|
+
return {
|
|
321
|
+
exitCode: 0,
|
|
322
|
+
stdout: JSON.stringify(response),
|
|
323
|
+
stderr: "",
|
|
324
|
+
policyName: policy.name,
|
|
325
|
+
reason,
|
|
326
|
+
decision: "deny",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
352
329
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
330
|
+
// Hermes: the block contract is `{"decision":"block","reason"}` on stdout;
|
|
331
|
+
// Hermes IGNORES exit codes, so the JSON is the only channel. A block on
|
|
332
|
+
// PreToolUse stops the tool before it runs, regardless of the originating
|
|
333
|
+
// platform (slack/telegram/cli/cron) or subagent.
|
|
334
|
+
//
|
|
335
|
+
// Only `pre_tool_call` and `pre_verify` are gated in upstream's
|
|
336
|
+
// `_parse_response` (agent/shell_hooks.py:567-621); the shape is still
|
|
337
|
+
// emitted for the other installed events, where it is read by nothing.
|
|
338
|
+
// We do not install `pre_verify` (see the note in types.ts), so Hermes
|
|
339
|
+
// has no canonical Stop event and no Stop branch is needed here.
|
|
340
|
+
if (session?.cli === "hermes") {
|
|
341
|
+
return {
|
|
342
|
+
exitCode: 0,
|
|
343
|
+
stdout: JSON.stringify({ decision: "block", reason: blockedMessage }),
|
|
344
|
+
stderr: "",
|
|
345
|
+
policyName: policy.name,
|
|
346
|
+
reason,
|
|
347
|
+
decision: "deny",
|
|
348
|
+
};
|
|
349
|
+
}
|
|
368
350
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
)
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
351
|
+
// OpenClaw: the shipped openclaw-plugin parses a flat {permission, reason}
|
|
352
|
+
// verdict and maps it per plugin-hook — before_tool_call → {block:true,
|
|
353
|
+
// blockReason}; before_agent_run → {outcome:"block", reason};
|
|
354
|
+
// before_agent_finalize (Stop) → {action:"revise", reason}. For Stop we
|
|
355
|
+
// emit the MANDATORY ACTION wording so the revise loop carries the
|
|
356
|
+
// directive. Observation hooks (after_tool_call / session_* /
|
|
357
|
+
// subagent_ended / before_compaction) ignore stdout, so the flat deny is
|
|
358
|
+
// logged but cannot veto — a documented limitation. Mirrors the Pi branch.
|
|
359
|
+
if (session?.cli === "openclaw") {
|
|
360
|
+
if (eventType === "Stop") {
|
|
361
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
|
|
362
|
+
return {
|
|
363
|
+
exitCode: 0,
|
|
364
|
+
stdout: JSON.stringify({ permission: "deny", reason: reasonText }),
|
|
365
|
+
stderr: "",
|
|
366
|
+
policyName: policy.name,
|
|
367
|
+
reason,
|
|
368
|
+
decision: "deny",
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
exitCode: 0,
|
|
373
|
+
stdout: JSON.stringify({ permission: "deny", reason: blockedMessage }),
|
|
374
|
+
stderr: "",
|
|
375
|
+
policyName: policy.name,
|
|
376
|
+
reason,
|
|
377
|
+
decision: "deny",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
395
380
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// is `continue: false` (+ an optional `user_message` shown to the
|
|
424
|
-
// user); an object carrying unknown keys validates and is dropped, so
|
|
425
|
-
// the flat deny below was inert and every prompt went through. Verified
|
|
426
|
-
// against cursor-agent 2026.07.16-899851b, 1931.index.js char 887883.
|
|
427
|
-
if (eventType === "UserPromptSubmit") {
|
|
428
|
-
return {
|
|
429
|
-
exitCode: 0,
|
|
430
|
-
stdout: JSON.stringify({ continue: false, user_message: blockedMessage }),
|
|
431
|
-
stderr: "",
|
|
432
|
-
policyName: policyName,
|
|
433
|
-
reason,
|
|
434
|
-
decision: "deny",
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
const response = {
|
|
438
|
-
permission: "deny",
|
|
439
|
-
user_message: blockedMessage,
|
|
440
|
-
agent_message: blockedMessage,
|
|
441
|
-
};
|
|
442
|
-
return {
|
|
443
|
-
exitCode: 0,
|
|
444
|
-
stdout: JSON.stringify(response),
|
|
445
|
-
stderr: "",
|
|
446
|
-
policyName: policyName,
|
|
447
|
-
reason,
|
|
448
|
-
decision: "deny",
|
|
449
|
-
};
|
|
450
|
-
}
|
|
381
|
+
// OpenCode: `session.idle` is a notification-only bus event — by the
|
|
382
|
+
// time the plugin handler fires, OpenCode has already gone idle and
|
|
383
|
+
// throwing from the handler does not force-retry. The only working
|
|
384
|
+
// channel is the shim's `client.session.prompt(...)` SDK call, which
|
|
385
|
+
// submits a new user message that re-triggers the agent loop. The
|
|
386
|
+
// shim already routes `hookSpecificOutput.additionalContext` through
|
|
387
|
+
// that path (see buildOpenCodePluginShim's applyDecision), so we emit
|
|
388
|
+
// the deny reason as additionalContext instead of exit-2. Mirrors the
|
|
389
|
+
// Cursor `followup_message` and Copilot `{decision:"block"}` Stop
|
|
390
|
+
// branches. SubagentStop is widened in for forward
|
|
391
|
+
// compat — OpenCode doesn't yet expose subagent boundaries to plugins.
|
|
392
|
+
if (session?.cli === "opencode") {
|
|
393
|
+
if (eventType === "Stop" || eventType === "SubagentStop") {
|
|
394
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
|
|
395
|
+
return {
|
|
396
|
+
exitCode: 0,
|
|
397
|
+
stdout: JSON.stringify({ hookSpecificOutput: { additionalContext: reasonText } }),
|
|
398
|
+
stderr: "",
|
|
399
|
+
policyName: policy.name,
|
|
400
|
+
reason,
|
|
401
|
+
decision: "deny",
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
// Non-Stop opencode events keep the generic Claude shape — the
|
|
405
|
+
// shim's applyDecision already handles permissionDecision: "deny"
|
|
406
|
+
// for tool events.
|
|
407
|
+
}
|
|
451
408
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
reason,
|
|
482
|
-
decision: "deny",
|
|
483
|
-
};
|
|
484
|
-
}
|
|
409
|
+
// Factory droid: droid drives tool blocking off EXIT CODE 2 (it ignores a
|
|
410
|
+
// JSON `{decision:…}` on tool events — verified live against droid
|
|
411
|
+
// v0.171.0: `Hook returned exit code 2, throwing ToolExecutionControlError`).
|
|
412
|
+
// The one exception is `Stop`, where droid does NOT honor exit-2
|
|
413
|
+
// force-retry; there it reads `{decision:"block", reason}` on stdout at
|
|
414
|
+
// exit 0 ("if decision is block, Droid does not stop"). So: Stop → JSON
|
|
415
|
+
// block; every other event (PreToolUse / PostToolUse / UserPromptSubmit /
|
|
416
|
+
// SubagentStop / …) → exit 2 + the blocked message on stderr.
|
|
417
|
+
if (session?.cli === "factory") {
|
|
418
|
+
if (eventType === "Stop") {
|
|
419
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
|
|
420
|
+
return {
|
|
421
|
+
exitCode: 0,
|
|
422
|
+
stdout: JSON.stringify({ decision: "block", reason: reasonText }),
|
|
423
|
+
stderr: "",
|
|
424
|
+
policyName: policy.name,
|
|
425
|
+
reason,
|
|
426
|
+
decision: "deny",
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
exitCode: 2,
|
|
431
|
+
stdout: "",
|
|
432
|
+
stderr: blockedMessage + "\n",
|
|
433
|
+
policyName: policy.name,
|
|
434
|
+
reason,
|
|
435
|
+
decision: "deny",
|
|
436
|
+
};
|
|
437
|
+
}
|
|
485
438
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}
|
|
439
|
+
// Devin CLI: a pure Claude-clone that honors `{decision:"block", reason}`
|
|
440
|
+
// on stdout at exit 0 for EVERY event (verified live against devin
|
|
441
|
+
// v3000.1.27 — the block overrode `--permission-mode dangerous`). On Stop
|
|
442
|
+
// the reason carries the MANDATORY-ACTION force-retry wording; on other
|
|
443
|
+
// events it's the plain blocked message. One branch covers all events.
|
|
444
|
+
if (session?.cli === "devin") {
|
|
445
|
+
const reasonText =
|
|
446
|
+
eventType === "Stop"
|
|
447
|
+
? `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`
|
|
448
|
+
: blockedMessage;
|
|
449
|
+
return {
|
|
450
|
+
exitCode: 0,
|
|
451
|
+
stdout: JSON.stringify({ decision: "block", reason: reasonText }),
|
|
452
|
+
stderr: "",
|
|
453
|
+
policyName: policy.name,
|
|
454
|
+
reason,
|
|
455
|
+
decision: "deny",
|
|
456
|
+
};
|
|
457
|
+
}
|
|
506
458
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
};
|
|
535
|
-
}
|
|
459
|
+
// Antigravity CLI: its OWN response shapes (NOT Claude's) — verified live
|
|
460
|
+
// against agy v1.1.2. Tool/prompt events honor `{decision:"deny", reason}`
|
|
461
|
+
// on stdout at exit 0 (hard block). The Stop event has no exit-2 retry;
|
|
462
|
+
// instead `{decision:"continue", reason}` re-enters the loop and injects
|
|
463
|
+
// the reason as a system message — that is how the 5 require-*-before-stop
|
|
464
|
+
// builtins enforce on Antigravity.
|
|
465
|
+
if (session?.cli === "antigravity") {
|
|
466
|
+
if (eventType === "Stop") {
|
|
467
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
|
|
468
|
+
return {
|
|
469
|
+
exitCode: 0,
|
|
470
|
+
stdout: JSON.stringify({ decision: "continue", reason: reasonText }),
|
|
471
|
+
stderr: "",
|
|
472
|
+
policyName: policy.name,
|
|
473
|
+
reason,
|
|
474
|
+
decision: "deny",
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
exitCode: 0,
|
|
479
|
+
stdout: JSON.stringify({ decision: "deny", reason: blockedMessage }),
|
|
480
|
+
stderr: "",
|
|
481
|
+
policyName: policy.name,
|
|
482
|
+
reason,
|
|
483
|
+
decision: "deny",
|
|
484
|
+
};
|
|
485
|
+
}
|
|
536
486
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
decision: "deny",
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
// Non-Stop opencode events keep the generic Claude shape — the
|
|
561
|
-
// shim's applyDecision already handles permissionDecision: "deny"
|
|
562
|
-
// for tool events.
|
|
563
|
-
}
|
|
487
|
+
// Goose: the deny contract is `{"decision":"block","reason"}` on stdout at
|
|
488
|
+
// exit 0, honored on PreToolUse ONLY (shipped goose ≥ v1.37.0, PR
|
|
489
|
+
// block/goose#9304; exit 2 also blocks but JSON carries the reason
|
|
490
|
+
// cleanly). Goose has NO Stop event (the 5 require-*-before-stop builtins
|
|
491
|
+
// never fire for it — see CLAUDE.md) and does NOT honor deny on
|
|
492
|
+
// UserPromptSubmit/PostToolUse (observation) — a block emitted on those
|
|
493
|
+
// events is ignored (fail-open), a documented limitation. PreToolUse fires
|
|
494
|
+
// for the shell tool AND inside delegated subagents, so this one branch
|
|
495
|
+
// covers the entire enforceable surface. Mirrors the Hermes branch (no
|
|
496
|
+
// turn-end event to special-case). Verified live against goose v1.43.0.
|
|
497
|
+
if (session?.cli === "goose") {
|
|
498
|
+
return {
|
|
499
|
+
exitCode: 0,
|
|
500
|
+
stdout: JSON.stringify({ decision: "block", reason: blockedMessage }),
|
|
501
|
+
stderr: "",
|
|
502
|
+
policyName: policy.name,
|
|
503
|
+
reason,
|
|
504
|
+
decision: "deny",
|
|
505
|
+
};
|
|
506
|
+
}
|
|
564
507
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
decision: "deny",
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
return {
|
|
586
|
-
exitCode: 2,
|
|
587
|
-
stdout: "",
|
|
588
|
-
stderr: blockedMessage + "\n",
|
|
589
|
-
policyName: policyName,
|
|
590
|
-
reason,
|
|
591
|
-
decision: "deny",
|
|
592
|
-
};
|
|
593
|
-
}
|
|
508
|
+
if (eventType === "PreToolUse") {
|
|
509
|
+
const response = {
|
|
510
|
+
hookSpecificOutput: {
|
|
511
|
+
hookEventName: eventType,
|
|
512
|
+
permissionDecision: "deny",
|
|
513
|
+
permissionDecisionReason: blockedMessage,
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
return {
|
|
517
|
+
exitCode: 0,
|
|
518
|
+
stdout: JSON.stringify(response),
|
|
519
|
+
stderr: "",
|
|
520
|
+
policyName: policy.name,
|
|
521
|
+
reason,
|
|
522
|
+
decision: "deny",
|
|
523
|
+
};
|
|
524
|
+
}
|
|
594
525
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
526
|
+
// Copilot reads two shapes we were not sending, so a deny on either of
|
|
527
|
+
// these events was emitted, logged, counted as enforcement — and ignored.
|
|
528
|
+
// Both verified against the shipped @github/copilot-linux-x64 bundle.
|
|
529
|
+
// Note exit 2 is NEVER a deny channel on copilot for any event; it is
|
|
530
|
+
// logged as `Hook command exited with code 2 (warning)`.
|
|
531
|
+
if (session?.cli === "copilot") {
|
|
532
|
+
// `userPromptSubmit` gates the turn, but only on {decision:"block"} at
|
|
533
|
+
// exit 0 (consumer app.js@2823018). We were emitting exit 2 + stderr,
|
|
534
|
+
// which copilot warns about and then submits the prompt anyway.
|
|
535
|
+
if (eventType === "UserPromptSubmit") {
|
|
536
|
+
return {
|
|
537
|
+
exitCode: 0,
|
|
538
|
+
stdout: JSON.stringify({ decision: "block", reason: blockedMessage }),
|
|
539
|
+
stderr: "",
|
|
540
|
+
policyName: policy.name,
|
|
541
|
+
reason,
|
|
542
|
+
decision: "deny",
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
// Copilot consumes a FLAT {behavior, message} here (normalizer
|
|
546
|
+
// CMn@179042 -> mapper h4t@2686538). The Codex-shaped nested
|
|
547
|
+
// hookSpecificOutput.decision below normalizes to `{}` on copilot, so
|
|
548
|
+
// the permission prompt proceeded as if no policy existed.
|
|
549
|
+
if (eventType === "PermissionRequest") {
|
|
550
|
+
return {
|
|
551
|
+
exitCode: 0,
|
|
552
|
+
stdout: JSON.stringify({ behavior: "deny", message: blockedMessage }),
|
|
553
|
+
stderr: "",
|
|
554
|
+
policyName: policy.name,
|
|
555
|
+
reason,
|
|
556
|
+
decision: "deny",
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
614
560
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
stderr: "",
|
|
637
|
-
policyName: policyName,
|
|
638
|
-
reason,
|
|
639
|
-
decision: "deny",
|
|
640
|
-
};
|
|
641
|
-
}
|
|
561
|
+
if (eventType === "PermissionRequest") {
|
|
562
|
+
// Codex-only: hookSpecificOutput.decision.behavior = "allow" | "deny"
|
|
563
|
+
// (per https://developers.openai.com/codex/hooks#permissionrequest).
|
|
564
|
+
const response = {
|
|
565
|
+
hookSpecificOutput: {
|
|
566
|
+
hookEventName: eventType,
|
|
567
|
+
decision: {
|
|
568
|
+
behavior: "deny",
|
|
569
|
+
message: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`,
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
return {
|
|
574
|
+
exitCode: 0,
|
|
575
|
+
stdout: JSON.stringify(response),
|
|
576
|
+
stderr: "",
|
|
577
|
+
policyName: policy.name,
|
|
578
|
+
reason,
|
|
579
|
+
decision: "deny",
|
|
580
|
+
};
|
|
581
|
+
}
|
|
642
582
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
583
|
+
if (eventType === "PostToolUse") {
|
|
584
|
+
// codex and copilot both gate PostToolUse on a TOP-LEVEL
|
|
585
|
+
// {decision:"block", reason:<string>}. Neither reads the
|
|
586
|
+
// hookSpecificOutput shape below, so every PostToolUse deny on them was
|
|
587
|
+
// emitted, logged, counted as enforcement — and dropped on the floor.
|
|
588
|
+
//
|
|
589
|
+
// codex 0.147.0, live A/B probe (same prompt, same hook, only the shape
|
|
590
|
+
// differs): with {decision:"block"} the run prints `hook: PostToolUse
|
|
591
|
+
// Blocked`, routes the reason through codex_core::tools::router, and the
|
|
592
|
+
// reason REPLACES the tool result — the model never sees the real
|
|
593
|
+
// output. With hookSpecificOutput it prints `hook: PostToolUse
|
|
594
|
+
// Completed` and the model reads the output verbatim. NOTE the codex
|
|
595
|
+
// rows elsewhere cite output_parser.rs / hook_runtime.rs / tools
|
|
596
|
+
// registry.rs line numbers that no longer exist in 0.147.0 (its hook
|
|
597
|
+
// sources are hooks/src/{engine,events}/…); this row is re-grounded on
|
|
598
|
+
// the live probe rather than those paths.
|
|
599
|
+
//
|
|
600
|
+
// copilot 1.0.78: BOTH postToolUse call sites in the shipped
|
|
601
|
+
// @github/copilot-linux-x64 app.js gate on
|
|
602
|
+
// vK = t => t?.decision === "block" && typeof t.reason === "string"
|
|
603
|
+
// -> "Tool result blocked". vK fails closed on a missing or non-string
|
|
604
|
+
// reason, so the reason must always be a non-empty string — do not
|
|
605
|
+
// "simplify" this to {decision:"block"} alone.
|
|
606
|
+
//
|
|
607
|
+
// The tool has already RUN in both cases. This replaces the result the
|
|
608
|
+
// model reads; it does not undo the side effect. That is the only
|
|
609
|
+
// semantic available at PostToolUse, and it is precisely what an
|
|
610
|
+
// output-scrubbing policy needs to keep a secret out of the context.
|
|
611
|
+
if (session?.cli === "codex" || session?.cli === "copilot") {
|
|
612
|
+
return {
|
|
613
|
+
exitCode: 0,
|
|
614
|
+
stdout: JSON.stringify({ decision: "block", reason: blockedMessage }),
|
|
615
|
+
stderr: "",
|
|
616
|
+
policyName: policy.name,
|
|
617
|
+
reason,
|
|
618
|
+
decision: "deny",
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const response = {
|
|
622
|
+
hookSpecificOutput: {
|
|
623
|
+
hookEventName: eventType,
|
|
624
|
+
additionalContext: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`,
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
return {
|
|
628
|
+
exitCode: 0,
|
|
629
|
+
stdout: JSON.stringify(response),
|
|
630
|
+
stderr: "",
|
|
631
|
+
policyName: policy.name,
|
|
632
|
+
reason,
|
|
633
|
+
decision: "deny",
|
|
634
|
+
};
|
|
635
|
+
}
|
|
663
636
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
637
|
+
if (eventType === "Stop" || eventType === "SubagentStop") {
|
|
638
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
|
|
639
|
+
// Copilot CLI: `agentStop` and `subagentStop` both honor
|
|
640
|
+
// `{decision: "block", reason}` JSON on stdout — the reason becomes the
|
|
641
|
+
// next-turn prompt and the agent (or subagent) retries. Exit-2 is logged
|
|
642
|
+
// as `[WARNING] Hook warning: ...` (verified empirically against Copilot
|
|
643
|
+
// CLI 1.0.41 events.jsonl) but does NOT trigger retry. We branch on both
|
|
644
|
+
// event types so that custom policies matching SubagentStop also enforce
|
|
645
|
+
// on Copilot subagent boundaries; the 5 builtin require-*-before-stop
|
|
646
|
+
// policies still match Stop only by design — they are session-completion
|
|
647
|
+
// gates (commit/push/PR/conflicts/CI), not subagent-return gates.
|
|
648
|
+
// Ref: https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-hooks-reference
|
|
649
|
+
if (session?.cli === "copilot") {
|
|
650
|
+
return {
|
|
651
|
+
exitCode: 0,
|
|
652
|
+
stdout: JSON.stringify({ decision: "block", reason: reasonText }),
|
|
653
|
+
stderr: "",
|
|
654
|
+
policyName: policy.name,
|
|
655
|
+
reason,
|
|
656
|
+
decision: "deny",
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
exitCode: 2,
|
|
661
|
+
stdout: "",
|
|
662
|
+
stderr: reasonText,
|
|
663
|
+
policyName: policy.name,
|
|
664
|
+
reason,
|
|
665
|
+
decision: "deny",
|
|
666
|
+
};
|
|
667
|
+
}
|
|
681
668
|
|
|
682
|
-
|
|
683
|
-
// these events was emitted, logged, counted as enforcement — and ignored.
|
|
684
|
-
// Both verified against the shipped @github/copilot-linux-x64 bundle.
|
|
685
|
-
// Note exit 2 is NEVER a deny channel on copilot for any event; it is
|
|
686
|
-
// logged as `Hook command exited with code 2 (warning)`.
|
|
687
|
-
if (session?.cli === "copilot") {
|
|
688
|
-
// `userPromptSubmit` gates the turn, but only on {decision:"block"} at
|
|
689
|
-
// exit 0 (consumer app.js@2823018). We were emitting exit 2 + stderr,
|
|
690
|
-
// which copilot warns about and then submits the prompt anyway.
|
|
691
|
-
if (eventType === "UserPromptSubmit") {
|
|
669
|
+
// Other event types (Cursor case already handled above): exit 2
|
|
692
670
|
return {
|
|
693
|
-
exitCode:
|
|
694
|
-
stdout:
|
|
695
|
-
stderr:
|
|
696
|
-
policyName:
|
|
671
|
+
exitCode: 2,
|
|
672
|
+
stdout: "",
|
|
673
|
+
stderr: reason,
|
|
674
|
+
policyName: policy.name,
|
|
697
675
|
reason,
|
|
698
676
|
decision: "deny",
|
|
699
677
|
};
|
|
700
678
|
}
|
|
701
|
-
// Copilot consumes a FLAT {behavior, message} here (normalizer
|
|
702
|
-
// CMn@179042 -> mapper h4t@2686538). The Codex-shaped nested
|
|
703
|
-
// hookSpecificOutput.decision below normalizes to `{}` on copilot, so
|
|
704
|
-
// the permission prompt proceeded as if no policy existed.
|
|
705
|
-
if (eventType === "PermissionRequest") {
|
|
706
|
-
return {
|
|
707
|
-
exitCode: 0,
|
|
708
|
-
stdout: JSON.stringify({ behavior: "deny", message: blockedMessage }),
|
|
709
|
-
stderr: "",
|
|
710
|
-
policyName: policyName,
|
|
711
|
-
reason,
|
|
712
|
-
decision: "deny",
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
679
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
message: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`,
|
|
726
|
-
},
|
|
727
|
-
},
|
|
728
|
-
};
|
|
729
|
-
return {
|
|
730
|
-
exitCode: 0,
|
|
731
|
-
stdout: JSON.stringify(response),
|
|
732
|
-
stderr: "",
|
|
733
|
-
policyName: policyName,
|
|
734
|
-
reason,
|
|
735
|
-
decision: "deny",
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (eventType === "PostToolUse") {
|
|
740
|
-
// codex and copilot both gate PostToolUse on a TOP-LEVEL
|
|
741
|
-
// {decision:"block", reason:<string>}. Neither reads the
|
|
742
|
-
// hookSpecificOutput shape below, so every PostToolUse deny on them was
|
|
743
|
-
// emitted, logged, counted as enforcement — and dropped on the floor.
|
|
744
|
-
//
|
|
745
|
-
// codex 0.147.0, live A/B probe (same prompt, same hook, only the shape
|
|
746
|
-
// differs): with {decision:"block"} the run prints `hook: PostToolUse
|
|
747
|
-
// Blocked`, routes the reason through codex_core::tools::router, and the
|
|
748
|
-
// reason REPLACES the tool result — the model never sees the real
|
|
749
|
-
// output. With hookSpecificOutput it prints `hook: PostToolUse
|
|
750
|
-
// Completed` and the model reads the output verbatim. NOTE the codex
|
|
751
|
-
// rows elsewhere cite output_parser.rs / hook_runtime.rs / tools
|
|
752
|
-
// registry.rs line numbers that no longer exist in 0.147.0 (its hook
|
|
753
|
-
// sources are hooks/src/{engine,events}/…); this row is re-grounded on
|
|
754
|
-
// the live probe rather than those paths.
|
|
755
|
-
//
|
|
756
|
-
// copilot 1.0.78: BOTH postToolUse call sites in the shipped
|
|
757
|
-
// @github/copilot-linux-x64 app.js gate on
|
|
758
|
-
// vK = t => t?.decision === "block" && typeof t.reason === "string"
|
|
759
|
-
// -> "Tool result blocked". vK fails closed on a missing or non-string
|
|
760
|
-
// reason, so the reason must always be a non-empty string — do not
|
|
761
|
-
// "simplify" this to {decision:"block"} alone.
|
|
762
|
-
//
|
|
763
|
-
// The tool has already RUN in both cases. This replaces the result the
|
|
764
|
-
// model reads; it does not undo the side effect. That is the only
|
|
765
|
-
// semantic available at PostToolUse, and it is precisely what an
|
|
766
|
-
// output-scrubbing policy needs to keep a secret out of the context.
|
|
767
|
-
if (session?.cli === "codex" || session?.cli === "copilot") {
|
|
768
|
-
return {
|
|
769
|
-
exitCode: 0,
|
|
770
|
-
stdout: JSON.stringify({ decision: "block", reason: blockedMessage }),
|
|
771
|
-
stderr: "",
|
|
772
|
-
policyName: policyName,
|
|
773
|
-
reason,
|
|
774
|
-
decision: "deny",
|
|
775
|
-
};
|
|
680
|
+
// Accumulate all instruct results (does not short-circuit — later policies can still deny)
|
|
681
|
+
if (result.decision === "instruct") {
|
|
682
|
+
const reason = appendHint(
|
|
683
|
+
result.reason ?? `Instruction from policy: ${policy.name}`,
|
|
684
|
+
getConfigParamsFor(config, policy.name)?.hint,
|
|
685
|
+
);
|
|
686
|
+
instructEntries.push({ policyName: policy.name, reason });
|
|
687
|
+
hookLogInfo(`instruct by "${policy.name}": ${reason}`);
|
|
776
688
|
}
|
|
777
|
-
const response = {
|
|
778
|
-
hookSpecificOutput: {
|
|
779
|
-
hookEventName: eventType,
|
|
780
|
-
additionalContext: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`,
|
|
781
|
-
},
|
|
782
|
-
};
|
|
783
|
-
return {
|
|
784
|
-
exitCode: 0,
|
|
785
|
-
stdout: JSON.stringify(response),
|
|
786
|
-
stderr: "",
|
|
787
|
-
policyName: policyName,
|
|
788
|
-
reason,
|
|
789
|
-
decision: "deny",
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
689
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
// `{decision: "block", reason}` JSON on stdout — the reason becomes the
|
|
797
|
-
// next-turn prompt and the agent (or subagent) retries. Exit-2 is logged
|
|
798
|
-
// as `[WARNING] Hook warning: ...` (verified empirically against Copilot
|
|
799
|
-
// CLI 1.0.41 events.jsonl) but does NOT trigger retry. We branch on both
|
|
800
|
-
// event types so that custom policies matching SubagentStop also enforce
|
|
801
|
-
// on Copilot subagent boundaries; the 5 builtin require-*-before-stop
|
|
802
|
-
// policies still match Stop only by design — they are session-completion
|
|
803
|
-
// gates (commit/push/PR/conflicts/CI), not subagent-return gates.
|
|
804
|
-
// Ref: https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-hooks-reference
|
|
805
|
-
if (session?.cli === "copilot") {
|
|
806
|
-
return {
|
|
807
|
-
exitCode: 0,
|
|
808
|
-
stdout: JSON.stringify({ decision: "block", reason: reasonText }),
|
|
809
|
-
stderr: "",
|
|
810
|
-
policyName: policyName,
|
|
811
|
-
reason,
|
|
812
|
-
decision: "deny",
|
|
813
|
-
};
|
|
690
|
+
// Accumulate informational messages from allow decisions
|
|
691
|
+
if (result.decision === "allow" && result.reason) {
|
|
692
|
+
allowEntries.push({ policyName: policy.name, reason: result.reason });
|
|
814
693
|
}
|
|
815
|
-
return {
|
|
816
|
-
exitCode: 2,
|
|
817
|
-
stdout: "",
|
|
818
|
-
stderr: reasonText,
|
|
819
|
-
policyName: policyName,
|
|
820
|
-
reason,
|
|
821
|
-
decision: "deny",
|
|
822
|
-
};
|
|
823
694
|
}
|
|
824
695
|
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
stderr: reason,
|
|
830
|
-
policyName: policyName,
|
|
831
|
-
reason,
|
|
832
|
-
decision: "deny",
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function formatInstruct(
|
|
837
|
-
eventType: HookEventType,
|
|
838
|
-
session: SessionMetadata | undefined,
|
|
839
|
-
instructEntries: ReadonlyArray<{ policyName: string; reason: string }>,
|
|
840
|
-
): EvaluationResult {
|
|
841
|
-
const combined = instructEntries.map((e) => e.reason).join("\n");
|
|
842
|
-
const policyNames = instructEntries.map((e) => e.policyName);
|
|
696
|
+
// No deny — check if we accumulated any instructs
|
|
697
|
+
if (instructEntries.length > 0) {
|
|
698
|
+
const combined = instructEntries.map((e) => e.reason).join("\n");
|
|
699
|
+
const policyNames = instructEntries.map((e) => e.policyName);
|
|
843
700
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
701
|
+
// Cursor's hook protocol uses a flat `{permission, additional_context}`
|
|
702
|
+
// shape for non-Stop and `{followup_message}` for Stop/SubagentStop.
|
|
703
|
+
// Branch first so the rest of the function only handles Claude-shaped
|
|
704
|
+
// responses. We match both Stop and SubagentStop so custom policies
|
|
705
|
+
// subscribing to SubagentStop on Cursor get the same force-retry
|
|
706
|
+
// semantics — mirrors the cli==="copilot" Stop|SubagentStop widening.
|
|
707
|
+
// Ref: https://cursor.com/docs/hooks (Stdout Response Format).
|
|
708
|
+
if (session?.cli === "cursor") {
|
|
709
|
+
if (eventType === "Stop" || eventType === "SubagentStop") {
|
|
710
|
+
const response = {
|
|
711
|
+
followup_message: `Instruction from failproofai: ${combined}`,
|
|
712
|
+
};
|
|
713
|
+
return {
|
|
714
|
+
exitCode: 0,
|
|
715
|
+
stdout: JSON.stringify(response),
|
|
716
|
+
stderr: "",
|
|
717
|
+
policyName: policyNames[0],
|
|
718
|
+
policyNames,
|
|
719
|
+
reason: combined,
|
|
720
|
+
decision: "instruct",
|
|
721
|
+
};
|
|
722
|
+
}
|
|
853
723
|
const response = {
|
|
854
|
-
|
|
724
|
+
permission: "allow",
|
|
725
|
+
additional_context: `Instruction from failproofai: ${combined}`,
|
|
855
726
|
};
|
|
856
727
|
return {
|
|
857
728
|
exitCode: 0,
|
|
@@ -863,37 +734,37 @@ function formatInstruct(
|
|
|
863
734
|
decision: "instruct",
|
|
864
735
|
};
|
|
865
736
|
}
|
|
866
|
-
const response = {
|
|
867
|
-
permission: "allow",
|
|
868
|
-
additional_context: `Instruction from failproofai: ${combined}`,
|
|
869
|
-
};
|
|
870
|
-
return {
|
|
871
|
-
exitCode: 0,
|
|
872
|
-
stdout: JSON.stringify(response),
|
|
873
|
-
stderr: "",
|
|
874
|
-
policyName: policyNames[0],
|
|
875
|
-
policyNames,
|
|
876
|
-
reason: combined,
|
|
877
|
-
decision: "instruct",
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
737
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
738
|
+
// Pi: instruct emits `{permission: "allow", reason}`. The shim won't
|
|
739
|
+
// block (no `"deny"`); it surfaces `reason` to the user where possible
|
|
740
|
+
// (Pi has no first-class `additional_context` channel in its tool-call
|
|
741
|
+
// return shape, so we log it). Stop is the exception — we emit a
|
|
742
|
+
// `permission: "deny"` with the MANDATORY ACTION wording so the shim
|
|
743
|
+
// captures it for next-turn before_agent_start injection. Same handoff
|
|
744
|
+
// contract as the deny branch above.
|
|
745
|
+
if (session?.cli === "pi") {
|
|
746
|
+
if (eventType === "Stop") {
|
|
747
|
+
const policyAttribution = policyNames.length === 1
|
|
748
|
+
? `policy: ${policyNames[0]}`
|
|
749
|
+
: `policies: ${policyNames.join(", ")}`;
|
|
750
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
|
|
751
|
+
return {
|
|
752
|
+
exitCode: 0,
|
|
753
|
+
stdout: JSON.stringify({ permission: "deny", reason: reasonText }),
|
|
754
|
+
stderr: "",
|
|
755
|
+
policyName: policyNames[0],
|
|
756
|
+
policyNames,
|
|
757
|
+
reason: combined,
|
|
758
|
+
decision: "instruct",
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
const response = {
|
|
762
|
+
permission: "allow",
|
|
763
|
+
reason: `Instruction from failproofai: ${combined}`,
|
|
764
|
+
};
|
|
894
765
|
return {
|
|
895
766
|
exitCode: 0,
|
|
896
|
-
stdout: JSON.stringify(
|
|
767
|
+
stdout: JSON.stringify(response),
|
|
897
768
|
stderr: "",
|
|
898
769
|
policyName: policyNames[0],
|
|
899
770
|
policyNames,
|
|
@@ -901,129 +772,149 @@ function formatInstruct(
|
|
|
901
772
|
decision: "instruct",
|
|
902
773
|
};
|
|
903
774
|
}
|
|
904
|
-
const response = {
|
|
905
|
-
permission: "allow",
|
|
906
|
-
reason: `Instruction from failproofai: ${combined}`,
|
|
907
|
-
};
|
|
908
|
-
return {
|
|
909
|
-
exitCode: 0,
|
|
910
|
-
stdout: JSON.stringify(response),
|
|
911
|
-
stderr: "",
|
|
912
|
-
policyName: policyNames[0],
|
|
913
|
-
policyNames,
|
|
914
|
-
reason: combined,
|
|
915
|
-
decision: "instruct",
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// Hermes: no additional-context channel on any event (the only actionable
|
|
920
|
-
// response is `{"decision":"block"}`). So instruct degrades to allow +
|
|
921
|
-
// log — we emit a non-blocking `{decision:"allow", reason}` (Hermes lets
|
|
922
|
-
// the tool run) and surface the note on stderr for the operator's logs.
|
|
923
|
-
// Documented limitation; there is no Stop event to force-retry into.
|
|
924
|
-
if (session?.cli === "hermes") {
|
|
925
|
-
const stderrMsg = instructEntries
|
|
926
|
-
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
927
|
-
.join("\n");
|
|
928
|
-
return {
|
|
929
|
-
exitCode: 0,
|
|
930
|
-
stdout: JSON.stringify({
|
|
931
|
-
decision: "allow",
|
|
932
|
-
reason: `Instruction from failproofai: ${combined}`,
|
|
933
|
-
}),
|
|
934
|
-
stderr: stderrMsg + "\n",
|
|
935
|
-
policyName: policyNames[0],
|
|
936
|
-
policyNames,
|
|
937
|
-
reason: combined,
|
|
938
|
-
decision: "instruct",
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
775
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
if (eventType === "Stop") {
|
|
952
|
-
const policyAttribution = policyNames.length === 1
|
|
953
|
-
? `policy: ${policyNames[0]}`
|
|
954
|
-
: `policies: ${policyNames.join(", ")}`;
|
|
955
|
-
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
|
|
776
|
+
// Hermes: no additional-context channel on any event (the only actionable
|
|
777
|
+
// response is `{"decision":"block"}`). So instruct degrades to allow +
|
|
778
|
+
// log — we emit a non-blocking `{decision:"allow", reason}` (Hermes lets
|
|
779
|
+
// the tool run) and surface the note on stderr for the operator's logs.
|
|
780
|
+
// Documented limitation; there is no Stop event to force-retry into.
|
|
781
|
+
if (session?.cli === "hermes") {
|
|
782
|
+
const stderrMsg = instructEntries
|
|
783
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
784
|
+
.join("\n");
|
|
956
785
|
return {
|
|
957
786
|
exitCode: 0,
|
|
958
|
-
stdout: JSON.stringify({
|
|
959
|
-
|
|
787
|
+
stdout: JSON.stringify({
|
|
788
|
+
decision: "allow",
|
|
789
|
+
reason: `Instruction from failproofai: ${combined}`,
|
|
790
|
+
}),
|
|
791
|
+
stderr: stderrMsg + "\n",
|
|
960
792
|
policyName: policyNames[0],
|
|
961
793
|
policyNames,
|
|
962
794
|
reason: combined,
|
|
963
795
|
decision: "instruct",
|
|
964
796
|
};
|
|
965
797
|
}
|
|
966
|
-
|
|
798
|
+
|
|
799
|
+
// OpenClaw: Stop (before_agent_finalize) can force a revise, so we emit the
|
|
800
|
+
// MANDATORY ACTION wording as a flat deny — the shim maps it to
|
|
801
|
+
// {action:"revise", reason}. PreToolUse has no non-blocking context channel,
|
|
802
|
+
// but a rejected tool's blockReason is model-visible. Preserve `instruct`
|
|
803
|
+
// in the wire verdict so the shim can interrupt the first matching attempt,
|
|
804
|
+
// deliver the instruction through blockReason, and allow a retry. Other
|
|
805
|
+
// events still degrade to allow + stderr because their return channels
|
|
806
|
+
// cannot carry an instruction to the model.
|
|
807
|
+
if (session?.cli === "openclaw") {
|
|
808
|
+
if (eventType === "Stop") {
|
|
809
|
+
const policyAttribution = policyNames.length === 1
|
|
810
|
+
? `policy: ${policyNames[0]}`
|
|
811
|
+
: `policies: ${policyNames.join(", ")}`;
|
|
812
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
|
|
813
|
+
return {
|
|
814
|
+
exitCode: 0,
|
|
815
|
+
stdout: JSON.stringify({ permission: "deny", reason: reasonText }),
|
|
816
|
+
stderr: "",
|
|
817
|
+
policyName: policyNames[0],
|
|
818
|
+
policyNames,
|
|
819
|
+
reason: combined,
|
|
820
|
+
decision: "instruct",
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
if (eventType === "PreToolUse") {
|
|
824
|
+
return {
|
|
825
|
+
exitCode: 0,
|
|
826
|
+
stdout: JSON.stringify({
|
|
827
|
+
permission: "instruct",
|
|
828
|
+
reason: `Instruction from failproofai: ${combined}`,
|
|
829
|
+
policyName: policyNames[0],
|
|
830
|
+
policyNames,
|
|
831
|
+
}),
|
|
832
|
+
stderr: "",
|
|
833
|
+
policyName: policyNames[0],
|
|
834
|
+
policyNames,
|
|
835
|
+
reason: combined,
|
|
836
|
+
decision: "instruct",
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const stderrMsg = instructEntries
|
|
840
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
841
|
+
.join("\n");
|
|
967
842
|
return {
|
|
968
843
|
exitCode: 0,
|
|
969
844
|
stdout: JSON.stringify({
|
|
970
|
-
permission: "
|
|
845
|
+
permission: "allow",
|
|
971
846
|
reason: `Instruction from failproofai: ${combined}`,
|
|
972
|
-
policyName: policyNames[0],
|
|
973
|
-
policyNames,
|
|
974
847
|
}),
|
|
975
|
-
stderr: "",
|
|
848
|
+
stderr: stderrMsg + "\n",
|
|
976
849
|
policyName: policyNames[0],
|
|
977
850
|
policyNames,
|
|
978
851
|
reason: combined,
|
|
979
852
|
decision: "instruct",
|
|
980
853
|
};
|
|
981
854
|
}
|
|
982
|
-
const stderrMsg = instructEntries
|
|
983
|
-
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
984
|
-
.join("\n");
|
|
985
|
-
return {
|
|
986
|
-
exitCode: 0,
|
|
987
|
-
stdout: JSON.stringify({
|
|
988
|
-
permission: "allow",
|
|
989
|
-
reason: `Instruction from failproofai: ${combined}`,
|
|
990
|
-
}),
|
|
991
|
-
stderr: stderrMsg + "\n",
|
|
992
|
-
policyName: policyNames[0],
|
|
993
|
-
policyNames,
|
|
994
|
-
reason: combined,
|
|
995
|
-
decision: "instruct",
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
855
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
856
|
+
// OpenCode: same rationale as the deny branch above — emit
|
|
857
|
+
// additionalContext so the shim submits a follow-up via
|
|
858
|
+
// client.session.prompt instead of throwing into a dead handler.
|
|
859
|
+
if (session?.cli === "opencode") {
|
|
860
|
+
if (eventType === "Stop" || eventType === "SubagentStop") {
|
|
861
|
+
const policyAttribution = policyNames.length === 1
|
|
862
|
+
? `policy: ${policyNames[0]}`
|
|
863
|
+
: `policies: ${policyNames.join(", ")}`;
|
|
864
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
|
|
865
|
+
return {
|
|
866
|
+
exitCode: 0,
|
|
867
|
+
stdout: JSON.stringify({ hookSpecificOutput: { additionalContext: reasonText } }),
|
|
868
|
+
stderr: "",
|
|
869
|
+
policyName: policyNames[0],
|
|
870
|
+
policyNames,
|
|
871
|
+
reason: combined,
|
|
872
|
+
decision: "instruct",
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Factory droid: on Stop, emit the MANDATORY ACTION wording as a
|
|
878
|
+
// `{decision:"block", reason}` on stdout (exit 0) — droid's only turn-end
|
|
879
|
+
// force-retry channel. Every other event lacks an additional-context
|
|
880
|
+
// channel (droid honors JSON only for the Stop block), so instruct degrades
|
|
881
|
+
// to allow + stderr note, like Hermes.
|
|
882
|
+
if (session?.cli === "factory") {
|
|
883
|
+
if (eventType === "Stop") {
|
|
884
|
+
const policyAttribution = policyNames.length === 1
|
|
885
|
+
? `policy: ${policyNames[0]}`
|
|
886
|
+
: `policies: ${policyNames.join(", ")}`;
|
|
887
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
|
|
888
|
+
return {
|
|
889
|
+
exitCode: 0,
|
|
890
|
+
stdout: JSON.stringify({ decision: "block", reason: reasonText }),
|
|
891
|
+
stderr: "",
|
|
892
|
+
policyName: policyNames[0],
|
|
893
|
+
policyNames,
|
|
894
|
+
reason: combined,
|
|
895
|
+
decision: "instruct",
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const stderrMsg = instructEntries
|
|
899
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
900
|
+
.join("\n");
|
|
1008
901
|
return {
|
|
1009
902
|
exitCode: 0,
|
|
1010
|
-
stdout:
|
|
1011
|
-
stderr: "",
|
|
903
|
+
stdout: "",
|
|
904
|
+
stderr: stderrMsg + "\n",
|
|
1012
905
|
policyName: policyNames[0],
|
|
1013
906
|
policyNames,
|
|
1014
907
|
reason: combined,
|
|
1015
908
|
decision: "instruct",
|
|
1016
909
|
};
|
|
1017
910
|
}
|
|
1018
|
-
}
|
|
1019
911
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
if (eventType === "Stop") {
|
|
912
|
+
// Devin CLI: a pure Claude-clone. On Stop, emit the MANDATORY ACTION
|
|
913
|
+
// wording as `{decision:"block", reason}` on stdout (exit 0) — Devin's
|
|
914
|
+
// turn-end force-retry channel (its exit-2 is not a force-retry). Every
|
|
915
|
+
// other event falls through to the generic Claude additionalContext path
|
|
916
|
+
// below (Devin honors `hookSpecificOutput.additionalContext`).
|
|
917
|
+
if (session?.cli === "devin" && eventType === "Stop") {
|
|
1027
918
|
const policyAttribution = policyNames.length === 1
|
|
1028
919
|
? `policy: ${policyNames[0]}`
|
|
1029
920
|
: `policies: ${policyNames.join(", ")}`;
|
|
@@ -1038,103 +929,123 @@ function formatInstruct(
|
|
|
1038
929
|
decision: "instruct",
|
|
1039
930
|
};
|
|
1040
931
|
}
|
|
1041
|
-
const stderrMsg = instructEntries
|
|
1042
|
-
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
1043
|
-
.join("\n");
|
|
1044
|
-
return {
|
|
1045
|
-
exitCode: 0,
|
|
1046
|
-
stdout: "",
|
|
1047
|
-
stderr: stderrMsg + "\n",
|
|
1048
|
-
policyName: policyNames[0],
|
|
1049
|
-
policyNames,
|
|
1050
|
-
reason: combined,
|
|
1051
|
-
decision: "instruct",
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
932
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
933
|
+
// Antigravity CLI: its OWN instruct shapes (verified live agy v1.1.2).
|
|
934
|
+
// • UserPromptSubmit (canonical for PreInvocation) → `{injectSteps:[{
|
|
935
|
+
// ephemeralMessage}]}` injects the instruction as a transient system
|
|
936
|
+
// message before the model runs.
|
|
937
|
+
// • Stop → `{decision:"continue", reason}` re-enters the loop with the
|
|
938
|
+
// MANDATORY-ACTION directive (Antigravity's only turn-end channel).
|
|
939
|
+
// • Every other event lacks an additional-context channel → degrade to
|
|
940
|
+
// allow + stderr note, like Hermes.
|
|
941
|
+
if (session?.cli === "antigravity") {
|
|
942
|
+
if (eventType === "UserPromptSubmit") {
|
|
943
|
+
return {
|
|
944
|
+
exitCode: 0,
|
|
945
|
+
stdout: JSON.stringify({
|
|
946
|
+
injectSteps: [{ ephemeralMessage: `Instruction from failproofai: ${combined}` }],
|
|
947
|
+
}),
|
|
948
|
+
stderr: "",
|
|
949
|
+
policyName: policyNames[0],
|
|
950
|
+
policyNames,
|
|
951
|
+
reason: combined,
|
|
952
|
+
decision: "instruct",
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (eventType === "Stop") {
|
|
956
|
+
const policyAttribution = policyNames.length === 1
|
|
957
|
+
? `policy: ${policyNames[0]}`
|
|
958
|
+
: `policies: ${policyNames.join(", ")}`;
|
|
959
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
|
|
960
|
+
return {
|
|
961
|
+
exitCode: 0,
|
|
962
|
+
stdout: JSON.stringify({ decision: "continue", reason: reasonText }),
|
|
963
|
+
stderr: "",
|
|
964
|
+
policyName: policyNames[0],
|
|
965
|
+
policyNames,
|
|
966
|
+
reason: combined,
|
|
967
|
+
decision: "instruct",
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const stderrMsg = instructEntries
|
|
971
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
972
|
+
.join("\n");
|
|
973
|
+
return {
|
|
974
|
+
exitCode: 0,
|
|
975
|
+
stdout: "",
|
|
976
|
+
stderr: stderrMsg + "\n",
|
|
977
|
+
policyName: policyNames[0],
|
|
978
|
+
policyNames,
|
|
979
|
+
reason: combined,
|
|
980
|
+
decision: "instruct",
|
|
981
|
+
};
|
|
982
|
+
}
|
|
1075
983
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
// allow + stderr note, like Hermes.
|
|
1084
|
-
if (session?.cli === "antigravity") {
|
|
1085
|
-
if (eventType === "UserPromptSubmit") {
|
|
984
|
+
// Goose: a non-block PreToolUse decision injects nothing (verified live — no
|
|
985
|
+
// additional-context channel), and Goose has no Stop event, so instruct
|
|
986
|
+
// degrades to allow + stderr note, like Hermes / Factory non-Stop events.
|
|
987
|
+
if (session?.cli === "goose") {
|
|
988
|
+
const stderrMsg = instructEntries
|
|
989
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
990
|
+
.join("\n");
|
|
1086
991
|
return {
|
|
1087
992
|
exitCode: 0,
|
|
1088
|
-
stdout:
|
|
1089
|
-
|
|
1090
|
-
}),
|
|
1091
|
-
stderr: "",
|
|
993
|
+
stdout: "",
|
|
994
|
+
stderr: stderrMsg + "\n",
|
|
1092
995
|
policyName: policyNames[0],
|
|
1093
996
|
policyNames,
|
|
1094
997
|
reason: combined,
|
|
1095
998
|
decision: "instruct",
|
|
1096
999
|
};
|
|
1097
1000
|
}
|
|
1098
|
-
|
|
1001
|
+
|
|
1002
|
+
if (eventType === "Stop" || eventType === "SubagentStop") {
|
|
1003
|
+
// Stop/SubagentStop instruct: exitCode 2 + stderr forces Claude to retry
|
|
1004
|
+
// the agent (or subagent) loop with the reason as context. Same widening
|
|
1005
|
+
// as the deny branch above — custom policies subscribing to
|
|
1006
|
+
// SubagentStop need the same retry semantics; the 5 builtin
|
|
1007
|
+
// require-*-before-stop policies still match Stop only by design.
|
|
1099
1008
|
const policyAttribution = policyNames.length === 1
|
|
1100
1009
|
? `policy: ${policyNames[0]}`
|
|
1101
1010
|
: `policies: ${policyNames.join(", ")}`;
|
|
1102
1011
|
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
|
|
1012
|
+
// Copilot CLI: exit-2 from agentStop / subagentStop is logged as
|
|
1013
|
+
// `[WARNING] Hook warning: ...` but does NOT trigger retry. The
|
|
1014
|
+
// documented retry shape is `{decision: "block", reason}` JSON on
|
|
1015
|
+
// stdout (exit 0). Mirrors the cli==="copilot" branch in the deny
|
|
1016
|
+
// path so custom instruct policies enforce on Copilot.
|
|
1017
|
+
if (session?.cli === "copilot") {
|
|
1018
|
+
return {
|
|
1019
|
+
exitCode: 0,
|
|
1020
|
+
stdout: JSON.stringify({ decision: "block", reason: reasonText }),
|
|
1021
|
+
stderr: "",
|
|
1022
|
+
policyName: policyNames[0],
|
|
1023
|
+
policyNames,
|
|
1024
|
+
reason: combined,
|
|
1025
|
+
decision: "instruct",
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1103
1028
|
return {
|
|
1104
|
-
exitCode:
|
|
1105
|
-
stdout:
|
|
1106
|
-
stderr:
|
|
1029
|
+
exitCode: 2,
|
|
1030
|
+
stdout: "",
|
|
1031
|
+
stderr: reasonText,
|
|
1107
1032
|
policyName: policyNames[0],
|
|
1108
1033
|
policyNames,
|
|
1109
1034
|
reason: combined,
|
|
1110
1035
|
decision: "instruct",
|
|
1111
1036
|
};
|
|
1112
1037
|
}
|
|
1113
|
-
const stderrMsg = instructEntries
|
|
1114
|
-
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
1115
|
-
.join("\n");
|
|
1116
|
-
return {
|
|
1117
|
-
exitCode: 0,
|
|
1118
|
-
stdout: "",
|
|
1119
|
-
stderr: stderrMsg + "\n",
|
|
1120
|
-
policyName: policyNames[0],
|
|
1121
|
-
policyNames,
|
|
1122
|
-
reason: combined,
|
|
1123
|
-
decision: "instruct",
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1126
1038
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
.join("\n");
|
|
1039
|
+
const response = {
|
|
1040
|
+
hookSpecificOutput: {
|
|
1041
|
+
hookEventName: eventType,
|
|
1042
|
+
additionalContext: `Instruction from failproofai: ${combined}`,
|
|
1043
|
+
},
|
|
1044
|
+
};
|
|
1134
1045
|
return {
|
|
1135
1046
|
exitCode: 0,
|
|
1136
|
-
stdout:
|
|
1137
|
-
stderr:
|
|
1047
|
+
stdout: JSON.stringify(response),
|
|
1048
|
+
stderr: "",
|
|
1138
1049
|
policyName: policyNames[0],
|
|
1139
1050
|
policyNames,
|
|
1140
1051
|
reason: combined,
|
|
@@ -1142,148 +1053,93 @@ function formatInstruct(
|
|
|
1142
1053
|
};
|
|
1143
1054
|
}
|
|
1144
1055
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
//
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
if (session?.cli === "copilot") {
|
|
1056
|
+
// All policies allowed — pass along any informational messages
|
|
1057
|
+
if (allowEntries.length > 0) {
|
|
1058
|
+
const combined = allowEntries.map((e) => e.reason).join("\n");
|
|
1059
|
+
const policyNames = allowEntries.map((e) => e.policyName);
|
|
1060
|
+
|
|
1061
|
+
// Cursor: emit the flat shape; allow-with-info maps to
|
|
1062
|
+
// `{permission: "allow", additional_context}`.
|
|
1063
|
+
if (session?.cli === "cursor") {
|
|
1064
|
+
const response = {
|
|
1065
|
+
permission: "allow",
|
|
1066
|
+
additional_context: `Note from failproofai: ${combined}`,
|
|
1067
|
+
};
|
|
1068
|
+
const stderrMsg = allowEntries
|
|
1069
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
1070
|
+
.join("\n");
|
|
1161
1071
|
return {
|
|
1162
1072
|
exitCode: 0,
|
|
1163
|
-
stdout: JSON.stringify(
|
|
1164
|
-
stderr: "",
|
|
1073
|
+
stdout: JSON.stringify(response),
|
|
1074
|
+
stderr: stderrMsg + "\n",
|
|
1165
1075
|
policyName: policyNames[0],
|
|
1166
1076
|
policyNames,
|
|
1167
1077
|
reason: combined,
|
|
1168
|
-
decision: "
|
|
1078
|
+
decision: "allow",
|
|
1169
1079
|
};
|
|
1170
1080
|
}
|
|
1171
|
-
return {
|
|
1172
|
-
exitCode: 2,
|
|
1173
|
-
stdout: "",
|
|
1174
|
-
stderr: reasonText,
|
|
1175
|
-
policyName: policyNames[0],
|
|
1176
|
-
policyNames,
|
|
1177
|
-
reason: combined,
|
|
1178
|
-
decision: "instruct",
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
1081
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
session: SessionMetadata | undefined,
|
|
1202
|
-
allowEntries: ReadonlyArray<{ policyName: string; reason: string }>,
|
|
1203
|
-
): EvaluationResult {
|
|
1204
|
-
const combined = allowEntries.map((e) => e.reason).join("\n");
|
|
1205
|
-
const policyNames = allowEntries.map((e) => e.policyName);
|
|
1206
|
-
|
|
1207
|
-
// Cursor: emit the flat shape; allow-with-info maps to
|
|
1208
|
-
// `{permission: "allow", additional_context}`.
|
|
1209
|
-
if (session?.cli === "cursor") {
|
|
1210
|
-
const response = {
|
|
1211
|
-
permission: "allow",
|
|
1212
|
-
additional_context: `Note from failproofai: ${combined}`,
|
|
1213
|
-
};
|
|
1214
|
-
const stderrMsg = allowEntries
|
|
1215
|
-
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
1216
|
-
.join("\n");
|
|
1217
|
-
return {
|
|
1218
|
-
exitCode: 0,
|
|
1219
|
-
stdout: JSON.stringify(response),
|
|
1220
|
-
stderr: stderrMsg + "\n",
|
|
1221
|
-
policyName: policyNames[0],
|
|
1222
|
-
policyNames,
|
|
1223
|
-
reason: combined,
|
|
1224
|
-
decision: "allow",
|
|
1225
|
-
};
|
|
1226
|
-
}
|
|
1082
|
+
// Pi: same shape as Cursor — flat `{permission: "allow", reason}`.
|
|
1083
|
+
if (session?.cli === "pi") {
|
|
1084
|
+
const response = {
|
|
1085
|
+
permission: "allow",
|
|
1086
|
+
reason: `Note from failproofai: ${combined}`,
|
|
1087
|
+
};
|
|
1088
|
+
const stderrMsg = allowEntries
|
|
1089
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
1090
|
+
.join("\n");
|
|
1091
|
+
return {
|
|
1092
|
+
exitCode: 0,
|
|
1093
|
+
stdout: JSON.stringify(response),
|
|
1094
|
+
stderr: stderrMsg + "\n",
|
|
1095
|
+
policyName: policyNames[0],
|
|
1096
|
+
policyNames,
|
|
1097
|
+
reason: combined,
|
|
1098
|
+
decision: "allow",
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1227
1101
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1102
|
+
// OpenClaw: same flat shape as Pi — {permission:"allow", reason}. The shim
|
|
1103
|
+
// returns undefined (no block) for an allow verdict regardless, so the note
|
|
1104
|
+
// surfaces via stderr; keeping the flat stdout shape keeps the shim's parse
|
|
1105
|
+
// path uniform across every verdict.
|
|
1106
|
+
if (session?.cli === "openclaw") {
|
|
1107
|
+
const stderrMsg = allowEntries
|
|
1108
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
1109
|
+
.join("\n");
|
|
1110
|
+
return {
|
|
1111
|
+
exitCode: 0,
|
|
1112
|
+
stdout: JSON.stringify({
|
|
1113
|
+
permission: "allow",
|
|
1114
|
+
reason: `Note from failproofai: ${combined}`,
|
|
1115
|
+
}),
|
|
1116
|
+
stderr: stderrMsg + "\n",
|
|
1117
|
+
policyName: policyNames[0],
|
|
1118
|
+
policyNames,
|
|
1119
|
+
reason: combined,
|
|
1120
|
+
decision: "allow",
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1247
1123
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1124
|
+
const supportsHookSpecificOutput =
|
|
1125
|
+
eventType === "PreToolUse" ||
|
|
1126
|
+
eventType === "PostToolUse" ||
|
|
1127
|
+
eventType === "UserPromptSubmit" ||
|
|
1128
|
+
eventType === "PermissionRequest";
|
|
1253
1129
|
const stderrMsg = allowEntries
|
|
1254
1130
|
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
1255
1131
|
.join("\n");
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
policyNames,
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
const supportsHookSpecificOutput =
|
|
1271
|
-
eventType === "PreToolUse" ||
|
|
1272
|
-
eventType === "PostToolUse" ||
|
|
1273
|
-
eventType === "UserPromptSubmit" ||
|
|
1274
|
-
eventType === "PermissionRequest";
|
|
1275
|
-
const stderrMsg = allowEntries
|
|
1276
|
-
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
1277
|
-
.join("\n");
|
|
1278
|
-
// Only events with a real additional-context channel carry the allow-note
|
|
1279
|
-
// to the agent. Everything else (Stop, SubagentStop, Session*, PreCompact, …)
|
|
1280
|
-
// has NO channel, so we keep informational allow-notes OUT of stdout — a
|
|
1281
|
-
// bare `{reason}` there is rendered as noise (e.g. droid printing a Stop's
|
|
1282
|
-
// "…skipping commit check…skipping PR check…" wall on a perfectly fine turn).
|
|
1283
|
-
// The note is still logged to stderr + the activity store for diagnostics.
|
|
1284
|
-
if (supportsHookSpecificOutput) {
|
|
1285
|
-
const response = { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } };
|
|
1286
|
-
return { exitCode: 0, stdout: JSON.stringify(response), stderr: stderrMsg + "\n", policyName: policyNames[0], policyNames, reason: combined, decision: "allow" };
|
|
1132
|
+
// Only events with a real additional-context channel carry the allow-note
|
|
1133
|
+
// to the agent. Everything else (Stop, SubagentStop, Session*, PreCompact, …)
|
|
1134
|
+
// has NO channel, so we keep informational allow-notes OUT of stdout — a
|
|
1135
|
+
// bare `{reason}` there is rendered as noise (e.g. droid printing a Stop's
|
|
1136
|
+
// "…skipping commit check…skipping PR check…" wall on a perfectly fine turn).
|
|
1137
|
+
// The note is still logged to stderr + the activity store for diagnostics.
|
|
1138
|
+
if (supportsHookSpecificOutput) {
|
|
1139
|
+
const response = { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } };
|
|
1140
|
+
return { exitCode: 0, stdout: JSON.stringify(response), stderr: stderrMsg + "\n", policyName: policyNames[0], policyNames, reason: combined, decision: "allow" };
|
|
1141
|
+
}
|
|
1142
|
+
return { exitCode: 0, stdout: "", stderr: stderrMsg + "\n", policyName: policyNames[0], policyNames, reason: combined, decision: "allow" };
|
|
1287
1143
|
}
|
|
1288
|
-
return { exitCode: 0, stdout: "", stderr:
|
|
1144
|
+
return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
|
|
1289
1145
|
}
|