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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* is agent-agnostic — only install/uninstall plumbing varies.
|
|
8
8
|
*/
|
|
9
9
|
import { execSync } from "node:child_process";
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, rmSync, chmodSync } from "node:fs";
|
|
11
11
|
import { resolve, dirname } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { homedir } from "node:os";
|
|
@@ -38,6 +38,14 @@ import {
|
|
|
38
38
|
ANTIGRAVITY_HOOK_EVENT_TYPES,
|
|
39
39
|
ANTIGRAVITY_HOOK_SCOPES,
|
|
40
40
|
GOOSE_HOOK_EVENT_TYPES,
|
|
41
|
+
GROK_HOOK_EVENT_TYPES,
|
|
42
|
+
GROK_HOOK_SCOPES,
|
|
43
|
+
QWEN_HOOK_EVENT_TYPES,
|
|
44
|
+
QWEN_HOOK_SCOPES,
|
|
45
|
+
ORI_HOOK_SCOPES,
|
|
46
|
+
ORI_HOOK_EVENT_TYPES,
|
|
47
|
+
CLINE_HOOK_SCOPES,
|
|
48
|
+
CLINE_HOOK_EVENT_TYPES,
|
|
41
49
|
GOOSE_HOOK_SCOPES,
|
|
42
50
|
FAILPROOFAI_HOOK_MARKER,
|
|
43
51
|
INTEGRATION_TYPES,
|
|
@@ -2275,13 +2283,755 @@ export const goose: Integration = {
|
|
|
2275
2283
|
},
|
|
2276
2284
|
};
|
|
2277
2285
|
|
|
2278
|
-
|
|
2286
|
+
|
|
2287
|
+
// ── Ori (OpenRouter's ori) ──────────────────────────────────────────────────
|
|
2288
|
+
//
|
|
2289
|
+
// Enforces on ori's OWN agent (bare `ori` / `ori code`), NOT on its launcher
|
|
2290
|
+
// path — `ori claude`, `ori codex`, `ori grok`, `ori opencode` and friends run
|
|
2291
|
+
// the real third-party binary with credentials injected and no config-dir
|
|
2292
|
+
// redirect, so the hooks already installed for those CLIs keep enforcing there
|
|
2293
|
+
// with nothing ori-specific required. See the ORI section of types.ts for the
|
|
2294
|
+
// live evidence behind that.
|
|
2295
|
+
//
|
|
2296
|
+
// A feature is a workspace package under the global workspace's `features/`,
|
|
2297
|
+
// AUTO-DISCOVERED with no config file to register it in — so, like Goose, the
|
|
2298
|
+
// install is "drop a directory" rather than "edit a settings file". The
|
|
2299
|
+
// bookkeeping JSON below exists purely so the Integration interface has a
|
|
2300
|
+
// settings path to read, write and detect against; ori itself ignores it.
|
|
2301
|
+
//
|
|
2302
|
+
// USER scope only: bare `ori` boots the GLOBAL workspace rather than the
|
|
2303
|
+
// project's, so one install covers every project and a project-scope install
|
|
2304
|
+
// would never be loaded.
|
|
2305
|
+
export const ORI_FEATURE_ID = "failproofai";
|
|
2306
|
+
|
|
2307
|
+
/** ori's home (override with ORI_HOME, used by tests). */
|
|
2308
|
+
export function oriHome(): string {
|
|
2309
|
+
return process.env.ORI_HOME || resolve(homedir(), ".ori");
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/** The feature directory failproofai generates inside ori's global workspace. */
|
|
2313
|
+
export function oriFeatureDir(): string {
|
|
2314
|
+
return resolve(oriHome(), "global", "features", ORI_FEATURE_ID);
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
/**
|
|
2318
|
+
* The ori feature shim.
|
|
2319
|
+
*
|
|
2320
|
+
* Claims all three published approval extension points. Each is `unique`, so
|
|
2321
|
+
* leaving one unclaimed invites another feature to take it — but the STATIC one
|
|
2322
|
+
* is deliberately inert (`defaultAction: "ask"`, which is what self-drive
|
|
2323
|
+
* already does). Claiming it with a blanket `reject` would brick every
|
|
2324
|
+
* self-drive session the moment failproofai is installed.
|
|
2325
|
+
*
|
|
2326
|
+
* FAIL-CLOSED BY DEFAULT, and this is the one integration where that is free:
|
|
2327
|
+
* ori declares `failureBehavior: "deny"` on both dynamic points — "a throwing,
|
|
2328
|
+
* rejecting, or malformed provider denies the request" — so letting an error
|
|
2329
|
+
* propagate IS the deny. Set FAILPROOFAI_ORI_FAIL_OPEN=1 to allow instead, for
|
|
2330
|
+
* anyone who would rather a failproofai fault not stop their agent.
|
|
2331
|
+
*/
|
|
2332
|
+
function buildOriFeatureShim(binaryPath: string): string {
|
|
2333
|
+
return `// AUTO-GENERATED by failproofai. ${FAILPROOFAI_HOOK_MARKER}
|
|
2334
|
+
// Re-generate via: failproofai policies --install --cli ori
|
|
2335
|
+
//
|
|
2336
|
+
// Bridges ori's approval extension points to the failproofai binary. Ships NO
|
|
2337
|
+
// inline tool maps: it forwards ori's raw tool name and arguments, and the
|
|
2338
|
+
// binary canonicalizes them via ORI_TOOL_MAP / ORI_TOOL_INPUT_MAP (one source
|
|
2339
|
+
// of truth), exactly as the OpenClaw plugin does.
|
|
2340
|
+
import { spawn } from "node:child_process";
|
|
2341
|
+
|
|
2342
|
+
import { ApprovalAskers, ApprovalPolicies, UnattendedApprovals } from "ori";
|
|
2343
|
+
|
|
2344
|
+
const BIN = ${JSON.stringify(binaryPath)};
|
|
2345
|
+
const FAIL_OPEN = process.env.FAILPROOFAI_ORI_FAIL_OPEN === "1";
|
|
2346
|
+
|
|
2347
|
+
/** ori hands arguments as a flat {name, value} string array. Rebuild the object
|
|
2348
|
+
* the binary's tool-input canonicalization expects. The synthetic
|
|
2349
|
+
* \`escalated\` entry ori appends on the second pass of its escalation ladder
|
|
2350
|
+
* is metadata, not a tool argument, so it is dropped from the tool input. */
|
|
2351
|
+
function toToolInput(request) {
|
|
2352
|
+
const out = {};
|
|
2353
|
+
for (const a of request.arguments ?? []) {
|
|
2354
|
+
if (request.escalated && a.name === "escalated" && a.value === "true") continue;
|
|
2355
|
+
out[a.name] = a.value;
|
|
2356
|
+
}
|
|
2357
|
+
return out;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
function callPolicy(request) {
|
|
2361
|
+
return new Promise((done, fail) => {
|
|
2362
|
+
let child;
|
|
2363
|
+
try {
|
|
2364
|
+
child = spawn("node", [BIN, "--hook", "PreToolUse", "--cli", "ori"], {
|
|
2365
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
2366
|
+
});
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
FAIL_OPEN ? done({ permission: "allow" }) : fail(err);
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
let out = "";
|
|
2372
|
+
let settled = false;
|
|
2373
|
+
const finish = (fn, arg) => {
|
|
2374
|
+
if (settled) return;
|
|
2375
|
+
settled = true;
|
|
2376
|
+
clearTimeout(timer);
|
|
2377
|
+
fn(arg);
|
|
2378
|
+
};
|
|
2379
|
+
const timer = setTimeout(() => {
|
|
2380
|
+
try { child.kill(); } catch { /* already gone */ }
|
|
2381
|
+
FAIL_OPEN
|
|
2382
|
+
? finish(done, { permission: "allow" })
|
|
2383
|
+
: finish(fail, new Error("failproofai timed out"));
|
|
2384
|
+
}, 30_000);
|
|
2385
|
+
child.stdout.on("data", (d) => { out += d; });
|
|
2386
|
+
child.on("error", (err) => {
|
|
2387
|
+
FAIL_OPEN ? finish(done, { permission: "allow" }) : finish(fail, err);
|
|
2388
|
+
});
|
|
2389
|
+
child.on("close", (code, signal) => {
|
|
2390
|
+
// A non-zero exit or a signal means failproofai produced NO verdict — a
|
|
2391
|
+
// missing bundle, a crashed evaluator, an OOM kill. Resolving that to
|
|
2392
|
+
// "allow" is exactly the silent non-enforcement ori's
|
|
2393
|
+
// failureBehavior deny exists to prevent, and it could never fire,
|
|
2394
|
+
// because resolving is what tells ori the provider succeeded. Only a
|
|
2395
|
+
// REJECT reaches that guarantee.
|
|
2396
|
+
if (signal || (typeof code === "number" && code !== 0)) {
|
|
2397
|
+
const err = new Error(
|
|
2398
|
+
"failproofai produced no verdict (" +
|
|
2399
|
+
(signal ? "killed by " + signal : "exit code " + code) +
|
|
2400
|
+
")",
|
|
2401
|
+
);
|
|
2402
|
+
FAIL_OPEN ? finish(done, { permission: "allow" }) : finish(fail, err);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
const stdout = out.trim();
|
|
2406
|
+
// Exit 0 with empty stdout IS the evaluator's clean-allow shape — the one
|
|
2407
|
+
// case where no output is a real verdict rather than a failure.
|
|
2408
|
+
if (!stdout) { finish(done, { permission: "allow" }); return; }
|
|
2409
|
+
let verdict;
|
|
2410
|
+
try {
|
|
2411
|
+
verdict = JSON.parse(stdout);
|
|
2412
|
+
} catch (err) {
|
|
2413
|
+
FAIL_OPEN ? finish(done, { permission: "allow" }) : finish(fail, err);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
// Anything that is not an explicit allow/deny is an unrecognised answer,
|
|
2417
|
+
// not an allow. decide() maps every non-"deny" to allow, so an
|
|
2418
|
+
// unvalidated object would silently become one.
|
|
2419
|
+
if (!verdict || (verdict.permission !== "allow" && verdict.permission !== "deny")) {
|
|
2420
|
+
const err = new Error("failproofai returned an unrecognised verdict: " + stdout.slice(0, 200));
|
|
2421
|
+
FAIL_OPEN ? finish(done, { permission: "allow" }) : finish(fail, err);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
finish(done, verdict);
|
|
2425
|
+
});
|
|
2426
|
+
try {
|
|
2427
|
+
child.stdin.end(JSON.stringify({
|
|
2428
|
+
hook_event_name: "PreToolUse",
|
|
2429
|
+
tool_name: request.tool,
|
|
2430
|
+
tool_input: toToolInput(request),
|
|
2431
|
+
cwd: process.cwd(),
|
|
2432
|
+
ori: {
|
|
2433
|
+
capabilities: request.capabilities ?? [],
|
|
2434
|
+
escalated: request.escalated === true,
|
|
2435
|
+
},
|
|
2436
|
+
}));
|
|
2437
|
+
} catch (err) {
|
|
2438
|
+
FAIL_OPEN ? finish(done, { permission: "allow" }) : finish(fail, err);
|
|
2439
|
+
}
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
async function decide(request) {
|
|
2444
|
+
const verdict = await callPolicy(request);
|
|
2445
|
+
return { outcome: verdict.permission === "deny" ? "deny" : "allow" };
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
export const provisions = [
|
|
2449
|
+
// Claimed so no other feature can take it; inert on purpose — see above.
|
|
2450
|
+
ApprovalPolicies.provide({ defaultAction: "ask", rules: [] }),
|
|
2451
|
+
// The gate that fires when a human is present (\`ori --approvals manual\`).
|
|
2452
|
+
ApprovalAskers.provide(decide),
|
|
2453
|
+
// The gate that fires on unattended runs.
|
|
2454
|
+
UnattendedApprovals.provide(decide),
|
|
2455
|
+
];
|
|
2456
|
+
`;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
const ORI_FEATURE_PACKAGE_JSON = `${JSON.stringify(
|
|
2460
|
+
{ name: "failproofai", private: true, type: "module" },
|
|
2461
|
+
null,
|
|
2462
|
+
2,
|
|
2463
|
+
)}\n`;
|
|
2464
|
+
|
|
2465
|
+
export const ori: Integration = {
|
|
2466
|
+
id: "ori",
|
|
2467
|
+
displayName: "Ori",
|
|
2468
|
+
scopes: ORI_HOOK_SCOPES,
|
|
2469
|
+
eventTypes: ORI_HOOK_EVENT_TYPES,
|
|
2470
|
+
|
|
2471
|
+
// USER scope only — ori has no project workspace that bare `ori` would boot.
|
|
2472
|
+
// The path is bookkeeping for this interface, not something ori reads.
|
|
2473
|
+
getSettingsPath() {
|
|
2474
|
+
return resolve(oriFeatureDir(), "failproofai.json");
|
|
2475
|
+
},
|
|
2476
|
+
|
|
2477
|
+
readSettings(settingsPath) {
|
|
2478
|
+
return readJsonFile(settingsPath);
|
|
2479
|
+
},
|
|
2480
|
+
|
|
2481
|
+
writeSettings(settingsPath, settings) {
|
|
2482
|
+
writeJsonFile(settingsPath, settings);
|
|
2483
|
+
},
|
|
2484
|
+
|
|
2485
|
+
buildHookEntry(binaryPath, eventType) {
|
|
2486
|
+
return { type: "command", command: `"${binaryPath}" --hook ${eventType} --cli ori`, [FAILPROOFAI_HOOK_MARKER]: true };
|
|
2487
|
+
},
|
|
2488
|
+
|
|
2489
|
+
isFailproofaiHook(hook) {
|
|
2490
|
+
if (!hook || typeof hook !== "object") return false;
|
|
2491
|
+
return (hook as Record<string, unknown>)[FAILPROOFAI_HOOK_MARKER] === true;
|
|
2492
|
+
},
|
|
2493
|
+
|
|
2494
|
+
/**
|
|
2495
|
+
* Install: write the feature (feature.ts + package.json) into ori's global
|
|
2496
|
+
* workspace, and record the entry in the bookkeeping JSON. ori discovers the
|
|
2497
|
+
* directory on its own — there is no config array to append to.
|
|
2498
|
+
*/
|
|
2499
|
+
writeHookEntries(settings, binaryPath) {
|
|
2500
|
+
const dir = oriFeatureDir();
|
|
2501
|
+
mkdirSync(dir, { recursive: true });
|
|
2502
|
+
writeFileSync(resolve(dir, "feature.ts"), buildOriFeatureShim(binaryPath), "utf8");
|
|
2503
|
+
writeFileSync(resolve(dir, "package.json"), ORI_FEATURE_PACKAGE_JSON, "utf8");
|
|
2504
|
+
|
|
2505
|
+
const s = settings as Record<string, unknown>;
|
|
2506
|
+
s.hooks = ORI_HOOK_EVENT_TYPES.map((eventType) =>
|
|
2507
|
+
this.buildHookEntry(binaryPath, eventType, "user"),
|
|
2508
|
+
);
|
|
2509
|
+
},
|
|
2510
|
+
|
|
2511
|
+
/**
|
|
2512
|
+
* Uninstall: drop the whole feature directory, but ONLY when the generated
|
|
2513
|
+
* feature carries our marker — never delete a hand-written ori feature that
|
|
2514
|
+
* happens to sit at the same path.
|
|
2515
|
+
*/
|
|
2516
|
+
removeHooksFromFile(settingsPath) {
|
|
2517
|
+
const dir = dirname(settingsPath);
|
|
2518
|
+
const featurePath = resolve(dir, "feature.ts");
|
|
2519
|
+
let removed = 0;
|
|
2520
|
+
|
|
2521
|
+
const settings = this.readSettings(settingsPath) as Record<string, unknown>;
|
|
2522
|
+
if (Array.isArray(settings.hooks)) {
|
|
2523
|
+
removed = settings.hooks.filter((h) => this.isFailproofaiHook(h)).length;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
const ours =
|
|
2527
|
+
existsSync(featurePath) &&
|
|
2528
|
+
readFileSync(featurePath, "utf8").includes(FAILPROOFAI_HOOK_MARKER);
|
|
2529
|
+
if (ours) {
|
|
2530
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2531
|
+
return removed || 1;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// Someone replaced the feature by hand — leave it, drop only our record.
|
|
2535
|
+
delete settings.hooks;
|
|
2536
|
+
this.writeSettings(settingsPath, settings);
|
|
2537
|
+
return removed;
|
|
2538
|
+
},
|
|
2539
|
+
|
|
2540
|
+
hooksInstalledInSettings() {
|
|
2541
|
+
const featurePath = resolve(oriFeatureDir(), "feature.ts");
|
|
2542
|
+
if (!existsSync(featurePath)) return false;
|
|
2543
|
+
try {
|
|
2544
|
+
return readFileSync(featurePath, "utf8").includes(FAILPROOFAI_HOOK_MARKER);
|
|
2545
|
+
} catch {
|
|
2546
|
+
return false;
|
|
2547
|
+
}
|
|
2548
|
+
},
|
|
2549
|
+
|
|
2550
|
+
detectInstalled() {
|
|
2551
|
+
return binaryExists("ori");
|
|
2552
|
+
},
|
|
2553
|
+
};
|
|
2554
|
+
|
|
2555
|
+
|
|
2556
|
+
// ── Cline (cline CLI) ───────────────────────────────────────────────────────
|
|
2557
|
+
//
|
|
2558
|
+
// Cline's config is a DIRECTORY OF EVENT-NAMED FILES, not a settings file — the
|
|
2559
|
+
// only integration shaped that way. There is no JSON to merge and no array to
|
|
2560
|
+
// append to: the install drops one launcher script per event, named exactly for
|
|
2561
|
+
// the event it subscribes to.
|
|
2562
|
+
//
|
|
2563
|
+
// Two rules from the live contract govern the whole install (cline v3.0.60):
|
|
2564
|
+
// 1. FILENAME == EVENT, case-insensitively, and only ten names exist. A typo
|
|
2565
|
+
// is not an error, it is SILENCE — which is exactly how an earlier probe
|
|
2566
|
+
// concluded, wrongly, that cline had no reachable hook surface.
|
|
2567
|
+
// 2. The EXTENSION must be in cline's allowlist or the file is skipped, also
|
|
2568
|
+
// silently. We write `.sh` with a matching shebang so the interpreter is
|
|
2569
|
+
// unambiguous whichever rule cline applies (shebang first, else extension).
|
|
2570
|
+
//
|
|
2571
|
+
// THE CRITICAL DIFFERENCE FROM ORI: ori owns its entire feature directory, so
|
|
2572
|
+
// uninstall can remove it wholesale. Cline's hooks directory is SHARED with the
|
|
2573
|
+
// user's own hooks, so uninstall deletes individual files carrying our marker
|
|
2574
|
+
// and MUST NEVER remove the directory — deleting someone's hand-written
|
|
2575
|
+
// `PreToolUse.py` because it sat at the same path is unrecoverable.
|
|
2576
|
+
//
|
|
2577
|
+
// NEVER emit `--hooks-dir`: that flag writes CLINE_HOOKS_DIR, which appears
|
|
2578
|
+
// exactly once in cline's shipped binary — the write — and is read by nothing.
|
|
2579
|
+
|
|
2580
|
+
/** The extension we write. In cline's allowlist, and paired with a shebang. */
|
|
2581
|
+
export const CLINE_HOOK_FILE_EXTENSION = ".sh";
|
|
2582
|
+
|
|
2583
|
+
/** cline's home (CLINE_DIR is cline's own var; CLINE_HOME is for tests). */
|
|
2584
|
+
export function clineIntegrationHome(): string {
|
|
2585
|
+
return process.env.CLINE_HOME || process.env.CLINE_DIR || resolve(homedir(), ".cline");
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
/**
|
|
2589
|
+
* cline scans four directories; we write the two that map onto our scopes.
|
|
2590
|
+
* User scope is `$CLINE_DIR/hooks`; project scope is `<cwd>/.clinerules/hooks`.
|
|
2591
|
+
*/
|
|
2592
|
+
export function clineHooksDir(scope: HookScope, cwd?: string): string {
|
|
2593
|
+
const base = cwd ? resolve(cwd) : process.cwd();
|
|
2594
|
+
return scope === "user"
|
|
2595
|
+
? resolve(clineIntegrationHome(), "hooks")
|
|
2596
|
+
: resolve(base, ".clinerules", "hooks");
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
function buildClineHookScript(binaryPath: string, eventType: string, scope: HookScope): string {
|
|
2600
|
+
const invocation = scope === "project" ? "npx -y failproofai" : `"${binaryPath}"`;
|
|
2601
|
+
return `#!/usr/bin/env bash
|
|
2602
|
+
# AUTO-GENERATED by failproofai. ${FAILPROOFAI_HOOK_MARKER}
|
|
2603
|
+
# Re-generate via: failproofai policies --install --cli cline
|
|
2604
|
+
#
|
|
2605
|
+
# THE FILENAME IS THE EVENT. cline binds this file to its ${eventType} event by
|
|
2606
|
+
# basename (case-insensitively) and by nothing else: renaming it silently
|
|
2607
|
+
# unsubscribes it, and an extension outside cline's allowlist is skipped without
|
|
2608
|
+
# a log line. Verified against cline v3.0.60.
|
|
2609
|
+
#
|
|
2610
|
+
# cline IGNORES THE EXIT CODE. The verdict is the single JSON object on stdout,
|
|
2611
|
+
# so this script must ALWAYS print exactly one object, and must print '{}' when
|
|
2612
|
+
# failproofai produced nothing: unparseable stdout makes cline skip the hook and
|
|
2613
|
+
# run the tool anyway (fail-open, 120s default timeout).
|
|
2614
|
+
#
|
|
2615
|
+
# Blocking shape, the only one cline reads: {"cancel":true,"errorMessage":"..."}.
|
|
2616
|
+
# There is no decision/block/permissionDecision field in cline's schema.
|
|
2617
|
+
set -u
|
|
2618
|
+
|
|
2619
|
+
payload="$(cat)"
|
|
2620
|
+
|
|
2621
|
+
# stderr is deliberately NOT redirected: failproofai's diagnostics belong in
|
|
2622
|
+
# cline's own hook log, and stdout stays clean because the evaluator writes
|
|
2623
|
+
# every operator note to stderr.
|
|
2624
|
+
verdict="$(printf '%s' "$payload" | ${invocation} --hook ${eventType} --cli cline)"
|
|
2625
|
+
|
|
2626
|
+
if [ -z "$verdict" ]; then
|
|
2627
|
+
verdict='{}'
|
|
2628
|
+
fi
|
|
2629
|
+
printf '%s\\n' "$verdict"
|
|
2630
|
+
exit 0
|
|
2631
|
+
`;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
export const cline: Integration = {
|
|
2635
|
+
id: "cline",
|
|
2636
|
+
displayName: "Cline",
|
|
2637
|
+
scopes: CLINE_HOOK_SCOPES,
|
|
2638
|
+
eventTypes: CLINE_HOOK_EVENT_TYPES,
|
|
2639
|
+
|
|
2640
|
+
getSettingsPath(scope, cwd) {
|
|
2641
|
+
// Bookkeeping only, so the Integration interface has something to read,
|
|
2642
|
+
// write and detect against. Inert for cline twice over: `.json` is not in
|
|
2643
|
+
// the extension allowlist AND `failproofai` is not one of the ten event
|
|
2644
|
+
// names, so cline never executes it.
|
|
2645
|
+
return resolve(
|
|
2646
|
+
clineHooksDir(scope === "local" ? "project" : scope, cwd),
|
|
2647
|
+
"failproofai.json",
|
|
2648
|
+
);
|
|
2649
|
+
},
|
|
2650
|
+
|
|
2651
|
+
readSettings(settingsPath) {
|
|
2652
|
+
return readJsonFile(settingsPath);
|
|
2653
|
+
},
|
|
2654
|
+
|
|
2655
|
+
writeSettings(settingsPath, settings) {
|
|
2656
|
+
writeJsonFile(settingsPath, settings);
|
|
2657
|
+
},
|
|
2658
|
+
|
|
2659
|
+
buildHookEntry(binaryPath, eventType, scope) {
|
|
2660
|
+
const effective: HookScope = scope === "local" ? "project" : (scope ?? "user");
|
|
2661
|
+
return {
|
|
2662
|
+
type: "command",
|
|
2663
|
+
file: `${eventType}${CLINE_HOOK_FILE_EXTENSION}`,
|
|
2664
|
+
command: `${
|
|
2665
|
+
effective === "project" ? "npx -y failproofai" : `"${binaryPath}"`
|
|
2666
|
+
} --hook ${eventType} --cli cline`,
|
|
2667
|
+
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
2668
|
+
};
|
|
2669
|
+
},
|
|
2670
|
+
|
|
2671
|
+
isFailproofaiHook(hook) {
|
|
2672
|
+
if (!hook || typeof hook !== "object") return false;
|
|
2673
|
+
return (hook as Record<string, unknown>)[FAILPROOFAI_HOOK_MARKER] === true;
|
|
2674
|
+
},
|
|
2675
|
+
|
|
2676
|
+
writeHookEntries(settings, binaryPath, scope) {
|
|
2677
|
+
const effective: HookScope = scope === "local" ? "project" : (scope ?? "user");
|
|
2678
|
+
const dir = clineHooksDir(effective);
|
|
2679
|
+
mkdirSync(dir, { recursive: true });
|
|
2680
|
+
for (const eventType of CLINE_HOOK_EVENT_TYPES) {
|
|
2681
|
+
const file = resolve(dir, `${eventType}${CLINE_HOOK_FILE_EXTENSION}`);
|
|
2682
|
+
// Refuse to clobber a hook we did not write. A hand-written PreToolUse.sh
|
|
2683
|
+
// is someone's enforcement too, and overwriting it is the same class of
|
|
2684
|
+
// damage as deleting it.
|
|
2685
|
+
if (existsSync(file)) {
|
|
2686
|
+
let existing = "";
|
|
2687
|
+
try {
|
|
2688
|
+
existing = readFileSync(file, "utf8");
|
|
2689
|
+
} catch {
|
|
2690
|
+
/* unreadable — treat as foreign and leave alone */
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
if (!existing.includes(FAILPROOFAI_HOOK_MARKER)) continue;
|
|
2694
|
+
}
|
|
2695
|
+
writeFileSync(file, buildClineHookScript(binaryPath, eventType, effective), "utf8");
|
|
2696
|
+
// cline does not require the executable bit (it picks an interpreter from
|
|
2697
|
+
// the shebang or the extension), but set it anyway: harmless, and it keeps
|
|
2698
|
+
// the file usable if cline ever execs directly.
|
|
2699
|
+
try {
|
|
2700
|
+
chmodSync(file, 0o755);
|
|
2701
|
+
} catch {
|
|
2702
|
+
/* best effort */
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
const s = settings as Record<string, unknown>;
|
|
2706
|
+
s.hooks = CLINE_HOOK_EVENT_TYPES.map((e) => this.buildHookEntry(binaryPath, e, effective));
|
|
2707
|
+
},
|
|
2708
|
+
|
|
2709
|
+
removeHooksFromFile(settingsPath) {
|
|
2710
|
+
const dir = dirname(settingsPath);
|
|
2711
|
+
let removed = 0;
|
|
2712
|
+
for (const eventType of CLINE_HOOK_EVENT_TYPES) {
|
|
2713
|
+
const file = resolve(dir, `${eventType}${CLINE_HOOK_FILE_EXTENSION}`);
|
|
2714
|
+
if (!existsSync(file)) continue;
|
|
2715
|
+
let body = "";
|
|
2716
|
+
try {
|
|
2717
|
+
body = readFileSync(file, "utf8");
|
|
2718
|
+
} catch {
|
|
2719
|
+
continue;
|
|
2720
|
+
}
|
|
2721
|
+
if (!body.includes(FAILPROOFAI_HOOK_MARKER)) continue; // not ours — leave it
|
|
2722
|
+
try {
|
|
2723
|
+
unlinkSync(file);
|
|
2724
|
+
removed++;
|
|
2725
|
+
} catch {
|
|
2726
|
+
/* already gone */
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
// NEVER remove `dir`. Unlike ori's feature directory, this one is SHARED
|
|
2730
|
+
// with the user's own hooks.
|
|
2731
|
+
const settings = this.readSettings(settingsPath) as Record<string, unknown>;
|
|
2732
|
+
delete settings.hooks;
|
|
2733
|
+
this.writeSettings(settingsPath, settings);
|
|
2734
|
+
return removed;
|
|
2735
|
+
},
|
|
2736
|
+
|
|
2737
|
+
hooksInstalledInSettings(scope, cwd) {
|
|
2738
|
+
const file = resolve(
|
|
2739
|
+
clineHooksDir(scope === "local" ? "project" : scope, cwd),
|
|
2740
|
+
`PreToolUse${CLINE_HOOK_FILE_EXTENSION}`,
|
|
2741
|
+
);
|
|
2742
|
+
if (!existsSync(file)) return false;
|
|
2743
|
+
try {
|
|
2744
|
+
return readFileSync(file, "utf8").includes(FAILPROOFAI_HOOK_MARKER);
|
|
2745
|
+
} catch {
|
|
2746
|
+
return false;
|
|
2747
|
+
}
|
|
2748
|
+
},
|
|
2749
|
+
|
|
2750
|
+
detectInstalled() {
|
|
2751
|
+
return binaryExists("cline");
|
|
2752
|
+
},
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2755
|
+
// ── Registry ──────────────────────────────────────────────────────────────
|
|
2279
2756
|
|
|
2280
2757
|
// `Partial` is kept (not every IntegrationType is guaranteed installable for
|
|
2281
2758
|
// LIVE hooks / Pillar 1) so a future audit-only CLI can omit its entry without a
|
|
2282
2759
|
// type error. `hermes` now has BOTH an audit adapter
|
|
2283
2760
|
// (src/audit/cli-adapters/hermes.ts) AND live-hook install support, so it is
|
|
2284
2761
|
// registered here.
|
|
2762
|
+
// ── grok (xAI grok CLI) integration ─────────────────────────────────────────
|
|
2763
|
+
//
|
|
2764
|
+
// grok reads Claude's NESTED hook schema out of its own directory: every
|
|
2765
|
+
// `*.json` under `~/.grok/hooks/` (user, always trusted) or
|
|
2766
|
+
// `<repo>/.grok/hooks/` (project). We own one file, `failproofai.json`, so the
|
|
2767
|
+
// install never has to merge with a user's other hook files — the same
|
|
2768
|
+
// arrangement as Copilot's `.github/hooks/failproofai.json`.
|
|
2769
|
+
//
|
|
2770
|
+
// `timeout` is in SECONDS (grok's default is 5, which is too tight for a cold
|
|
2771
|
+
// `npx` start; 30 matches what the other second-based CLIs use here).
|
|
2772
|
+
//
|
|
2773
|
+
// TWO project-scope conditions that are NOT in grok's docs and were found by
|
|
2774
|
+
// probing grok 1.0.3 — both silently produce a config that never fires:
|
|
2775
|
+
// • the directory must be a GIT REPO (a trusted non-git dir logs
|
|
2776
|
+
// `project_sources=0`), and
|
|
2777
|
+
// • the folder must be TRUSTED (`grok --trust` or `/hooks-trust`).
|
|
2778
|
+
// `projectScopeWarning()` below reports the first; the second is surfaced in
|
|
2779
|
+
// the install output, since neither has any other detector.
|
|
2780
|
+
//
|
|
2781
|
+
// Deny/instruct semantics live in policy-evaluator.ts's `cli === "grok"` branch
|
|
2782
|
+
// (grok's OWN `{decision:"deny"}` / `{decision:"block"}` shapes — it ignores
|
|
2783
|
+
// Claude's hookSpecificOutput deny, verified by A/B on a live session).
|
|
2784
|
+
const grok: Integration = {
|
|
2785
|
+
id: "grok",
|
|
2786
|
+
displayName: "grok CLI",
|
|
2787
|
+
scopes: GROK_HOOK_SCOPES,
|
|
2788
|
+
eventTypes: GROK_HOOK_EVENT_TYPES,
|
|
2789
|
+
|
|
2790
|
+
getSettingsPath(scope, cwd) {
|
|
2791
|
+
const base = cwd ? resolve(cwd) : process.cwd();
|
|
2792
|
+
switch (scope) {
|
|
2793
|
+
case "user":
|
|
2794
|
+
return resolve(homedir(), ".grok", "hooks", "failproofai.json");
|
|
2795
|
+
case "project":
|
|
2796
|
+
case "local":
|
|
2797
|
+
// grok has no "local" scope; fall back to project so callers don't crash.
|
|
2798
|
+
return resolve(base, ".grok", "hooks", "failproofai.json");
|
|
2799
|
+
}
|
|
2800
|
+
},
|
|
2801
|
+
|
|
2802
|
+
readSettings(settingsPath) {
|
|
2803
|
+
return readJsonFile(settingsPath);
|
|
2804
|
+
},
|
|
2805
|
+
|
|
2806
|
+
writeSettings(settingsPath, settings) {
|
|
2807
|
+
writeJsonFile(settingsPath, settings);
|
|
2808
|
+
},
|
|
2809
|
+
|
|
2810
|
+
buildHookEntry(binaryPath, eventType, scope) {
|
|
2811
|
+
const command =
|
|
2812
|
+
scope === "project"
|
|
2813
|
+
? `npx -y failproofai --hook ${eventType} --cli grok`
|
|
2814
|
+
: `"${binaryPath}" --hook ${eventType} --cli grok`;
|
|
2815
|
+
return {
|
|
2816
|
+
type: "command",
|
|
2817
|
+
command,
|
|
2818
|
+
// grok reads `timeout` in SECONDS. Its own default is 5s (600s for
|
|
2819
|
+
// Stop/SubagentStop); 30 leaves room for a cold `npx` start without
|
|
2820
|
+
// stalling the UI, and a timed-out hook fails OPEN on grok.
|
|
2821
|
+
timeout: 30,
|
|
2822
|
+
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
2823
|
+
};
|
|
2824
|
+
},
|
|
2825
|
+
|
|
2826
|
+
isFailproofaiHook: isMarkedHook,
|
|
2827
|
+
|
|
2828
|
+
writeHookEntries(settings, binaryPath, scope) {
|
|
2829
|
+
const s = settings as ClaudeSettings;
|
|
2830
|
+
if (!s.hooks) s.hooks = {};
|
|
2831
|
+
|
|
2832
|
+
for (const eventType of GROK_HOOK_EVENT_TYPES) {
|
|
2833
|
+
const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry;
|
|
2834
|
+
if (!s.hooks[eventType]) s.hooks[eventType] = [];
|
|
2835
|
+
const matchers: ClaudeHookMatcher[] = s.hooks[eventType];
|
|
2836
|
+
|
|
2837
|
+
let found = false;
|
|
2838
|
+
for (const matcher of matchers) {
|
|
2839
|
+
if (!matcher.hooks) continue;
|
|
2840
|
+
const idx = matcher.hooks.findIndex((h) => isMarkedHook(h as Record<string, unknown>));
|
|
2841
|
+
if (idx >= 0) {
|
|
2842
|
+
matcher.hooks[idx] = hookEntry;
|
|
2843
|
+
found = true;
|
|
2844
|
+
break;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
// No `matcher` key: an omitted matcher matches every tool. Do NOT write
|
|
2848
|
+
// `"*"` here — grok treats a matcher as a REGEX, and the same bare `"*"`
|
|
2849
|
+
// is an invalid pattern that matches nothing on Goose.
|
|
2850
|
+
if (!found) matchers.push({ hooks: [hookEntry] });
|
|
2851
|
+
}
|
|
2852
|
+
},
|
|
2853
|
+
|
|
2854
|
+
removeHooksFromFile(settingsPath) {
|
|
2855
|
+
const settings = this.readSettings(settingsPath) as ClaudeSettings;
|
|
2856
|
+
if (!settings.hooks) return 0;
|
|
2857
|
+
|
|
2858
|
+
let removed = 0;
|
|
2859
|
+
for (const eventType of Object.keys(settings.hooks)) {
|
|
2860
|
+
const matchers = settings.hooks[eventType];
|
|
2861
|
+
if (!Array.isArray(matchers)) continue;
|
|
2862
|
+
for (let i = matchers.length - 1; i >= 0; i--) {
|
|
2863
|
+
const matcher = matchers[i];
|
|
2864
|
+
if (!matcher.hooks) continue;
|
|
2865
|
+
const before = matcher.hooks.length;
|
|
2866
|
+
matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h as Record<string, unknown>));
|
|
2867
|
+
removed += before - matcher.hooks.length;
|
|
2868
|
+
if (matcher.hooks.length === 0) matchers.splice(i, 1);
|
|
2869
|
+
}
|
|
2870
|
+
if (matchers.length === 0) delete settings.hooks[eventType];
|
|
2871
|
+
}
|
|
2872
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
2873
|
+
|
|
2874
|
+
this.writeSettings(settingsPath, settings as Record<string, unknown>);
|
|
2875
|
+
return removed;
|
|
2876
|
+
},
|
|
2877
|
+
|
|
2878
|
+
hooksInstalledInSettings(scope, cwd) {
|
|
2879
|
+
const settingsPath = this.getSettingsPath(scope, cwd);
|
|
2880
|
+
if (!existsSync(settingsPath)) return false;
|
|
2881
|
+
try {
|
|
2882
|
+
const settings = this.readSettings(settingsPath) as ClaudeSettings;
|
|
2883
|
+
if (!settings.hooks) return false;
|
|
2884
|
+
for (const matchers of Object.values(settings.hooks)) {
|
|
2885
|
+
if (!Array.isArray(matchers)) continue;
|
|
2886
|
+
for (const matcher of matchers) {
|
|
2887
|
+
if (!matcher.hooks) continue;
|
|
2888
|
+
if (matcher.hooks.some((h) => isMarkedHook(h as Record<string, unknown>))) return true;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
} catch {
|
|
2892
|
+
// Corrupt settings — treat as not installed
|
|
2893
|
+
}
|
|
2894
|
+
return false;
|
|
2895
|
+
},
|
|
2896
|
+
|
|
2897
|
+
detectInstalled() {
|
|
2898
|
+
return binaryExists("grok");
|
|
2899
|
+
},
|
|
2900
|
+
};
|
|
2901
|
+
|
|
2902
|
+
// ── qwen (Qwen Code) integration ────────────────────────────────────────────
|
|
2903
|
+
//
|
|
2904
|
+
// qwen keeps hooks under a Claude-style `"hooks"` key inside its normal
|
|
2905
|
+
// settings file, which also holds `model`, `modelProviders`, auth, etc. — so
|
|
2906
|
+
// reads/writes go through the merge-preserving readJsonFile/writeJsonFile
|
|
2907
|
+
// helpers (like Claude/Copilot/Devin), never a whole-file replace.
|
|
2908
|
+
// user → ~/.qwen/settings.json
|
|
2909
|
+
// project → <cwd>/.qwen/settings.json
|
|
2910
|
+
//
|
|
2911
|
+
// The one shape difference from every other integration: `timeout` is in
|
|
2912
|
+
// MILLISECONDS (qwen's default is 60000). Do not "unify" it with the
|
|
2913
|
+
// seconds-based CLIs — 30 would mean 30ms and every hook would time out.
|
|
2914
|
+
//
|
|
2915
|
+
// Deny is Claude's own `hookSpecificOutput.permissionDecision` on PreToolUse
|
|
2916
|
+
// (so no evaluator branch is needed for it); only Stop diverges. See the
|
|
2917
|
+
// `cli === "qwen"` branch in policy-evaluator.ts.
|
|
2918
|
+
const qwen: Integration = {
|
|
2919
|
+
id: "qwen",
|
|
2920
|
+
displayName: "Qwen Code",
|
|
2921
|
+
scopes: QWEN_HOOK_SCOPES,
|
|
2922
|
+
eventTypes: QWEN_HOOK_EVENT_TYPES,
|
|
2923
|
+
|
|
2924
|
+
getSettingsPath(scope, cwd) {
|
|
2925
|
+
const base = cwd ? resolve(cwd) : process.cwd();
|
|
2926
|
+
switch (scope) {
|
|
2927
|
+
case "user":
|
|
2928
|
+
return resolve(homedir(), ".qwen", "settings.json");
|
|
2929
|
+
case "project":
|
|
2930
|
+
case "local":
|
|
2931
|
+
// qwen has no "local" scope; fall back to project so callers don't crash.
|
|
2932
|
+
return resolve(base, ".qwen", "settings.json");
|
|
2933
|
+
}
|
|
2934
|
+
},
|
|
2935
|
+
|
|
2936
|
+
readSettings(settingsPath) {
|
|
2937
|
+
return readJsonFile(settingsPath);
|
|
2938
|
+
},
|
|
2939
|
+
|
|
2940
|
+
writeSettings(settingsPath, settings) {
|
|
2941
|
+
writeJsonFile(settingsPath, settings);
|
|
2942
|
+
},
|
|
2943
|
+
|
|
2944
|
+
buildHookEntry(binaryPath, eventType, scope) {
|
|
2945
|
+
const command =
|
|
2946
|
+
scope === "project"
|
|
2947
|
+
? `npx -y failproofai --hook ${eventType} --cli qwen`
|
|
2948
|
+
: `"${binaryPath}" --hook ${eventType} --cli qwen`;
|
|
2949
|
+
return {
|
|
2950
|
+
type: "command",
|
|
2951
|
+
command,
|
|
2952
|
+
name: `failproofai-${eventType}`,
|
|
2953
|
+
// MILLISECONDS — qwen is the only integration that is not seconds-based.
|
|
2954
|
+
// 30000 = 30s, matching the wall-clock budget the others use.
|
|
2955
|
+
timeout: 30000,
|
|
2956
|
+
[FAILPROOFAI_HOOK_MARKER]: true,
|
|
2957
|
+
};
|
|
2958
|
+
},
|
|
2959
|
+
|
|
2960
|
+
isFailproofaiHook: isMarkedHook,
|
|
2961
|
+
|
|
2962
|
+
writeHookEntries(settings, binaryPath, scope) {
|
|
2963
|
+
const s = settings as ClaudeSettings;
|
|
2964
|
+
if (!s.hooks) s.hooks = {};
|
|
2965
|
+
|
|
2966
|
+
for (const eventType of QWEN_HOOK_EVENT_TYPES) {
|
|
2967
|
+
const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry;
|
|
2968
|
+
if (!s.hooks[eventType]) s.hooks[eventType] = [];
|
|
2969
|
+
const matchers: ClaudeHookMatcher[] = s.hooks[eventType];
|
|
2970
|
+
|
|
2971
|
+
let found = false;
|
|
2972
|
+
for (const matcher of matchers) {
|
|
2973
|
+
if (!matcher.hooks) continue;
|
|
2974
|
+
const idx = matcher.hooks.findIndex((h) => isMarkedHook(h as Record<string, unknown>));
|
|
2975
|
+
if (idx >= 0) {
|
|
2976
|
+
matcher.hooks[idx] = hookEntry;
|
|
2977
|
+
found = true;
|
|
2978
|
+
break;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
// Matcher omitted = match every tool. qwen also accepts `""`/`"*"`, but an
|
|
2982
|
+
// omitted key is the one form that is unambiguous across all of them.
|
|
2983
|
+
if (!found) matchers.push({ hooks: [hookEntry] });
|
|
2984
|
+
}
|
|
2985
|
+
},
|
|
2986
|
+
|
|
2987
|
+
removeHooksFromFile(settingsPath) {
|
|
2988
|
+
const settings = this.readSettings(settingsPath) as ClaudeSettings;
|
|
2989
|
+
if (!settings.hooks) return 0;
|
|
2990
|
+
|
|
2991
|
+
let removed = 0;
|
|
2992
|
+
for (const eventType of Object.keys(settings.hooks)) {
|
|
2993
|
+
const matchers = settings.hooks[eventType];
|
|
2994
|
+
if (!Array.isArray(matchers)) continue;
|
|
2995
|
+
for (let i = matchers.length - 1; i >= 0; i--) {
|
|
2996
|
+
const matcher = matchers[i];
|
|
2997
|
+
if (!matcher.hooks) continue;
|
|
2998
|
+
const before = matcher.hooks.length;
|
|
2999
|
+
matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h as Record<string, unknown>));
|
|
3000
|
+
removed += before - matcher.hooks.length;
|
|
3001
|
+
if (matcher.hooks.length === 0) matchers.splice(i, 1);
|
|
3002
|
+
}
|
|
3003
|
+
if (matchers.length === 0) delete settings.hooks[eventType];
|
|
3004
|
+
}
|
|
3005
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
3006
|
+
|
|
3007
|
+
this.writeSettings(settingsPath, settings as Record<string, unknown>);
|
|
3008
|
+
return removed;
|
|
3009
|
+
},
|
|
3010
|
+
|
|
3011
|
+
hooksInstalledInSettings(scope, cwd) {
|
|
3012
|
+
const settingsPath = this.getSettingsPath(scope, cwd);
|
|
3013
|
+
if (!existsSync(settingsPath)) return false;
|
|
3014
|
+
try {
|
|
3015
|
+
const settings = this.readSettings(settingsPath) as ClaudeSettings;
|
|
3016
|
+
if (!settings.hooks) return false;
|
|
3017
|
+
for (const matchers of Object.values(settings.hooks)) {
|
|
3018
|
+
if (!Array.isArray(matchers)) continue;
|
|
3019
|
+
for (const matcher of matchers) {
|
|
3020
|
+
if (!matcher.hooks) continue;
|
|
3021
|
+
if (matcher.hooks.some((h) => isMarkedHook(h as Record<string, unknown>))) return true;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
} catch {
|
|
3025
|
+
// Corrupt settings — treat as not installed
|
|
3026
|
+
}
|
|
3027
|
+
return false;
|
|
3028
|
+
},
|
|
3029
|
+
|
|
3030
|
+
detectInstalled() {
|
|
3031
|
+
return binaryExists("qwen");
|
|
3032
|
+
},
|
|
3033
|
+
};
|
|
3034
|
+
|
|
2285
3035
|
const INTEGRATIONS: Partial<Record<IntegrationType, Integration>> = {
|
|
2286
3036
|
claude: claudeCode,
|
|
2287
3037
|
codex,
|
|
@@ -2295,6 +3045,10 @@ const INTEGRATIONS: Partial<Record<IntegrationType, Integration>> = {
|
|
|
2295
3045
|
devin,
|
|
2296
3046
|
antigravity,
|
|
2297
3047
|
goose,
|
|
3048
|
+
grok,
|
|
3049
|
+
qwen,
|
|
3050
|
+
ori,
|
|
3051
|
+
cline,
|
|
2298
3052
|
};
|
|
2299
3053
|
|
|
2300
3054
|
export function getIntegration(id: IntegrationType): Integration {
|