failproofai 1.0.3 → 1.0.4-beta.1
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/.grok/hooks/failproofai.json +172 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_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 +4 -4
- 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 +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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +1 -1
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
- 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/build-manifest.json +2 -2
- 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 +2 -2
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- 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]__0u3wrvq._.js → [externals]__0_gzk8q._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1zndo1i._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07hor7h._.js +5 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0elsuli._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0gnj_qa._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljuzm_._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w23bpg._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0x5c7sh._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yik9_w._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1jg6s9m._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1s6qci9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xfm05z._.js +3 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tdy_6l._.js +3 -0
- 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/_1ek68ln._.js +21 -14
- package/.next/standalone/.next/server/chunks/lib_grok-projects_ts_1i2epl_._.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_qwen-projects_ts_0wu_a8e._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_4gcz8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d--wmy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qqrlf_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s11fx1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0wvusu0._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1b5jzxr._.js → [root-of-the-server]__16gamwq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bbk-si._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1c3ybxr._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1izi7zn._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zl_r0t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__21bj7uw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0v8eebd._.js → _00z5uuq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1if9ixi._.js → _08f0jd2._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0wso5d3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0j6gd_k._.js → _1byck5v._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +155 -1
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_cline-projects_ts_13_hbdd._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_grok-projects_ts_0rc-d0o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_ori-projects_ts_04tofqo._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_qwen-projects_ts_0w3i9o7._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- 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 +19 -19
- package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 00gv0v91mycm-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 01hvwwrhvbzyh.js} +1 -1
- package/.next/standalone/.next/static/chunks/0hzmxwb2pvk5p.js +6 -0
- package/.next/standalone/.next/static/chunks/{0nm11ysn0153h.js → 0j884dwnwtyrt.js} +1 -1
- package/.next/standalone/.next/static/chunks/0mr9dg3bnloa1.js +1 -0
- package/.next/standalone/.next/static/chunks/0qn9khv0oo9ex.js +1 -0
- package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 1_2jsatfuthrw.js} +1 -1
- package/.next/standalone/.next/static/chunks/1_bxotmf9qngs.js +1 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 1po3lirgk2riw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{40lpt2o2b283e.js → 2bnf_4pgyk1rq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 2bvwsyu6x__ia.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3l36d73kj18zw.js → 3_--3ibia6ba1.js} +1 -1
- package/.next/standalone/.next/static/chunks/3gf1jg-5m-u1t.css +2 -0
- package/.next/standalone/.next/static/chunks/{3pibxnbhjza7n.js → 3ws7eljkq25wg.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-18qf4jodtft6b.js} +1 -1
- package/.next/standalone/.qwen/settings.json +251 -0
- package/.next/standalone/app/project/[name]/page.tsx +24 -4
- package/.next/standalone/assets/logos/cline.png +0 -0
- package/.next/standalone/assets/logos/grok-dark.svg +1 -0
- package/.next/standalone/assets/logos/grok-light.svg +1 -0
- package/.next/standalone/assets/logos/ori-dark.svg +1 -0
- package/.next/standalone/assets/logos/ori-light.svg +1 -0
- package/.next/standalone/assets/logos/qwen.svg +1 -0
- package/.next/standalone/lib/cli-registry.ts +33 -1
- package/.next/standalone/lib/cline-projects.ts +155 -0
- package/.next/standalone/lib/cline-sessions.ts +268 -0
- package/.next/standalone/lib/download-session.ts +37 -0
- package/.next/standalone/lib/grok-projects.ts +123 -0
- package/.next/standalone/lib/grok-sessions.ts +355 -0
- package/.next/standalone/lib/ori-projects.ts +141 -0
- package/.next/standalone/lib/ori-sessions.ts +221 -0
- package/.next/standalone/lib/projects.ts +27 -3
- package/.next/standalone/lib/qwen-projects.ts +106 -0
- package/.next/standalone/lib/qwen-sessions.ts +313 -0
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +10 -10
- package/.next/standalone/package.json +11 -11
- package/.next/standalone/sdk/python/uv.lock +29 -29
- package/.next/standalone/server.js +1 -1
- package/README.md +38 -7
- package/bin/failproofai.mjs +24 -7
- package/dist/cli.mjs +20313 -17756
- package/dist/worker.mjs +8521 -6768
- package/lib/cli-registry.ts +33 -1
- package/lib/cline-projects.ts +155 -0
- package/lib/cline-sessions.ts +268 -0
- package/lib/download-session.ts +37 -0
- package/lib/grok-projects.ts +123 -0
- package/lib/grok-sessions.ts +355 -0
- package/lib/ori-projects.ts +141 -0
- package/lib/ori-sessions.ts +221 -0
- package/lib/projects.ts +27 -3
- package/lib/qwen-projects.ts +106 -0
- package/lib/qwen-sessions.ts +313 -0
- package/package.json +11 -11
- package/src/audit/cli-adapters/cline.ts +50 -0
- package/src/audit/cli-adapters/grok.ts +70 -0
- package/src/audit/cli-adapters/index.ts +24 -0
- package/src/audit/cli-adapters/ori.ts +55 -0
- package/src/audit/cli-adapters/qwen.ts +71 -0
- package/src/hooks/batch-expand.ts +380 -0
- package/src/hooks/batch-fanout.ts +142 -0
- package/src/hooks/builtin-policies.ts +6 -4
- package/src/hooks/enforcement-capability.ts +97 -0
- package/src/hooks/handler.ts +61 -5
- package/src/hooks/harness-cli.ts +3 -1
- package/src/hooks/integrations.ts +756 -2
- package/src/hooks/normalize-cli-payload.ts +126 -0
- package/src/hooks/policy-evaluator.ts +297 -6
- package/src/hooks/resolve-transcript-path.ts +10 -0
- package/src/hooks/risk-patterns.ts +26 -0
- package/src/hooks/tool-name-canonicalize.ts +64 -0
- package/src/hooks/types.ts +533 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +0 -5
- package/.next/standalone/.next/server/chunks/_1-akg6w._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00-s7h8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__190won8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1sc84bb._.js +0 -3
- package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
- package/.next/standalone/.next/static/chunks/1-_zkmnm-jwei.js +0 -1
- package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
- package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
- package/.next/standalone/.next/static/chunks/40nr6ehj4p3cw.css +0 -2
- package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_ssgManifest.js +0 -0
|
@@ -10,6 +10,56 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { IntegrationType } from "./types";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* True when `parsed` carries grok's camelCase envelope.
|
|
15
|
+
*
|
|
16
|
+
* This exists because grok EXECUTES OTHER CLIS' HOOK CONFIGS. Its discovery
|
|
17
|
+
* scans `~/.claude/settings.json`, `~/.claude/settings.local.json`,
|
|
18
|
+
* `<cwd>/.claude/settings.json` and the `.cursor` equivalents by default
|
|
19
|
+
* (`[compat.claude] hooks = true`) — and `<cwd>/.claude/settings.json` is
|
|
20
|
+
* exactly the file `policies --install --cli claude --scope project` writes. So
|
|
21
|
+
* on any machine with both tools, grok runs OUR hooks and hands them `--cli
|
|
22
|
+
* claude` on the command line while piping ITS OWN camelCase payload.
|
|
23
|
+
*
|
|
24
|
+
* Verified live against grok 1.0.3: the hook fired, `tool_name` and
|
|
25
|
+
* `tool_input` were both undefined, and every builtin that reads either —
|
|
26
|
+
* block-sudo, block-env-files, block-secrets-write, block-force-push — allowed.
|
|
27
|
+
* Installed, running, costing latency, enforcing nothing; strictly worse than
|
|
28
|
+
* no coverage, because the install reports success. Detecting the envelope lets
|
|
29
|
+
* handler.ts re-route the event onto grok's path (tool maps AND response
|
|
30
|
+
* shapes — grok ignores Claude's `hookSpecificOutput.permissionDecision`, also
|
|
31
|
+
* verified by A/B).
|
|
32
|
+
*
|
|
33
|
+
* The test is on the PAYLOAD, never on an env var: `GROK_HOOK_EVENT` is set by
|
|
34
|
+
* grok's runner but is still just an env var, and mistaking a real Claude event
|
|
35
|
+
* for a grok one would break Claude's own enforcement. `workspaceRoot` +
|
|
36
|
+
* `hookEventName` with no `hook_event_name` is a shape Claude never sends.
|
|
37
|
+
*/
|
|
38
|
+
export function isGrokEnvelope(parsed: Record<string, unknown>): boolean {
|
|
39
|
+
return (
|
|
40
|
+
typeof parsed.hookEventName === "string" &&
|
|
41
|
+
typeof parsed.workspaceRoot === "string" &&
|
|
42
|
+
parsed.hook_event_name === undefined
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The CLI whose contract actually governs this event.
|
|
48
|
+
*
|
|
49
|
+
* Identical to the declared `--cli` for every integration except the grok
|
|
50
|
+
* cross-execution case above, where the flag says `claude` and the wire says
|
|
51
|
+
* grok. Returning "grok" there routes the rest of the pipeline — tool-name and
|
|
52
|
+
* tool-input canonicalization, and the response shape policy-evaluator emits —
|
|
53
|
+
* onto the contract that will actually be honored.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveEffectiveCli(
|
|
56
|
+
cli: IntegrationType,
|
|
57
|
+
parsed: Record<string, unknown>,
|
|
58
|
+
): IntegrationType {
|
|
59
|
+
if (cli === "claude" && isGrokEnvelope(parsed)) return "grok";
|
|
60
|
+
return cli;
|
|
61
|
+
}
|
|
62
|
+
|
|
13
63
|
export function normalizeCliPayload(cli: IntegrationType, parsed: Record<string, unknown>): void {
|
|
14
64
|
// Antigravity (agy) pipes a camelCase protojson payload; normalize the fields
|
|
15
65
|
// the handler downstream reads to canonical snake_case BEFORE any
|
|
@@ -47,6 +97,33 @@ export function normalizeCliPayload(cli: IntegrationType, parsed: Record<string,
|
|
|
47
97
|
}
|
|
48
98
|
}
|
|
49
99
|
|
|
100
|
+
// grok pipes a camelCase envelope (verified live against grok 1.0.3 with a
|
|
101
|
+
// recorder hook on all 14 events). Normalize the fields the handler reads to
|
|
102
|
+
// snake_case BEFORE any canonicalization runs. Note `workspaceRoot` → `cwd`:
|
|
103
|
+
// grok sends BOTH, they were identical in every capture, and `cwd` is what
|
|
104
|
+
// resolveCwd + block-read-outside-cwd need. `toolResult` is grok's PostToolUse
|
|
105
|
+
// output key — it is NOT Claude's `tool_response`, so a sanitize policy
|
|
106
|
+
// reading the documented Claude key would see nothing without this line.
|
|
107
|
+
// `hookEventName` is deliberately NOT mapped: its value is snake_case
|
|
108
|
+
// ("pre_tool_use") whereas the canonical set is PascalCase, and the `--hook`
|
|
109
|
+
// arg already carries the PascalCase name.
|
|
110
|
+
if (cli === "grok") {
|
|
111
|
+
if (typeof parsed.toolName === "string") parsed.tool_name = parsed.toolName;
|
|
112
|
+
if (parsed.toolInput !== undefined) parsed.tool_input = parsed.toolInput;
|
|
113
|
+
if (parsed.toolResult !== undefined && parsed.tool_response === undefined) {
|
|
114
|
+
parsed.tool_response = parsed.toolResult;
|
|
115
|
+
}
|
|
116
|
+
if (typeof parsed.sessionId === "string") parsed.session_id = parsed.sessionId;
|
|
117
|
+
if (typeof parsed.transcriptPath === "string") parsed.transcript_path = parsed.transcriptPath;
|
|
118
|
+
if (typeof parsed.workspaceRoot === "string" && parsed.cwd === undefined) {
|
|
119
|
+
parsed.cwd = parsed.workspaceRoot;
|
|
120
|
+
}
|
|
121
|
+
if (typeof parsed.permissionMode === "string") parsed.permission_mode = parsed.permissionMode;
|
|
122
|
+
if (typeof parsed.stopHookActive === "boolean") {
|
|
123
|
+
parsed.stop_hook_active = parsed.stopHookActive;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
50
127
|
// Goose pipes `event` / `working_dir` instead of Claude's `hook_event_name` /
|
|
51
128
|
// `cwd` (its `tool_name` / `tool_input` are already canonical field names).
|
|
52
129
|
// Normalize both so resolveCwd keeps its cwd (block-read-outside-cwd) and the
|
|
@@ -58,4 +135,53 @@ export function normalizeCliPayload(cli: IntegrationType, parsed: Record<string,
|
|
|
58
135
|
parsed.hook_event_name = parsed.event;
|
|
59
136
|
}
|
|
60
137
|
}
|
|
138
|
+
|
|
139
|
+
// Cline pipes its own payload verbatim — its hooks are event-NAMED scripts,
|
|
140
|
+
// not a generated shim that could pre-shape stdin — so without this every
|
|
141
|
+
// field the handler reads (tool_name, tool_input, session_id, cwd) is
|
|
142
|
+
// undefined and resolveCwd / block-read-outside-cwd see nothing. Verified live
|
|
143
|
+
// against cline v3.0.60 with a recorder hook on PreToolUse.
|
|
144
|
+
//
|
|
145
|
+
// `tool_call.input` is the source, NOT `preToolUse.parameters`: the latter
|
|
146
|
+
// JSON-STRINGIFIES every array value (`commands` arrives as the literal string
|
|
147
|
+
// `["echo hi"]`), so reading it would hand the batch expander a string and
|
|
148
|
+
// silently drop the entire fan-out. `preToolUse` is used only as a fallback
|
|
149
|
+
// for a payload carrying no `tool_call` at all.
|
|
150
|
+
//
|
|
151
|
+
// `hookName` is deliberately NOT mapped onto hook_event_name: its value is
|
|
152
|
+
// cline's INTERNAL name ("tool_call"), not the filename event, and the --hook
|
|
153
|
+
// argument already carries the canonical one. Same reasoning as grok's
|
|
154
|
+
// hookEventName.
|
|
155
|
+
if (cli === "cline") {
|
|
156
|
+
const tc = parsed.tool_call;
|
|
157
|
+
if (tc && typeof tc === "object" && !Array.isArray(tc)) {
|
|
158
|
+
const t = tc as Record<string, unknown>;
|
|
159
|
+
if (typeof t.name === "string") parsed.tool_name = t.name;
|
|
160
|
+
if (t.input !== undefined) parsed.tool_input = t.input;
|
|
161
|
+
}
|
|
162
|
+
const pre = parsed.preToolUse;
|
|
163
|
+
if (pre && typeof pre === "object" && !Array.isArray(pre)) {
|
|
164
|
+
const pp = pre as Record<string, unknown>;
|
|
165
|
+
if (parsed.tool_name === undefined && typeof pp.toolName === "string") {
|
|
166
|
+
parsed.tool_name = pp.toolName;
|
|
167
|
+
}
|
|
168
|
+
if (parsed.tool_input === undefined && pp.parameters !== undefined) {
|
|
169
|
+
parsed.tool_input = pp.parameters;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// taskId is the conversation ("conv_…"). sessionContext.rootSessionId and
|
|
173
|
+
// agent_id / parent_agent_id stay raw for custom policies correlating a
|
|
174
|
+
// subagent back to its root.
|
|
175
|
+
if (typeof parsed.taskId === "string") parsed.session_id = parsed.taskId;
|
|
176
|
+
const roots = parsed.workspaceRoots;
|
|
177
|
+
if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].length > 0) {
|
|
178
|
+
parsed.cwd = roots[0];
|
|
179
|
+
} else {
|
|
180
|
+
const wi = parsed.workspaceInfo;
|
|
181
|
+
if (wi && typeof wi === "object" && !Array.isArray(wi)) {
|
|
182
|
+
const rp = (wi as Record<string, unknown>).rootPath;
|
|
183
|
+
if (typeof rp === "string" && rp.length > 0) parsed.cwd = rp;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
61
187
|
}
|
|
@@ -27,7 +27,45 @@ function appendHint(baseReason: string, hint: unknown): string {
|
|
|
27
27
|
return `${base}. ${normalizedHint}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Which element of a batch this evaluation is for. Absent for every non-batch
|
|
31
|
+
* call — which is every other CLI, and every cline event that is not a
|
|
32
|
+
* fan-out tool. */
|
|
33
|
+
export interface BatchContext {
|
|
34
|
+
index: number;
|
|
35
|
+
count: number;
|
|
36
|
+
label: string;
|
|
37
|
+
degraded: boolean;
|
|
38
|
+
/** Every element's canonical input, so a batch-aware policy sees the set. */
|
|
39
|
+
all: Array<Record<string, unknown>>;
|
|
40
|
+
/** The un-expanded original tool input. */
|
|
41
|
+
raw: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface EvaluateOptions {
|
|
45
|
+
/** Per-INVOCATION telemetry dedupe set, shared across every element of a
|
|
46
|
+
* batch fan-out. Without it a 12-element batch fires 12 copies of the same
|
|
47
|
+
* policy_evaluation_error for one real tool call. */
|
|
48
|
+
dedupe?: Set<string>;
|
|
49
|
+
/** Set when this call is one element of an expanded batch. */
|
|
50
|
+
batch?: BatchContext;
|
|
51
|
+
/** Return the raw instruct/allow entries instead of shaping them; the batch
|
|
52
|
+
* combiner shapes ONCE at the end. A DENY is unaffected — it short-circuits
|
|
53
|
+
* and is returned fully shaped, because it is the answer. */
|
|
54
|
+
collectEntries?: boolean;
|
|
55
|
+
/** Skip the policy loop entirely and shape THESE entries. This is why no
|
|
56
|
+
* shaping code had to move: the existing per-CLI instruct/allow tails run in
|
|
57
|
+
* place, with the real eventType, session and payload. */
|
|
58
|
+
preEvaluated?: {
|
|
59
|
+
instructEntries: Array<{ policyName: string; reason: string }>;
|
|
60
|
+
allowEntries: Array<{ policyName: string; reason: string }>;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
30
64
|
export interface EvaluationResult {
|
|
65
|
+
/** Raw instruct entries, returned ONLY when opts.collectEntries is set. */
|
|
66
|
+
instructEntries?: Array<{ policyName: string; reason: string }>;
|
|
67
|
+
/** Raw allow-note entries, returned ONLY when opts.collectEntries is set. */
|
|
68
|
+
allowEntries?: Array<{ policyName: string; reason: string }>;
|
|
31
69
|
exitCode: number;
|
|
32
70
|
stdout: string;
|
|
33
71
|
stderr: string;
|
|
@@ -130,6 +168,7 @@ export async function evaluatePolicies(
|
|
|
130
168
|
payload: Record<string, unknown>,
|
|
131
169
|
session?: SessionMetadata,
|
|
132
170
|
config?: HooksConfig,
|
|
171
|
+
opts?: EvaluateOptions,
|
|
133
172
|
): Promise<EvaluationResult> {
|
|
134
173
|
const toolName = payload.tool_name as string | undefined;
|
|
135
174
|
const toolInput = payload.tool_input as Record<string, unknown> | undefined;
|
|
@@ -138,7 +177,7 @@ export async function evaluatePolicies(
|
|
|
138
177
|
|
|
139
178
|
hookLogInfo(`evaluating ${policies.length} policies for ${eventType}`);
|
|
140
179
|
|
|
141
|
-
if (policies.length === 0) {
|
|
180
|
+
if (policies.length === 0 && !opts?.preEvaluated) {
|
|
142
181
|
return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
|
|
143
182
|
}
|
|
144
183
|
|
|
@@ -151,13 +190,18 @@ export async function evaluatePolicies(
|
|
|
151
190
|
cli: session?.cli,
|
|
152
191
|
};
|
|
153
192
|
|
|
154
|
-
// Track all instruct results (accumulated, does not short-circuit)
|
|
155
|
-
|
|
193
|
+
// Track all instruct results (accumulated, does not short-circuit).
|
|
194
|
+
// Seeded from opts.preEvaluated on the batch combiner's final shaping call.
|
|
195
|
+
const instructEntries: Array<{ policyName: string; reason: string }> = [
|
|
196
|
+
...(opts?.preEvaluated?.instructEntries ?? []),
|
|
197
|
+
];
|
|
156
198
|
|
|
157
199
|
// Track informational messages from allow decisions (with policy attribution)
|
|
158
|
-
const allowEntries: Array<{ policyName: string; reason: string }> = [
|
|
200
|
+
const allowEntries: Array<{ policyName: string; reason: string }> = [
|
|
201
|
+
...(opts?.preEvaluated?.allowEntries ?? []),
|
|
202
|
+
];
|
|
159
203
|
|
|
160
|
-
for (const policy of policies) {
|
|
204
|
+
for (const policy of opts?.preEvaluated ? [] : policies) {
|
|
161
205
|
// Inject params: merge policyParams[policy.name] over schema defaults.
|
|
162
206
|
// policy.name is canonical (e.g. "failproofai/block-force-push"); user
|
|
163
207
|
// config keys may be flat or canonical — getConfigParamsFor accepts both.
|
|
@@ -235,7 +279,17 @@ export async function evaluatePolicies(
|
|
|
235
279
|
} else {
|
|
236
280
|
displayTool = "operation";
|
|
237
281
|
}
|
|
238
|
-
|
|
282
|
+
// One locator, built once, at the single site every CLI's deny shape
|
|
283
|
+
// interpolates — so cline's {cancel,errorMessage} names the offending
|
|
284
|
+
// element for free and nothing else needs to know a batch happened.
|
|
285
|
+
const batchLocator = opts?.batch
|
|
286
|
+
? ` [batch ${opts.batch.index + 1}/${opts.batch.count}: ${
|
|
287
|
+
opts.batch.label.length > 120
|
|
288
|
+
? `${opts.batch.label.slice(0, 117)}…`
|
|
289
|
+
: opts.batch.label
|
|
290
|
+
}]`
|
|
291
|
+
: "";
|
|
292
|
+
const blockedMessage = `Blocked ${displayTool}${batchLocator} by failproofai because: ${reason}, as per the policy configured by the user`;
|
|
239
293
|
|
|
240
294
|
// Cursor's hook protocol expects a flat `{permission, user_message,
|
|
241
295
|
// agent_message}` shape for any blocking decision, regardless of which
|
|
@@ -378,6 +432,58 @@ export async function evaluatePolicies(
|
|
|
378
432
|
};
|
|
379
433
|
}
|
|
380
434
|
|
|
435
|
+
// Ori: the generated ori feature parses a flat {permission, reason} and
|
|
436
|
+
// returns ori's own {outcome:"allow"|"deny"} to the approval extension
|
|
437
|
+
// point. There is exactly one event (PreToolUse) — ori exposes no Stop,
|
|
438
|
+
// prompt, post-tool or session gate — so unlike Pi/OpenClaw there is no
|
|
439
|
+
// Stop special case to write. The reason is carried for the operator's
|
|
440
|
+
// logs only: ori's verdict shape has no reason field, so a denial reaches
|
|
441
|
+
// the model as a bare tool failure.
|
|
442
|
+
if (session?.cli === "ori") {
|
|
443
|
+
return {
|
|
444
|
+
exitCode: 0,
|
|
445
|
+
stdout: JSON.stringify({ permission: "deny", reason: blockedMessage }),
|
|
446
|
+
stderr: "",
|
|
447
|
+
policyName: policy.name,
|
|
448
|
+
reason,
|
|
449
|
+
decision: "deny",
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Cline: the installed launcher relays this object verbatim on stdout, and
|
|
454
|
+
// cline IGNORES THE EXIT CODE — the JSON is the entire verdict. The only
|
|
455
|
+
// blocking shape its schema accepts is {cancel, errorMessage}; there is no
|
|
456
|
+
// decision/block/permissionDecision field, so every generic branch below
|
|
457
|
+
// is inert for cline and this one must come first.
|
|
458
|
+
//
|
|
459
|
+
// Stop / StopFailure are deliberately NOT a cancel: `cancel:true` becomes
|
|
460
|
+
// {stop:true} -> ControlledStopError -> the whole run aborts, and at
|
|
461
|
+
// TaskComplete the task has ALREADY finished, so that kills a completed
|
|
462
|
+
// run instead of re-entering the loop. Destructive and useless, hence the
|
|
463
|
+
// context channel there and `Stop: "observe"` in enforcement-capability.
|
|
464
|
+
if (session?.cli === "cline") {
|
|
465
|
+
if (eventType === "Stop" || eventType === "StopFailure") {
|
|
466
|
+
return {
|
|
467
|
+
exitCode: 0,
|
|
468
|
+
stdout: JSON.stringify({
|
|
469
|
+
context: `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}`,
|
|
470
|
+
}),
|
|
471
|
+
stderr: `[failproofai] ${policy.name}: ${reason}\n`,
|
|
472
|
+
policyName: policy.name,
|
|
473
|
+
reason,
|
|
474
|
+
decision: "deny",
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
exitCode: 0,
|
|
479
|
+
stdout: JSON.stringify({ cancel: true, errorMessage: blockedMessage }),
|
|
480
|
+
stderr: "",
|
|
481
|
+
policyName: policy.name,
|
|
482
|
+
reason,
|
|
483
|
+
decision: "deny",
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
381
487
|
// OpenCode: `session.idle` is a notification-only bus event — by the
|
|
382
488
|
// time the plugin handler fires, OpenCode has already gone idle and
|
|
383
489
|
// throwing from the handler does not force-retry. The only working
|
|
@@ -505,6 +611,91 @@ export async function evaluatePolicies(
|
|
|
505
611
|
};
|
|
506
612
|
}
|
|
507
613
|
|
|
614
|
+
// grok: deny is `{decision:"deny", reason}` on stdout at exit 0 — verified
|
|
615
|
+
// live against grok 1.0.3, and it beat `--yolo` (bypassPermissions). grok
|
|
616
|
+
// does NOT read Claude's hookSpecificOutput shape: the same hook emitting
|
|
617
|
+
// Claude's shape let the command run, emitting this one blocked it.
|
|
618
|
+
//
|
|
619
|
+
// Stop takes `{decision:"block", reason}` instead (Claude's turn-end
|
|
620
|
+
// vocabulary, which grok shares) — but ONLY on a real turn end. grok fires
|
|
621
|
+
// Stop a second time at session shutdown (`reason: "shutdown"`) and
|
|
622
|
+
// explicitly discards that decision, so blocking there would record
|
|
623
|
+
// enforcement that cannot happen. Falling through to allow is correct:
|
|
624
|
+
// the turn is already over. grok caps continuations at 8 per turn.
|
|
625
|
+
if (session?.cli === "grok") {
|
|
626
|
+
if (eventType === "Stop" || eventType === "SubagentStop") {
|
|
627
|
+
const grokStopReason = typeof payload.reason === "string" ? payload.reason : undefined;
|
|
628
|
+
if (eventType === "Stop" && grokStopReason && grokStopReason !== "end_turn") {
|
|
629
|
+
return {
|
|
630
|
+
exitCode: 0,
|
|
631
|
+
stdout: "",
|
|
632
|
+
stderr: "",
|
|
633
|
+
policyName: policy.name,
|
|
634
|
+
reason,
|
|
635
|
+
decision: "allow",
|
|
636
|
+
};
|
|
637
|
+
}
|
|
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
|
+
return {
|
|
640
|
+
exitCode: 0,
|
|
641
|
+
stdout: JSON.stringify({ decision: "block", reason: reasonText }),
|
|
642
|
+
stderr: "",
|
|
643
|
+
policyName: policy.name,
|
|
644
|
+
reason,
|
|
645
|
+
decision: "deny",
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
exitCode: 0,
|
|
650
|
+
stdout: JSON.stringify({ decision: "deny", reason: blockedMessage }),
|
|
651
|
+
stderr: "",
|
|
652
|
+
policyName: policy.name,
|
|
653
|
+
reason,
|
|
654
|
+
decision: "deny",
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// qwen: PreToolUse honors Claude's own
|
|
659
|
+
// `hookSpecificOutput.permissionDecision` shape (verified live — it beat
|
|
660
|
+
// `-y`), so that event deliberately falls through to the generic Claude
|
|
661
|
+
// branch below rather than being duplicated here. Stop is the divergence:
|
|
662
|
+
// it reads the top-level `{decision:"block", reason}` instead, which is
|
|
663
|
+
// what forces another turn (verified live — the agent ran the required
|
|
664
|
+
// command and only then finished). Unlike grok there is no session-end
|
|
665
|
+
// Stop fire to filter out.
|
|
666
|
+
// qwen's todo hooks (canonical TaskCreated / TaskCompleted) are the only
|
|
667
|
+
// events in the widened set that can actually veto. They read the
|
|
668
|
+
// top-level `{decision:"block", reason}` — NOT Claude's permissionDecision
|
|
669
|
+
// — and only during their `validation` phase; in `postWrite` the todo is
|
|
670
|
+
// already persisted and upstream ignores the block. Emitting it
|
|
671
|
+
// unconditionally is right: it enforces where it can and is inert where it
|
|
672
|
+
// cannot, and the phase is upstream's to decide, not ours to guess.
|
|
673
|
+
if (
|
|
674
|
+
session?.cli === "qwen" &&
|
|
675
|
+
(eventType === "TaskCreated" || eventType === "TaskCompleted")
|
|
676
|
+
) {
|
|
677
|
+
return {
|
|
678
|
+
exitCode: 0,
|
|
679
|
+
stdout: JSON.stringify({ decision: "block", reason: blockedMessage }),
|
|
680
|
+
stderr: "",
|
|
681
|
+
policyName: policy.name,
|
|
682
|
+
reason,
|
|
683
|
+
decision: "deny",
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (session?.cli === "qwen" && (eventType === "Stop" || eventType === "SubagentStop")) {
|
|
688
|
+
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.`;
|
|
689
|
+
return {
|
|
690
|
+
exitCode: 0,
|
|
691
|
+
stdout: JSON.stringify({ decision: "block", reason: reasonText }),
|
|
692
|
+
stderr: "",
|
|
693
|
+
policyName: policy.name,
|
|
694
|
+
reason,
|
|
695
|
+
decision: "deny",
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
508
699
|
if (eventType === "PreToolUse") {
|
|
509
700
|
const response = {
|
|
510
701
|
hookSpecificOutput: {
|
|
@@ -693,6 +884,21 @@ export async function evaluatePolicies(
|
|
|
693
884
|
}
|
|
694
885
|
}
|
|
695
886
|
|
|
887
|
+
// The batch combiner asked for RAW entries; it dedupes the union across all
|
|
888
|
+
// elements and shapes it once, through these same tails, on a final call.
|
|
889
|
+
if (opts?.collectEntries) {
|
|
890
|
+
return {
|
|
891
|
+
exitCode: 0,
|
|
892
|
+
stdout: "",
|
|
893
|
+
stderr: "",
|
|
894
|
+
policyName: null,
|
|
895
|
+
reason: null,
|
|
896
|
+
decision: "allow",
|
|
897
|
+
instructEntries,
|
|
898
|
+
allowEntries,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
696
902
|
// No deny — check if we accumulated any instructs
|
|
697
903
|
if (instructEntries.length > 0) {
|
|
698
904
|
const combined = instructEntries.map((e) => e.reason).join("\n");
|
|
@@ -980,6 +1186,44 @@ export async function evaluatePolicies(
|
|
|
980
1186
|
};
|
|
981
1187
|
}
|
|
982
1188
|
|
|
1189
|
+
// Ori: the approval extension points return {outcome:"allow"|"deny"} and
|
|
1190
|
+
// nothing else — there is no additional-context channel to carry a
|
|
1191
|
+
// directive, and no Stop event to convert one into a retry. So instruct()
|
|
1192
|
+
// allows the action and writes the directive to stderr for the operator's
|
|
1193
|
+
// logs; the model never sees it. Same degradation as Goose and Hermes.
|
|
1194
|
+
if (session?.cli === "ori") {
|
|
1195
|
+
const stderrMsg = instructEntries
|
|
1196
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
1197
|
+
.join("\n");
|
|
1198
|
+
return {
|
|
1199
|
+
exitCode: 0,
|
|
1200
|
+
stdout: "",
|
|
1201
|
+
stderr: stderrMsg + "\n",
|
|
1202
|
+
policyName: policyNames[0],
|
|
1203
|
+
policyNames,
|
|
1204
|
+
reason: combined,
|
|
1205
|
+
decision: "instruct",
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Cline HAS a real additional-context channel — `context` is in its verdict
|
|
1210
|
+
// schema and parseHookControl reads it — so unlike Goose/Hermes/ori this
|
|
1211
|
+
// does not have to degrade to a stderr note. It is mirrored to stderr as
|
|
1212
|
+
// well: the field is verified-PARSED by cline, but that the model actually
|
|
1213
|
+
// sees it is unprobed, and the operator's log should not depend on that.
|
|
1214
|
+
if (session?.cli === "cline") {
|
|
1215
|
+
return {
|
|
1216
|
+
exitCode: 0,
|
|
1217
|
+
stdout: JSON.stringify({ context: `Instruction from failproofai: ${combined}` }),
|
|
1218
|
+
stderr:
|
|
1219
|
+
instructEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join("\n") + "\n",
|
|
1220
|
+
policyName: policyNames[0],
|
|
1221
|
+
policyNames,
|
|
1222
|
+
reason: combined,
|
|
1223
|
+
decision: "instruct",
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
|
|
983
1227
|
if (eventType === "Stop" || eventType === "SubagentStop") {
|
|
984
1228
|
// Stop/SubagentStop instruct: exitCode 2 + stderr forces Claude to retry
|
|
985
1229
|
// the agent (or subagent) loop with the reason as context. Same widening
|
|
@@ -1102,6 +1346,53 @@ export async function evaluatePolicies(
|
|
|
1102
1346
|
};
|
|
1103
1347
|
}
|
|
1104
1348
|
|
|
1349
|
+
// grok: instruct has a channel only at the turn boundary. On Stop we reuse
|
|
1350
|
+
// the verified `{decision:"block", reason}` force-retry shape so the
|
|
1351
|
+
// instruction actually reaches the model (grok also documents a softer
|
|
1352
|
+
// `hookSpecificOutput.additionalContext` for Stop, but that path is not
|
|
1353
|
+
// verified and "block" is). The `end_turn` guard is the same one the deny
|
|
1354
|
+
// branch uses — the session-shutdown fire has no turn left to instruct.
|
|
1355
|
+
// On every other event grok has no additional-context channel, so instruct
|
|
1356
|
+
// degrades to allow + a stderr note (like Hermes/Goose/Factory); emitting
|
|
1357
|
+
// Claude's hookSpecificOutput there would be a shape grok discards.
|
|
1358
|
+
//
|
|
1359
|
+
// qwen deliberately has NO branch here: it honors Claude's
|
|
1360
|
+
// `hookSpecificOutput.additionalContext` on PreToolUse/PostToolUse/
|
|
1361
|
+
// UserPromptSubmit (wrapping it in a `<qwen:user-prompt-submit-context>`
|
|
1362
|
+
// provenance tag), so the generic path below is already correct for it.
|
|
1363
|
+
if (session?.cli === "grok") {
|
|
1364
|
+
const stderrMsg = allowEntries
|
|
1365
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
1366
|
+
.join("\n");
|
|
1367
|
+
const grokStopReason = typeof payload.reason === "string" ? payload.reason : undefined;
|
|
1368
|
+
const atRealTurnEnd =
|
|
1369
|
+
eventType === "SubagentStop" ||
|
|
1370
|
+
(eventType === "Stop" && (!grokStopReason || grokStopReason === "end_turn"));
|
|
1371
|
+
if (atRealTurnEnd) {
|
|
1372
|
+
return {
|
|
1373
|
+
exitCode: 0,
|
|
1374
|
+
stdout: JSON.stringify({
|
|
1375
|
+
decision: "block",
|
|
1376
|
+
reason: `Instruction from failproofai: ${combined}`,
|
|
1377
|
+
}),
|
|
1378
|
+
stderr: stderrMsg + "\n",
|
|
1379
|
+
policyName: policyNames[0],
|
|
1380
|
+
policyNames,
|
|
1381
|
+
reason: combined,
|
|
1382
|
+
decision: "instruct",
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
exitCode: 0,
|
|
1387
|
+
stdout: "",
|
|
1388
|
+
stderr: stderrMsg + "\n",
|
|
1389
|
+
policyName: policyNames[0],
|
|
1390
|
+
policyNames,
|
|
1391
|
+
reason: combined,
|
|
1392
|
+
decision: "allow",
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1105
1396
|
const supportsHookSpecificOutput =
|
|
1106
1397
|
eventType === "PreToolUse" ||
|
|
1107
1398
|
eventType === "PostToolUse" ||
|
|
@@ -31,6 +31,8 @@ import { findCodexTranscript } from "../../lib/codex-sessions";
|
|
|
31
31
|
import { findCopilotTranscript } from "../../lib/copilot-sessions";
|
|
32
32
|
import { findCursorTranscript } from "../../lib/cursor-sessions";
|
|
33
33
|
import { findPiTranscript } from "../../lib/pi-sessions";
|
|
34
|
+
import { findGrokTranscript } from "../../lib/grok-sessions";
|
|
35
|
+
import { findQwenTranscript } from "../../lib/qwen-sessions";
|
|
34
36
|
import { findFactoryTranscript } from "../../lib/factory-sessions";
|
|
35
37
|
import { findAntigravityTranscript } from "../../lib/antigravity-sessions";
|
|
36
38
|
import type { IntegrationType } from "./types";
|
|
@@ -74,6 +76,14 @@ export function resolveTranscriptPath(
|
|
|
74
76
|
// its live-hook payload carries no transcript file, so hand back a virtual
|
|
75
77
|
// path (like devin) — audit/download read the DB directly.
|
|
76
78
|
return `goose-db://${sessionId}`;
|
|
79
|
+
case "grok":
|
|
80
|
+
// grok writes real JSONL at ~/.grok/sessions/<percent-encoded-cwd>/
|
|
81
|
+
// <sessionId>/chat_history.jsonl.
|
|
82
|
+
return findGrokTranscript(sessionId) ?? undefined;
|
|
83
|
+
case "qwen":
|
|
84
|
+
// qwen writes real JSONL at ~/.qwen/projects/<encoded-cwd>/chats/
|
|
85
|
+
// <sessionId>.jsonl (note the extra `chats/` level vs Claude/Factory).
|
|
86
|
+
return findQwenTranscript(sessionId) ?? undefined;
|
|
77
87
|
case "opencode":
|
|
78
88
|
return `opencode-db://${sessionId}`;
|
|
79
89
|
case "hermes":
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-risk regexes, shared by the builtins that enforce on them and by the
|
|
3
|
+
* batch-collapse fallback that has to decide WHICH path survives a collapse.
|
|
4
|
+
*
|
|
5
|
+
* They live here rather than in builtin-policies.ts for two reasons:
|
|
6
|
+
*
|
|
7
|
+
* 1. A second hand-maintained copy is exactly how a bypass gets reintroduced:
|
|
8
|
+
* someone tightens `SECRET_FILE_RE` in the builtin and the collapse
|
|
9
|
+
* fallback keeps probing the old shape, so the riskiest path stops being
|
|
10
|
+
* picked and a `.pem` write rides through on a batch.
|
|
11
|
+
* 2. `tool-name-canonicalize.ts` is on the AUDIT path, and builtin-policies.ts
|
|
12
|
+
* imports `node:child_process`. Importing the builtins from there to reach
|
|
13
|
+
* four regexes would drag the policy engine into transcript replay.
|
|
14
|
+
*
|
|
15
|
+
* Zero imports, on purpose. Keep it that way.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** `.env`, `.env.local`, `path/to/.env` — but not `something.environment`. */
|
|
19
|
+
export const ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
|
|
20
|
+
|
|
21
|
+
/** NOTE the `$` anchor: this can only ever match at the END of a string, which
|
|
22
|
+
* is why joining a list of paths and testing the join is a SILENT BYPASS —
|
|
23
|
+
* only the last element could match. See pickRiskiestPath in batch-expand.ts. */
|
|
24
|
+
export const SECRET_FILE_RE = /\.(?:pem|key)$/;
|
|
25
|
+
export const SECRET_FILE_ID_RSA_RE = /id_rsa/;
|
|
26
|
+
export const SECRET_FILE_CREDENTIALS_RE = /credentials/;
|
|
@@ -25,7 +25,14 @@ import {
|
|
|
25
25
|
ANTIGRAVITY_TOOL_INPUT_MAP,
|
|
26
26
|
GOOSE_TOOL_MAP,
|
|
27
27
|
GOOSE_TOOL_INPUT_MAP,
|
|
28
|
+
GROK_TOOL_MAP,
|
|
29
|
+
GROK_TOOL_INPUT_MAP,
|
|
30
|
+
QWEN_TOOL_MAP,
|
|
31
|
+
ORI_TOOL_MAP,
|
|
32
|
+
ORI_TOOL_INPUT_MAP,
|
|
33
|
+
CLINE_TOOL_MAP,
|
|
28
34
|
} from "./types";
|
|
35
|
+
import { canonicalizeClineToolInput, applyPatchFilePaths } from "./batch-expand";
|
|
29
36
|
|
|
30
37
|
/**
|
|
31
38
|
* Canonicalize a per-CLI tool name to the Claude PascalCase form that builtin
|
|
@@ -56,6 +63,18 @@ export function canonicalizeToolName(
|
|
|
56
63
|
// Goose: shell→Bash, write/edit/view→file ops, todo__todo_write→TodoWrite, …
|
|
57
64
|
// (verified live against goose v1.43.0). Handles bare + `<ext>__<tool>` names.
|
|
58
65
|
if (cli === "goose") return GOOSE_TOOL_MAP[raw] ?? raw;
|
|
66
|
+
// grok: run_terminal_command→Bash, write/read_file/search_replace→file ops, …
|
|
67
|
+
// (every entry observed on the wire against grok 1.0.3).
|
|
68
|
+
if (cli === "grok") return GROK_TOOL_MAP[raw] ?? raw;
|
|
69
|
+
// qwen: run_shell_command→Bash, write_file/read_file/edit→file ops, …
|
|
70
|
+
// (verified live against qwen-code 0.21.12).
|
|
71
|
+
if (cli === "qwen") return QWEN_TOOL_MAP[raw] ?? raw;
|
|
72
|
+
// Ori: tool ids arrive lowercase (bash/read/write/edit/glob/grep) — all six
|
|
73
|
+
// captured at the live approval gate on ori 0.12.0+68f9a36.
|
|
74
|
+
if (cli === "ori") return ORI_TOOL_MAP[raw] ?? raw;
|
|
75
|
+
// Cline: run_commands/read_files/search_codebase/apply_patch — all four
|
|
76
|
+
// observed live on cline v3.0.60's PreToolUse payload.
|
|
77
|
+
if (cli === "cline") return CLINE_TOOL_MAP[raw] ?? raw;
|
|
59
78
|
return raw;
|
|
60
79
|
}
|
|
61
80
|
|
|
@@ -95,6 +114,26 @@ export function canonicalizeToolInput(
|
|
|
95
114
|
// Goose file tools (write/edit/view) deliver the path as `path`, read_image as
|
|
96
115
|
// `source`; map to `file_path` so path builtins fire (verified goose v1.43.0).
|
|
97
116
|
else if (cli === "goose") perToolMap = GOOSE_TOOL_INPUT_MAP[toolName];
|
|
117
|
+
// grok's read_file delivers `target_file` and list_dir `target_directory`;
|
|
118
|
+
// every other grok tool is already canonical. Without the Read entry a live
|
|
119
|
+
// `.env` read walks past block-env-files (verified grok 1.0.3). qwen needs no
|
|
120
|
+
// entry at all — all six of its tools deliver canonical keys.
|
|
121
|
+
else if (cli === "grok") perToolMap = GROK_TOOL_INPUT_MAP[toolName];
|
|
122
|
+
// Ori: read/write deliver the path as `path`; bash/glob/grep are already
|
|
123
|
+
// canonical. `edit` is special — its path lives INSIDE the patch blob and is
|
|
124
|
+
// derived below rather than renamed, because there is no key to rename.
|
|
125
|
+
else if (cli === "ori") {
|
|
126
|
+
if (toolName === "Edit") return canonicalizeOriEditInput(rawInput as Record<string, unknown>);
|
|
127
|
+
perToolMap = ORI_TOOL_INPUT_MAP[toolName];
|
|
128
|
+
}
|
|
129
|
+
// Cline has NO key-rename map: every tool is batch- or blob-shaped, so this
|
|
130
|
+
// is a value-SHAPE transform. It is the SAFETY NET for the paths that do not
|
|
131
|
+
// fan out (PostToolUse, audit replay, fail-closed shaping); PreToolUse
|
|
132
|
+
// enforces through expandBatchToolInput instead, because a collapse cannot
|
|
133
|
+
// make an anchored regex match every element.
|
|
134
|
+
else if (cli === "cline") {
|
|
135
|
+
return canonicalizeClineToolInput(toolName, rawInput as Record<string, unknown>);
|
|
136
|
+
}
|
|
98
137
|
if (!perToolMap) return rawInput;
|
|
99
138
|
const out: Record<string, unknown> = {};
|
|
100
139
|
for (const [k, v] of Object.entries(rawInput as Record<string, unknown>)) {
|
|
@@ -102,3 +141,28 @@ export function canonicalizeToolInput(
|
|
|
102
141
|
}
|
|
103
142
|
return out;
|
|
104
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Recover the file path(s) from an OpenAI apply_patch blob.
|
|
147
|
+
*
|
|
148
|
+
* Kept as the ori-facing NAME so existing callers and
|
|
149
|
+
* __tests__/hooks/ori-canonicalize.test.ts keep working, but the implementation
|
|
150
|
+
* now lives in batch-expand.ts and is shared: ori's `edit` and cline's
|
|
151
|
+
* `apply_patch` carry byte-identical blobs, and one implementation is what
|
|
152
|
+
* stops a third CLI adding a third regex.
|
|
153
|
+
*
|
|
154
|
+
* NOTE for a follow-up: `splitApplyPatch` (same module) now returns per-file
|
|
155
|
+
* old/new text as well, which closes ori's documented multi-file KNOWN GAP —
|
|
156
|
+
* and a worse one the gap comment understates, that ori's Edit sets NO
|
|
157
|
+
* old_string/new_string at all, so block-secrets-write can never fire on it.
|
|
158
|
+
* Deliberately NOT changed here: ori's behaviour stays as shipped in this PR.
|
|
159
|
+
*/
|
|
160
|
+
export const oriPatchFilePaths = applyPatchFilePaths;
|
|
161
|
+
|
|
162
|
+
function canonicalizeOriEditInput(input: Record<string, unknown>): Record<string, unknown> {
|
|
163
|
+
const patch = input.patch;
|
|
164
|
+
if (typeof patch !== "string") return input;
|
|
165
|
+
const paths = oriPatchFilePaths(patch);
|
|
166
|
+
if (paths.length === 0) return input;
|
|
167
|
+
return { ...input, file_path: paths[0], ori_patch_files: paths };
|
|
168
|
+
}
|