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
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expand a CLI's BATCH tool call into per-element canonical scalar inputs.
|
|
3
|
+
*
|
|
4
|
+
* failproofai's builtins read SCALARS — `tool_input.command`, `.file_path`,
|
|
5
|
+
* `.pattern`, `.old_string`. Cline delivers ARRAYS: `run_commands{commands[]}`,
|
|
6
|
+
* `read_files{files[]}`, `search_codebase{queries[]}`, plus one multi-file blob
|
|
7
|
+
* in `apply_patch{input}`. With only a key rename, `block-sudo`,
|
|
8
|
+
* `block-env-files`, `block-secrets-write`, `block-force-push` and
|
|
9
|
+
* `block-read-outside-cwd` all read `undefined` and ALLOW SILENTLY — the
|
|
10
|
+
* inert-hook failure this repo has already shipped twice (grok, and our own
|
|
11
|
+
* Claude hooks running inside grok).
|
|
12
|
+
*
|
|
13
|
+
* Joining the array into one string is NOT a fix, and the reason is specific:
|
|
14
|
+
* `SECRET_FILE_RE` is `/\.(?:pem|key)$/`. Under any join only the LAST element
|
|
15
|
+
* can ever match, so a `.pem` at `files[0]` rides straight through. That is why
|
|
16
|
+
* this expands and the evaluator runs the policy set once per element.
|
|
17
|
+
*
|
|
18
|
+
* PURE: no evaluator import, no fs, no policy registry — so the audit path can
|
|
19
|
+
* call it without dragging the policy engine along.
|
|
20
|
+
*
|
|
21
|
+
* CONTAINMENT: returns null for every CLI except cline and every tool except
|
|
22
|
+
* cline's four containers. `null` means "take the existing single-shot path,
|
|
23
|
+
* byte-for-byte unchanged", so this cannot regress the other 15 integrations.
|
|
24
|
+
*/
|
|
25
|
+
import type { IntegrationType } from "./types";
|
|
26
|
+
import {
|
|
27
|
+
ENV_FILE_PATH_RE,
|
|
28
|
+
SECRET_FILE_RE,
|
|
29
|
+
SECRET_FILE_ID_RSA_RE,
|
|
30
|
+
SECRET_FILE_CREDENTIALS_RE,
|
|
31
|
+
} from "./risk-patterns";
|
|
32
|
+
|
|
33
|
+
export interface FanoutElement {
|
|
34
|
+
/** Canonical SCALAR tool input: {command} | {file_path,…} | {pattern} |
|
|
35
|
+
* {file_path, old_string, new_string, patch}. */
|
|
36
|
+
input: Record<string, unknown>;
|
|
37
|
+
/** Short human locator; goes into the deny message and the activity row. */
|
|
38
|
+
label: string;
|
|
39
|
+
index: number;
|
|
40
|
+
/** True when this element is a COLLAPSE of several that the cap or the
|
|
41
|
+
* wall-clock budget refused to run apart. A weaker guarantee we state
|
|
42
|
+
* explicitly, never a silent skip. */
|
|
43
|
+
degraded?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BatchExpansion {
|
|
47
|
+
/** Canonical tool name (Bash | Read | Grep | Edit). */
|
|
48
|
+
tool: string;
|
|
49
|
+
elements: FanoutElement[];
|
|
50
|
+
/** The un-expanded original, so a batch-aware policy still sees the whole call. */
|
|
51
|
+
raw: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The separator used wherever a command list still has to collapse to one
|
|
56
|
+
* string. `" &&\n"` is DERIVED, not chosen:
|
|
57
|
+
* • `"\n"` alone silently disables `READ_LIKE_CMDS` — its boundary
|
|
58
|
+
* alternation `(?:^|;|&&|\|\||\|)` contains no newline, so a batched
|
|
59
|
+
* `cat /etc/passwd` stops looking read-like and `block-read-outside-cwd`
|
|
60
|
+
* never inspects it.
|
|
61
|
+
* • `" && "` alone manufactures a FALSE deny: a `.`-based lookahead can cross
|
|
62
|
+
* the boundary and fire on two commands neither of which triggers it alone.
|
|
63
|
+
* `" &&\n"` keeps every `&&`-aware segmenter alive while the newline stops
|
|
64
|
+
* `.`-based lookaheads crossing a boundary.
|
|
65
|
+
*/
|
|
66
|
+
export const BATCH_JOIN = " &&\n";
|
|
67
|
+
|
|
68
|
+
function omit(o: Record<string, unknown>, key: string): Record<string, unknown> {
|
|
69
|
+
const { [key]: _dropped, ...rest } = o;
|
|
70
|
+
return rest;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function expandBatchToolInput(
|
|
74
|
+
cli: IntegrationType,
|
|
75
|
+
toolName: string | undefined,
|
|
76
|
+
rawInput: unknown,
|
|
77
|
+
): BatchExpansion | null {
|
|
78
|
+
if (cli !== "cline" || !toolName) return null;
|
|
79
|
+
if (!rawInput || typeof rawInput !== "object" || Array.isArray(rawInput)) return null;
|
|
80
|
+
const raw = rawInput as Record<string, unknown>;
|
|
81
|
+
|
|
82
|
+
// An expansion to ZERO elements would evaluate nothing and report a clean
|
|
83
|
+
// allow — the exact inert-hook failure this exists to prevent. So an empty or
|
|
84
|
+
// mis-shaped container returns null and falls back to the collapse safety net
|
|
85
|
+
// in canonicalizeClineToolInput, which at least still reads SOMETHING.
|
|
86
|
+
const mk = (
|
|
87
|
+
parts: Array<{ input: Record<string, unknown>; label: string }>,
|
|
88
|
+
): BatchExpansion | null =>
|
|
89
|
+
parts.length === 0
|
|
90
|
+
? null
|
|
91
|
+
: { tool: toolName, raw, elements: parts.map((e, index) => ({ ...e, index })) };
|
|
92
|
+
|
|
93
|
+
switch (toolName) {
|
|
94
|
+
// run_commands {commands: ["cd '<dir>' && ls -la", …]}
|
|
95
|
+
case "Bash": {
|
|
96
|
+
const cmds = raw.commands;
|
|
97
|
+
if (!Array.isArray(cmds)) return null;
|
|
98
|
+
return mk(
|
|
99
|
+
cmds
|
|
100
|
+
.filter((c): c is string => typeof c === "string")
|
|
101
|
+
.map((c) => ({ input: { ...omit(raw, "commands"), command: c }, label: c })),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
// read_files {files: [{path, start_line, …}, …]}
|
|
105
|
+
case "Read": {
|
|
106
|
+
const files = raw.files;
|
|
107
|
+
if (!Array.isArray(files)) return null;
|
|
108
|
+
return mk(
|
|
109
|
+
files.flatMap((f) => {
|
|
110
|
+
if (!f || typeof f !== "object" || Array.isArray(f)) return [];
|
|
111
|
+
const e = f as Record<string, unknown>;
|
|
112
|
+
if (typeof e.path !== "string") return [];
|
|
113
|
+
// Spread the entry so start_line/limit survive for policies that read
|
|
114
|
+
// them; `path` is KEPT too, because a cline-aware custom policy may
|
|
115
|
+
// already be reading it.
|
|
116
|
+
return [{ input: { ...e, file_path: e.path }, label: e.path }];
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
// search_codebase {queries: ["alpha", …]}
|
|
121
|
+
case "Grep": {
|
|
122
|
+
const qs = raw.queries;
|
|
123
|
+
if (!Array.isArray(qs)) return null;
|
|
124
|
+
return mk(
|
|
125
|
+
qs
|
|
126
|
+
.filter((q): q is string => typeof q === "string")
|
|
127
|
+
.map((q) => ({ input: { ...omit(raw, "queries"), pattern: q }, label: q })),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
// apply_patch {input: "*** Begin Patch …"} — byte-identical to the OpenAI
|
|
131
|
+
// apply_patch blob ori's `edit` tool carries, so this splitter is
|
|
132
|
+
// back-portable to ori and closes ori's documented multi-file KNOWN GAP.
|
|
133
|
+
case "Edit": {
|
|
134
|
+
const blob = raw.input;
|
|
135
|
+
if (typeof blob !== "string") return null;
|
|
136
|
+
return mk(
|
|
137
|
+
splitApplyPatch(blob).map((s) => ({
|
|
138
|
+
input: {
|
|
139
|
+
file_path: s.path,
|
|
140
|
+
old_string: s.oldText,
|
|
141
|
+
new_string: s.newText,
|
|
142
|
+
patch: s.subPatch,
|
|
143
|
+
cline_patch_op: s.op,
|
|
144
|
+
},
|
|
145
|
+
label: `${s.op} ${s.path}`,
|
|
146
|
+
})),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
default:
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface PatchSection {
|
|
155
|
+
op: "add" | "update" | "delete" | "move";
|
|
156
|
+
path: string;
|
|
157
|
+
oldText: string;
|
|
158
|
+
newText: string;
|
|
159
|
+
/** A standalone, re-emittable patch for just this file. */
|
|
160
|
+
subPatch: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const FILE_OP_RE = /^\*\*\* (Add File|Update File|Delete File|Move to): (.+)$/;
|
|
164
|
+
const OPS: Record<string, PatchSection["op"]> = {
|
|
165
|
+
"Add File": "add",
|
|
166
|
+
"Update File": "update",
|
|
167
|
+
"Delete File": "delete",
|
|
168
|
+
"Move to": "move",
|
|
169
|
+
};
|
|
170
|
+
const HEADERS: Record<PatchSection["op"], string> = {
|
|
171
|
+
add: "Add File",
|
|
172
|
+
update: "Update File",
|
|
173
|
+
delete: "Delete File",
|
|
174
|
+
move: "Move to",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
interface OpenSection {
|
|
178
|
+
op: PatchSection["op"];
|
|
179
|
+
path: string;
|
|
180
|
+
body: string[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function build(cur: OpenSection, op: PatchSection["op"], path: string): PatchSection {
|
|
184
|
+
const minus: string[] = [];
|
|
185
|
+
const plus: string[] = [];
|
|
186
|
+
for (const l of cur.body) {
|
|
187
|
+
if (l.startsWith("-")) minus.push(l.slice(1));
|
|
188
|
+
else if (l.startsWith("+")) plus.push(l.slice(1));
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
op,
|
|
192
|
+
path,
|
|
193
|
+
oldText: minus.join("\n"),
|
|
194
|
+
newText: plus.join("\n"),
|
|
195
|
+
subPatch: ["*** Begin Patch", `*** ${HEADERS[op]}: ${path}`, ...cur.body, "*** End Patch"].join("\n"),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Split an OpenAI apply_patch blob into one section per file it touches.
|
|
201
|
+
*
|
|
202
|
+
* Serves BOTH cline's `apply_patch` and ori's `edit` — the format is identical,
|
|
203
|
+
* and one implementation is what stops a third CLI adding a third regex.
|
|
204
|
+
*/
|
|
205
|
+
export function splitApplyPatch(patch: string): PatchSection[] {
|
|
206
|
+
const out: PatchSection[] = [];
|
|
207
|
+
let cur: OpenSection | null = null;
|
|
208
|
+
|
|
209
|
+
for (const line of patch.split("\n")) {
|
|
210
|
+
const m = FILE_OP_RE.exec(line);
|
|
211
|
+
if (m) {
|
|
212
|
+
const op = OPS[m[1]!]!;
|
|
213
|
+
const path = m[2]!.trim();
|
|
214
|
+
if (op === "move") {
|
|
215
|
+
// `Move to:` is a RENAME MODIFIER on the preceding Update File, not an
|
|
216
|
+
// independent op. We emit it as its OWN element anyway — deliberately —
|
|
217
|
+
// so a rename INTO `.env` is seen by the path builtins. It inherits the
|
|
218
|
+
// in-progress body so old/new_string stay right, and the Update element
|
|
219
|
+
// is still emitted at the next flush. Cost: one rename yields two
|
|
220
|
+
// elements. That is the trade: a duplicate row, never a missed path.
|
|
221
|
+
// Flush the section being renamed FIRST, then emit the move, so paths
|
|
222
|
+
// come out in literal file order — `Update File: a` + `Move to: b`
|
|
223
|
+
// yields [a, b], never [b, a]. Getting this backwards silently changes
|
|
224
|
+
// which path `pickRiskiestPath` and `file_path = paths[0]` land on.
|
|
225
|
+
if (cur) {
|
|
226
|
+
out.push(build(cur, cur.op, cur.path));
|
|
227
|
+
out.push(build(cur, "move", path));
|
|
228
|
+
cur = null;
|
|
229
|
+
} else
|
|
230
|
+
out.push({
|
|
231
|
+
op: "move",
|
|
232
|
+
path,
|
|
233
|
+
oldText: "",
|
|
234
|
+
newText: "",
|
|
235
|
+
subPatch: `*** Begin Patch\n*** Move to: ${path}\n*** End Patch`,
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (cur) out.push(build(cur, cur.op, cur.path));
|
|
240
|
+
cur = { op, path, body: [] };
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (line.startsWith("*** ")) continue; // Begin Patch / End Patch / End of File
|
|
244
|
+
if (cur) cur.body.push(line);
|
|
245
|
+
}
|
|
246
|
+
if (cur) out.push(build(cur, cur.op, cur.path));
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Every path an apply_patch blob touches, in file order. */
|
|
251
|
+
export function applyPatchFilePaths(patch: string): string[] {
|
|
252
|
+
return splitApplyPatch(patch).map((s) => s.path);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* When a path list must still collapse to ONE (the overflow tail, and the
|
|
257
|
+
* non-fan-out safety net), pick the one a builtin is most likely to deny on
|
|
258
|
+
* rather than element [0]. The probes IMPORT the real builtin regexes — a
|
|
259
|
+
* second hand-maintained copy is how a bypass gets reintroduced by a builtin
|
|
260
|
+
* edit nobody thought to mirror. Falls back to paths[0] when nothing matches.
|
|
261
|
+
*
|
|
262
|
+
* NOT the primary mechanism, and it cannot be: it runs before params are bound,
|
|
263
|
+
* so `block-secrets-write`'s `additionalPatterns` and `block-read-outside-cwd`'s
|
|
264
|
+
* `allowPaths` have no influence on it, and neither does any custom policy.
|
|
265
|
+
* That is exactly why PreToolUse fans out instead of relying on this.
|
|
266
|
+
*/
|
|
267
|
+
export function pickRiskiestPath(paths: string[]): string | undefined {
|
|
268
|
+
if (paths.length === 0) return undefined;
|
|
269
|
+
for (const re of [
|
|
270
|
+
ENV_FILE_PATH_RE,
|
|
271
|
+
SECRET_FILE_RE,
|
|
272
|
+
SECRET_FILE_ID_RSA_RE,
|
|
273
|
+
SECRET_FILE_CREDENTIALS_RE,
|
|
274
|
+
]) {
|
|
275
|
+
const hit = paths.find((p) => re.test(p));
|
|
276
|
+
if (hit) return hit;
|
|
277
|
+
}
|
|
278
|
+
return paths[0];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Everything past the cap / budget, joined into ONE degraded element. It is
|
|
282
|
+
* still EVALUATED — a weaker guarantee we can state honestly, never a silent
|
|
283
|
+
* skip. */
|
|
284
|
+
export function collapseElements(tool: string, els: FanoutElement[]): FanoutElement {
|
|
285
|
+
const first = els[0]!;
|
|
286
|
+
const base = { label: `${els.length} collapsed`, index: first.index, degraded: true as const };
|
|
287
|
+
switch (tool) {
|
|
288
|
+
case "Bash":
|
|
289
|
+
return {
|
|
290
|
+
...base,
|
|
291
|
+
input: {
|
|
292
|
+
...first.input,
|
|
293
|
+
command: els.map((e) => String(e.input.command ?? "")).join(BATCH_JOIN),
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
case "Read":
|
|
297
|
+
case "Edit": {
|
|
298
|
+
const paths = els.map((e) => String(e.input.file_path ?? "")).filter(Boolean);
|
|
299
|
+
return {
|
|
300
|
+
...base,
|
|
301
|
+
input: { ...first.input, file_path: pickRiskiestPath(paths), cline_collapsed_paths: paths },
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
case "Grep":
|
|
305
|
+
return {
|
|
306
|
+
...base,
|
|
307
|
+
input: { ...first.input, pattern: els.map((e) => String(e.input.pattern ?? "")).join("|") },
|
|
308
|
+
};
|
|
309
|
+
default:
|
|
310
|
+
return { ...first, ...base };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Cline → canonical, for every path that does NOT fan out: PostToolUse, audit
|
|
316
|
+
* replay, fail-closed shaping, and a policy reading `ctx.payload`. Derives the
|
|
317
|
+
* best single scalar and PRESERVES the arrays under `cline_*` so a batch-aware
|
|
318
|
+
* policy can still see everything.
|
|
319
|
+
*
|
|
320
|
+
* This is a SAFETY NET, not the enforcement mechanism: a collapse cannot make
|
|
321
|
+
* an anchored regex match every element, which is precisely why PreToolUse fans
|
|
322
|
+
* out instead. `pickRiskiestPath` is what makes the net worth having — element
|
|
323
|
+
* [0] would silently miss a `.env` at `files[1]`.
|
|
324
|
+
*/
|
|
325
|
+
export function canonicalizeClineToolInput(
|
|
326
|
+
toolName: string,
|
|
327
|
+
input: Record<string, unknown>,
|
|
328
|
+
): Record<string, unknown> {
|
|
329
|
+
switch (toolName) {
|
|
330
|
+
case "Bash": {
|
|
331
|
+
const cs = input.commands;
|
|
332
|
+
if (!Array.isArray(cs)) return input;
|
|
333
|
+
const commands = cs.filter((c): c is string => typeof c === "string");
|
|
334
|
+
if (commands.length === 0) return input;
|
|
335
|
+
return { ...omit(input, "commands"), command: commands.join(BATCH_JOIN), cline_commands: commands };
|
|
336
|
+
}
|
|
337
|
+
case "Read": {
|
|
338
|
+
const files = input.files;
|
|
339
|
+
if (!Array.isArray(files)) return input;
|
|
340
|
+
const paths = files
|
|
341
|
+
.map((f) =>
|
|
342
|
+
f && typeof f === "object" && !Array.isArray(f)
|
|
343
|
+
? (f as Record<string, unknown>).path
|
|
344
|
+
: undefined,
|
|
345
|
+
)
|
|
346
|
+
.filter((p): p is string => typeof p === "string");
|
|
347
|
+
if (paths.length === 0) return input;
|
|
348
|
+
return {
|
|
349
|
+
...omit(input, "files"),
|
|
350
|
+
file_path: pickRiskiestPath(paths),
|
|
351
|
+
cline_files: files,
|
|
352
|
+
cline_file_paths: paths,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
case "Grep": {
|
|
356
|
+
const qs = input.queries;
|
|
357
|
+
if (!Array.isArray(qs)) return input;
|
|
358
|
+
const queries = qs.filter((q): q is string => typeof q === "string");
|
|
359
|
+
if (queries.length === 0) return input;
|
|
360
|
+
return { ...omit(input, "queries"), pattern: queries.join("|"), cline_queries: queries };
|
|
361
|
+
}
|
|
362
|
+
case "Edit": {
|
|
363
|
+
const blob = input.input;
|
|
364
|
+
if (typeof blob !== "string") return input;
|
|
365
|
+
const sections = splitApplyPatch(blob);
|
|
366
|
+
if (sections.length === 0) return input;
|
|
367
|
+
const paths = sections.map((s) => s.path);
|
|
368
|
+
return {
|
|
369
|
+
...input,
|
|
370
|
+
patch: blob,
|
|
371
|
+
file_path: pickRiskiestPath(paths),
|
|
372
|
+
old_string: sections.map((s) => s.oldText).join("\n"),
|
|
373
|
+
new_string: sections.map((s) => s.newText).join("\n"),
|
|
374
|
+
cline_patch_files: paths,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
default:
|
|
378
|
+
return input;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run the policy set once per element of an expanded batch tool call.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists rather than a cleverer canonicalization: cline's tools carry
|
|
5
|
+
* ARRAYS, and collapsing an array into one scalar cannot preserve an ANCHORED
|
|
6
|
+
* regex. `SECRET_FILE_RE` is `/\.(?:pem|key)$/`, so under any join only the last
|
|
7
|
+
* path can match and a `.pem` at `files[0]` rides straight through — a silent
|
|
8
|
+
* allow, which is the failure mode this repo has already shipped twice.
|
|
9
|
+
*
|
|
10
|
+
* Combination rules:
|
|
11
|
+
* all allow, no notes → plain allow, identical to a non-batch call
|
|
12
|
+
* any deny → LOWEST element index wins, returned verbatim,
|
|
13
|
+
* short-circuiting the rest
|
|
14
|
+
* no deny, >=1 instruct → union deduped by (policy, reason), shaped ONCE
|
|
15
|
+
* no deny/instruct, >=1 note→ same
|
|
16
|
+
*
|
|
17
|
+
* The deny is returned verbatim because `evaluatePolicies` already shaped it for
|
|
18
|
+
* the calling CLI and already embedded the element locator (it received
|
|
19
|
+
* `opts.batch`). No re-shaping, and therefore nothing to keep in sync with the
|
|
20
|
+
* 16 per-CLI deny shapes.
|
|
21
|
+
*/
|
|
22
|
+
import { evaluatePolicies, type EvaluationResult } from "./policy-evaluator";
|
|
23
|
+
import { collapseElements, type BatchExpansion, type FanoutElement } from "./batch-expand";
|
|
24
|
+
import { hookLogWarn } from "./hook-logger";
|
|
25
|
+
import type { HookEventType, SessionMetadata } from "./types";
|
|
26
|
+
import type { HooksConfig } from "./policy-types";
|
|
27
|
+
|
|
28
|
+
/** A shape guard, not the real one: 32 elements of registered policies sits
|
|
29
|
+
* comfortably inside daemon-client.ts's 30s RESPONSE budget. The wall clock
|
|
30
|
+
* below is what actually protects that budget. */
|
|
31
|
+
const MAX_ELEMENTS = 32;
|
|
32
|
+
const WALL_CLOCK_BUDGET_MS = 5_000;
|
|
33
|
+
|
|
34
|
+
export interface BatchOutcome {
|
|
35
|
+
size: number;
|
|
36
|
+
evaluated: number;
|
|
37
|
+
decidedIndex: number | null;
|
|
38
|
+
decidedLabel: string | null;
|
|
39
|
+
degraded: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Entry = { policyName: string; reason: string };
|
|
43
|
+
|
|
44
|
+
/** Policy P saying the SAME thing about elements 3 and 7 contributes one line;
|
|
45
|
+
* P saying two different things contributes both. Keyed on (name, reason). */
|
|
46
|
+
function pushDeduped(into: Entry[], from: Entry[] | undefined): void {
|
|
47
|
+
for (const e of from ?? []) {
|
|
48
|
+
const k = `${e.policyName}\t${e.reason}`;
|
|
49
|
+
if (!into.some((x) => `${x.policyName}\t${x.reason}` === k)) into.push(e);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function evaluateExpandedBatch(
|
|
54
|
+
eventType: HookEventType,
|
|
55
|
+
payload: Record<string, unknown>,
|
|
56
|
+
session: SessionMetadata | undefined,
|
|
57
|
+
config: HooksConfig | undefined,
|
|
58
|
+
expansion: BatchExpansion,
|
|
59
|
+
dedupe: Set<string>,
|
|
60
|
+
): Promise<EvaluationResult & { batch: BatchOutcome }> {
|
|
61
|
+
const started = Date.now();
|
|
62
|
+
|
|
63
|
+
let queue: FanoutElement[] =
|
|
64
|
+
expansion.elements.length > MAX_ELEMENTS
|
|
65
|
+
? [
|
|
66
|
+
...expansion.elements.slice(0, MAX_ELEMENTS),
|
|
67
|
+
collapseElements(expansion.tool, expansion.elements.slice(MAX_ELEMENTS)),
|
|
68
|
+
]
|
|
69
|
+
: [...expansion.elements];
|
|
70
|
+
|
|
71
|
+
const instructs: Entry[] = [];
|
|
72
|
+
const allows: Entry[] = [];
|
|
73
|
+
let evaluated = 0;
|
|
74
|
+
let sawDegraded = false;
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < queue.length; i++) {
|
|
77
|
+
// Checked BETWEEN elements only — a policy already inside its own timeout
|
|
78
|
+
// race is not interruptible from here.
|
|
79
|
+
if (i > 0 && Date.now() - started > WALL_CLOCK_BUDGET_MS) {
|
|
80
|
+
const rest = queue.slice(i);
|
|
81
|
+
hookLogWarn(
|
|
82
|
+
`batch fan-out budget exceeded after ${i}/${queue.length} elements; the remaining ` +
|
|
83
|
+
`${rest.length} are being evaluated COLLAPSED — anchored patterns may not match`,
|
|
84
|
+
);
|
|
85
|
+
queue = [...queue.slice(0, i), collapseElements(expansion.tool, rest)];
|
|
86
|
+
}
|
|
87
|
+
const el = queue[i]!;
|
|
88
|
+
sawDegraded = sawDegraded || !!el.degraded;
|
|
89
|
+
|
|
90
|
+
const r = await evaluatePolicies(
|
|
91
|
+
eventType,
|
|
92
|
+
{ ...payload, tool_input: el.input },
|
|
93
|
+
session,
|
|
94
|
+
config,
|
|
95
|
+
{
|
|
96
|
+
dedupe,
|
|
97
|
+
collectEntries: true,
|
|
98
|
+
batch: {
|
|
99
|
+
index: el.index,
|
|
100
|
+
count: expansion.elements.length,
|
|
101
|
+
label: el.label,
|
|
102
|
+
degraded: !!el.degraded,
|
|
103
|
+
all: expansion.elements.map((e) => e.input),
|
|
104
|
+
raw: expansion.raw,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
evaluated++;
|
|
109
|
+
|
|
110
|
+
if (r.decision === "deny") {
|
|
111
|
+
return {
|
|
112
|
+
...r,
|
|
113
|
+
batch: {
|
|
114
|
+
size: expansion.elements.length,
|
|
115
|
+
evaluated,
|
|
116
|
+
decidedIndex: el.index,
|
|
117
|
+
decidedLabel: el.label,
|
|
118
|
+
degraded: sawDegraded,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
pushDeduped(instructs, r.instructEntries);
|
|
123
|
+
pushDeduped(allows, r.allowEntries);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// One shaping pass over the deduped union, through the SAME per-CLI tails
|
|
127
|
+
// every other integration uses — no shaping code was extracted to get here.
|
|
128
|
+
const shaped = await evaluatePolicies(eventType, payload, session, config, {
|
|
129
|
+
dedupe,
|
|
130
|
+
preEvaluated: { instructEntries: instructs, allowEntries: allows },
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
...shaped,
|
|
134
|
+
batch: {
|
|
135
|
+
size: expansion.elements.length,
|
|
136
|
+
evaluated,
|
|
137
|
+
decidedIndex: null,
|
|
138
|
+
decidedLabel: null,
|
|
139
|
+
degraded: sawDegraded,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -16,6 +16,12 @@ import { POLICY_CATALOG } from "./policy-catalog";
|
|
|
16
16
|
import { allow, deny, instruct } from "./policy-helpers";
|
|
17
17
|
import { normalizePolicyName, registerPolicy } from "./policy-registry";
|
|
18
18
|
import { hookLogWarn } from "./hook-logger";
|
|
19
|
+
import {
|
|
20
|
+
ENV_FILE_PATH_RE,
|
|
21
|
+
SECRET_FILE_RE,
|
|
22
|
+
SECRET_FILE_ID_RSA_RE,
|
|
23
|
+
SECRET_FILE_CREDENTIALS_RE,
|
|
24
|
+
} from "./risk-patterns";
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
27
|
* Whether `resolved` lives under an agent CLI's home directory
|
|
@@ -192,7 +198,6 @@ const DOTNET_GETENV_RE = /\[Environment\]::GetEnvironment/i;
|
|
|
192
198
|
const CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
|
|
193
199
|
|
|
194
200
|
// blockEnvFiles
|
|
195
|
-
const ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
|
|
196
201
|
const ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
|
|
197
202
|
|
|
198
203
|
// blockSudo
|
|
@@ -444,9 +449,6 @@ const SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
|
|
|
444
449
|
const SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"] as const;
|
|
445
450
|
|
|
446
451
|
// blockSecretsWrite
|
|
447
|
-
const SECRET_FILE_RE = /\.(?:pem|key)$/;
|
|
448
|
-
const SECRET_FILE_ID_RSA_RE = /id_rsa/;
|
|
449
|
-
const SECRET_FILE_CREDENTIALS_RE = /credentials/;
|
|
450
452
|
|
|
451
453
|
// blockWorkOnMain
|
|
452
454
|
const GIT_COMMIT_MERGE_RE = /git\s+(commit|merge|rebase|cherry-pick)\b/;
|
|
@@ -253,6 +253,103 @@ export const ENFORCEMENT_CAPABILITY: Record<
|
|
|
253
253
|
// the policy-evaluator comment both claim "Goose has NO Stop event"; that is
|
|
254
254
|
// false at v1.43.0 (agent.rs:1956, :2840 emit_stop_hook_blocking).
|
|
255
255
|
},
|
|
256
|
+
|
|
257
|
+
// grok 1.0.3 (1a29d5bc12). Every "block" row below was proven by a live
|
|
258
|
+
// probe, not read from grok's hooks doc — which is also where two of the
|
|
259
|
+
// three surprises came from (see types.ts): the doc's own tool name for the
|
|
260
|
+
// shell tool disagrees with the wire, and nothing documents that project
|
|
261
|
+
// hooks need a git repo. grok's capability advertisement corroborates the
|
|
262
|
+
// blocking set: its ACP initialize response carries
|
|
263
|
+
// x.ai/hooks: {blockingEvents:["pre_tool_use","stop","subagent_stop"],
|
|
264
|
+
// decisions:["deny","block"], stopSignals:[…]}
|
|
265
|
+
// — exactly the three rows marked "block" here.
|
|
266
|
+
grok: {
|
|
267
|
+
PreToolUse: "block", // VERIFIED live: {decision:"deny",reason} blocked `echo` and beat --yolo (permissionMode bypassPermissions); Claude's hookSpecificOutput shape did NOT block (A/B on the same hook)
|
|
268
|
+
Stop: "block", // VERIFIED live: {decision:"block",reason} forced another turn — the agent ran the required command, then stopped. ONLY on reason==="end_turn"; the session-shutdown fire is parsed and discarded upstream. Cap: 8 continuations/turn
|
|
269
|
+
SubagentStop: "block", // advertised in x.ai/hooks blockingEvents; not exercised by a probe
|
|
270
|
+
UserPromptSubmit: "observe", // fired live; not in blockingEvents
|
|
271
|
+
PostToolUse: "observe", // fired live; post-hoc, and not in blockingEvents
|
|
272
|
+
PostToolUseFailure: "observe",
|
|
273
|
+
SessionStart: "observe", // fired live
|
|
274
|
+
SessionEnd: "observe", // fired live
|
|
275
|
+
// The six below are the widened set. All OBSERVE by construction, not by
|
|
276
|
+
// caution: blockingEvents lists exactly three events, so grok cannot honour
|
|
277
|
+
// a deny on any of these no matter what we send.
|
|
278
|
+
StopFailure: "observe", // VERIFIED firing (a 429 turn): {error:"rate_limit", errorDetails, lastAssistantMessage}
|
|
279
|
+
Notification: "observe", // VERIFIED firing: notificationType "agent_error" — a type grok's own docs do not list
|
|
280
|
+
PermissionDenied: "observe", // config key accepted (count=14); firing not exercised — needs a real permission denial
|
|
281
|
+
SubagentStart: "observe", // config key accepted; firing not exercised — needs a subagent
|
|
282
|
+
PreCompact: "observe", // config key accepted; firing not exercised — needs compaction
|
|
283
|
+
PostCompact: "observe", // config key accepted; firing not exercised — needs compaction
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// qwen-code 0.21.12. A near-pure Claude clone on the wire, so most rows
|
|
287
|
+
// inherit Claude's semantics — but only the two proven ones are "block".
|
|
288
|
+
qwen: {
|
|
289
|
+
PreToolUse: "block", // VERIFIED live: hookSpecificOutput.permissionDecision:"deny" blocked run_shell_command and beat -y (yolo); reason reached the model verbatim. ("ask" degrades to deny in headless and background subagents)
|
|
290
|
+
Stop: "block", // VERIFIED live: top-level {decision:"block",reason} forced another turn. NOTE stop_hook_active is true on the FIRST fire, so it is not a usable loop guard here
|
|
291
|
+
UserPromptSubmit: "observe", // docs accept a decision, unverified — and it fires per MODEL INVOCATION (4× in one observed turn), so treat as observation until probed
|
|
292
|
+
PostToolUse: "observe", // docs accept `decision`, unverified; post-hoc regardless
|
|
293
|
+
PostToolUseFailure: "observe",
|
|
294
|
+
PermissionRequest: "observe",
|
|
295
|
+
PermissionDenied: "observe",
|
|
296
|
+
SubagentStart: "observe",
|
|
297
|
+
SubagentStop: "observe", // sibling of Stop upstream, but not probed — left honest rather than assumed
|
|
298
|
+
PreCompact: "observe",
|
|
299
|
+
SessionStart: "observe", // fired live
|
|
300
|
+
SessionEnd: "observe",
|
|
301
|
+
// Widened set. Every one has a real executeHooks() dispatch site in the
|
|
302
|
+
// shipped bundle; the three marked VERIFIED were also observed firing.
|
|
303
|
+
TaskCreated: "block", // qwen's TodoCreated. VERIFIED firing ×4 with phase:"validation", where upstream docs state a {decision:"block"} PREVENTS the write and returns the reason to the model
|
|
304
|
+
TaskCompleted: "block", // qwen's TodoCompleted. VERIFIED firing ×4, same validation-phase contract
|
|
305
|
+
InstructionsLoaded: "observe", // VERIFIED firing: {file_path, memory_type, load_reason}
|
|
306
|
+
Notification: "observe", // VERIFIED firing: notification_type "auth_success"
|
|
307
|
+
StopFailure: "observe", // upstream documents it fire-and-forget — output and exit code ignored
|
|
308
|
+
PostCompact: "observe",
|
|
309
|
+
UserPromptExpansion: "observe",
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
// ── ori ───────────────────────────────────────────────────────────────────
|
|
313
|
+
// ori 0.12.0+68f9a36, probed LIVE against the built-in `@ori-runloop/
|
|
314
|
+
// agent-loop` harness driving nvidia/nemotron-3.5-lightning:free. Enforcement
|
|
315
|
+
// is the `approval-asker` / `unattended-approvals` extension points rather
|
|
316
|
+
// than a hook-event stream, so PreToolUse is the only row there can be: ori
|
|
317
|
+
// exposes no prompt-submit, post-tool, session or stop gate to subscribe to.
|
|
318
|
+
ori: {
|
|
319
|
+
PreToolUse: "block", // LIVE: {outcome:"deny"} from our approval-asker provision turned 3 tool.started into 3 tool.failed with ZERO tool.succeeded, and ori held the deny across the model's retries. MODE-GATED — read the note below before quoting this row
|
|
320
|
+
// DELIBERATELY NO OTHER ROWS. Two things this table cannot express, both
|
|
321
|
+
// verified live and both load-bearing:
|
|
322
|
+
// 1. ori's approval mode defaults to `self-drive`, and in that mode the
|
|
323
|
+
// DYNAMIC points are never called — so no deny is ever PRODUCED, and
|
|
324
|
+
// the row above never gets a chance to apply. Isolated three ways: no
|
|
325
|
+
// callback under self-drive; still none after claiming approval-policy
|
|
326
|
+
// with defaultAction:"ask"; but defaultAction:"reject" DID block every
|
|
327
|
+
// call, which proves the static point is wired and that self-drive
|
|
328
|
+
// skips specifically the dynamic asker. Coverage therefore requires
|
|
329
|
+
// `--approvals manual` (or /approvals in the TUI).
|
|
330
|
+
// 2. There is no Stop event at all, so the 5 require-*-before-stop
|
|
331
|
+
// builtins are INAPPLICABLE on ori, exactly as on Hermes and Goose.
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
// ── cline ─────────────────────────────────────────────────────────────────
|
|
335
|
+
// cline v3.0.60. Its hooks are event-NAMED scripts under a hooks directory,
|
|
336
|
+
// and THE EXIT CODE IS IGNORED on every event — so every row here is a claim
|
|
337
|
+
// about a stdout SHAPE, never about exit 2. cline's verdict schema has no
|
|
338
|
+
// `decision` / `block` / `permissionDecision` field at all, which is why every
|
|
339
|
+
// generic branch in policy-evaluator.ts is inert for it.
|
|
340
|
+
cline: {
|
|
341
|
+
PreToolUse: "block", // LIVE: {"cancel":true,"errorMessage":…} on stdout stopped a real run_commands call and the side effect never happened. CAVEAT, and it is a product decision rather than a footnote: `cancel` becomes {stop:true} -> applyStopControl THROWS ControlledStopError, ABORTING THE WHOLE RUN ("[abort] aborted by another client"), not just this tool call. A STRONGER action than a per-tool deny, not a weaker one
|
|
342
|
+
Stop: "observe", // TaskComplete maps here, and `cancel` is NOT a force-retry channel: the task has already completed, so a cancel kills a finished run rather than re-entering the loop. The 5 require-*-before-stop builtins are INAPPLICABLE on cline, exactly as on Hermes, Goose and ori
|
|
343
|
+
// DELIBERATELY NO OTHER ROWS. PostToolUse, UserPromptSubmit, TaskStart,
|
|
344
|
+
// TaskResume, TaskCancel, TaskError and SessionShutdown ARE installed — for
|
|
345
|
+
// the audit trail and for observe-mode custom policies — but nothing has
|
|
346
|
+
// been probed showing a verdict on any of them changes what the agent does,
|
|
347
|
+
// and ABSENT MEANS UNKNOWN in this file.
|
|
348
|
+
//
|
|
349
|
+
// FAIL-OPEN across the whole surface, and unlike ori there is no
|
|
350
|
+
// failureBehavior:"deny" to inherit: a timeout (120s default), a parse
|
|
351
|
+
// failure or a spawn error makes cline SKIP the hook and run the tool.
|
|
352
|
+
},
|
|
256
353
|
};
|
|
257
354
|
|
|
258
355
|
/**
|