failproofai 0.0.14 → 0.0.15-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- 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 +2 -2
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- 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 +2 -2
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +1 -1
- 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 +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +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 +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 +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +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 +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- 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 +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- 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 +3 -3
- 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 +4 -4
- 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 +3 -3
- 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 +4 -4
- 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 +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -3
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0hv08mx._.js → [root-of-the-server]__0-dh5jy._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0obaznk._.js → [root-of-the-server]__0h3lfmp._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0xvsnvt._.js → [root-of-the-server]__0l4vuqm._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0longh-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sb_5m8._.js +4 -4
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +6 -4
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1hkdkmz._.js → [root-of-the-server]__10oa-v9._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1i9rknc._.js → [root-of-the-server]__18d-9oi._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1is3glr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +6 -6
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0hdsupo._.js → [root-of-the-server]__0114ewg._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01od0m6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1s2b6h0._.js → [root-of-the-server]__04o-kw7._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lpvwqm._.js +21 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ynf7tx._.js → [root-of-the-server]__0qucxyj._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0uno39i._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +8 -8
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00cz9as._.js → [root-of-the-server]__1apb7k_._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1i21qhv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1py795m._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_077y5np._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1-3unkg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1m_p7pi._.js +3 -0
- 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_audit_loading_tsx_1j1kc6j._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_hermes-projects_ts_05b8-it._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0gkzjaa.js → node_modules_html-to-image_es_index_0w82veb.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/static/chunks/0hn30_j22wpp_.js +6 -0
- package/.next/standalone/.next/static/chunks/{3s8aaig91937v.js → 1_20f96f1253b.js} +1 -1
- package/.next/standalone/.next/static/chunks/1hdhteui932bq.js +1 -0
- package/.next/standalone/.next/static/chunks/{33u59vf_8xpd-.js → 1j2xm011_aoae.js} +1 -1
- package/.next/standalone/.next/static/chunks/1qzsbft-jtnf8.js +1 -0
- package/.next/standalone/.next/static/chunks/{3-ogr79xqbw0h.js → 2mazwb1qmur1p.js} +1 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +2 -0
- package/.next/standalone/.next/static/chunks/2xbkqlma_-61k.js +1 -0
- package/.next/standalone/.next/static/chunks/{29o4j32z643_a.js → 2y7acsw0--8xf.js} +1 -1
- package/.next/standalone/.next/static/chunks/3060a-0fayz96.js +1 -0
- package/.next/standalone/.next/static/chunks/312ihvhxeuoi_.js +1 -0
- package/.next/standalone/.next/static/chunks/31c084gsop1bb.js +1 -0
- package/.next/standalone/.next/static/chunks/{1ua5armwfph8o.js → 31fmsp1soxi07.js} +1 -1
- package/.next/standalone/.next/static/chunks/{02jcotghcxrbu.js → 3kmwfieo15mr2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2uh_4k4m296z5.js → 3q9y67-fobgpc.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-10hj8t5pf33tu.js → turbopack-1lsv92x7klwrn.js} +1 -1
- package/.next/standalone/app/actions/get-hooks-config.ts +85 -0
- package/.next/standalone/app/components/project-list.tsx +229 -42
- package/.next/standalone/app/policies/hooks-client.tsx +112 -14
- package/.next/standalone/app/project/[name]/page.tsx +12 -2
- package/.next/standalone/integration-suite/README.md +43 -1
- package/.next/standalone/integration-suite/ci-entrypoint.sh +21 -6
- package/.next/standalone/integration-suite/install-clis.sh +58 -13
- package/.next/standalone/integration-suite/probe-cli.sh +51 -11
- package/.next/standalone/integration-suite/report.js +52 -6
- package/.next/standalone/integration-suite/run.sh +55 -3
- package/.next/standalone/lib/hermes-profiles.ts +156 -0
- package/.next/standalone/lib/hermes-projects.ts +91 -25
- package/.next/standalone/lib/hermes-sessions.ts +65 -10
- package/.next/standalone/lib/openclaw-projects.ts +111 -18
- package/.next/standalone/lib/openclaw-sessions.ts +20 -0
- package/.next/standalone/lib/paths.ts +36 -0
- package/.next/standalone/lib/project-tree.ts +193 -0
- package/.next/standalone/lib/projects.ts +22 -0
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/next-config-ts/transpile-config.js +3 -114
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/cli/next-test.js +2 -1
- package/.next/standalone/node_modules/next/dist/compiled/http-proxy/index.js +5 -5
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +11 -10
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +11 -10
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +2 -2
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +9 -9
- package/.next/standalone/node_modules/next/dist/lib/install-dependencies.js +1 -1
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/lib/typescript/loadTsConfig.js +181 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/missingDependencyError.js +1 -1
- package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeCheckCli.js +53 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +256 -0
- package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +97 -56
- package/.next/standalone/node_modules/next/dist/server/app-render/action-handler.js +129 -34
- package/.next/standalone/node_modules/next/dist/server/app-render/manifests-singleton.js +52 -4
- package/.next/standalone/node_modules/next/dist/server/config-schema.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/config-shared.js +1 -2
- 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 +8 -27
- 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/index.js +90 -53
- package/.next/standalone/node_modules/next/dist/server/lib/patch-fetch.js +15 -2
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/proxy-request.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/unstable-cache.js +1 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/prepare-destination.js +2 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/server-reference-info.js +12 -0
- 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 +7 -6
- package/.next/standalone/pi-extension/index.ts +38 -5
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +623 -308
- package/lib/hermes-profiles.ts +156 -0
- package/lib/hermes-projects.ts +91 -25
- package/lib/hermes-sessions.ts +65 -10
- package/lib/openclaw-projects.ts +111 -18
- package/lib/openclaw-sessions.ts +20 -0
- package/lib/paths.ts +36 -0
- package/lib/project-tree.ts +193 -0
- package/lib/projects.ts +22 -0
- package/package.json +7 -6
- package/pi-extension/index.ts +38 -5
- package/scripts/launch.ts +7 -0
- package/src/audit/cli-adapters/hermes.ts +6 -3
- package/src/audit/cli-adapters/openclaw.ts +17 -13
- package/src/hooks/configure-wizard.ts +24 -2
- package/src/hooks/custom-hooks-loader.ts +58 -4
- package/src/hooks/enforcement-capability.ts +266 -0
- package/src/hooks/handler.ts +12 -1
- package/src/hooks/hook-activity-store.ts +14 -0
- package/src/hooks/hooks-config.ts +59 -1
- package/src/hooks/integrations.ts +123 -19
- package/src/hooks/manager.ts +93 -27
- package/src/hooks/policy-evaluator.ts +59 -7
- package/src/hooks/policy-types.ts +26 -0
- package/src/hooks/types.ts +50 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d9qen3._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zf8xlq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fax1sl._.js +0 -19
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0_9_ytf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +0 -3
- package/.next/standalone/.next/static/chunks/06z_ex1ts3n4s.js +0 -1
- package/.next/standalone/.next/static/chunks/0o0ut0ugd0_-s.js +0 -1
- package/.next/standalone/.next/static/chunks/0xl97x-874k-u.js +0 -1
- package/.next/standalone/.next/static/chunks/19d7hqx8udopz.js +0 -1
- package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +0 -2
- package/.next/standalone/.next/static/chunks/370sj2lgkw5ey.js +0 -1
- package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +0 -1
- package/.next/standalone/.next/static/chunks/42oeva1-w0z2d.js +0 -6
- /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → QFmU1xD4R9N_HVfwccP6A}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → QFmU1xD4R9N_HVfwccP6A}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → QFmU1xD4R9N_HVfwccP6A}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does a policy DENY on this (cli, event) pair actually change the agent's
|
|
3
|
+
* behaviour, given the wire shape failproofai emits TODAY?
|
|
4
|
+
*
|
|
5
|
+
* This exists because the same claim used to live in prose, and prose drifted:
|
|
6
|
+
* CLAUDE.md documented Hermes `subagent_stop` as a working gate while upstream
|
|
7
|
+
* discarded its return, so every customer policy on that event enforced
|
|
8
|
+
* nothing, silently, for months. A table a test can assert against cannot rot
|
|
9
|
+
* that way.
|
|
10
|
+
*
|
|
11
|
+
* ABSENT MEANS UNKNOWN, NOT "block". Only rows traced to a consuming call site,
|
|
12
|
+
* an official contract, or a recorded live probe appear here. Callers must
|
|
13
|
+
* treat a missing entry as "say nothing" — a hedge rendered in the UI is still
|
|
14
|
+
* a claim, and an unverified claim is what this file exists to prevent.
|
|
15
|
+
*/
|
|
16
|
+
// src/hooks/enforcement-capability.ts
|
|
17
|
+
//
|
|
18
|
+
// Does a policy DENY on this (cli, event) pair actually change the agent's
|
|
19
|
+
// behaviour, given the wire shape failproofai emits TODAY?
|
|
20
|
+
//
|
|
21
|
+
// "block" — the verdict is read at a call site that prevents the action
|
|
22
|
+
// or forces the agent to continue/retry.
|
|
23
|
+
// "observe" — the action proceeds regardless. Either the CLI discards the
|
|
24
|
+
// verdict, or the tool already ran, or the CLI *could* block but
|
|
25
|
+
// our shape is inert (each such line says so).
|
|
26
|
+
// ABSENT — NOT VERIFIED. The UI must stay silent, never assume "block".
|
|
27
|
+
//
|
|
28
|
+
// Only SOURCE / VENDOR_DOCS / LIVE_PROBE rows appear here. Every line carries
|
|
29
|
+
// its evidence. Versions probed: claude 2.1.220 · codex fe01054a · copilot
|
|
30
|
+
// 1.0.71 (some sites re-read in 1.0.68) · cursor-agent 2026.07.16-899851b ·
|
|
31
|
+
// opencode 7565e035 (v1.18.9, re-checked at v1.14.33) · pi 0.80.10 (source
|
|
32
|
+
// 0.82.1) · hermes-agent 5771a6e · openclaw f8ed8ecf (v2026.7.2) · droid
|
|
33
|
+
// 0.175.1 · devin 3000.2.17 · agy 1.1.8 · goose 1.43.0.
|
|
34
|
+
import type { HookEventType, IntegrationType } from "./types";
|
|
35
|
+
|
|
36
|
+
export type EnforcementCapability = "block" | "observe";
|
|
37
|
+
|
|
38
|
+
export const ENFORCEMENT_CAPABILITY: Record<
|
|
39
|
+
IntegrationType,
|
|
40
|
+
Partial<Record<HookEventType, EnforcementCapability>>
|
|
41
|
+
> = {
|
|
42
|
+
// ── claude ────────────────────────────────────────────────────────────────
|
|
43
|
+
// Closed source, but the shipped launcher embeds its minified JS as plaintext.
|
|
44
|
+
// Offsets are BYTE offsets into the shipped claude 2.1.220 launcher bundle.
|
|
45
|
+
// The hook RUNNER is event-agnostic (exit 2 -> blockingError @259223339); all
|
|
46
|
+
// asymmetry lives in the ~29 call sites that read or ignore it.
|
|
47
|
+
claude: {
|
|
48
|
+
PreToolUse: "block", // @256826565 gan() -> behavior==="deny" short-circuits the permission pipeline; tool.call never reached (@256827907)
|
|
49
|
+
UserPromptSubmit: "block", // @262298150 blockingError -> {shouldQuery:false} early return; prompt never sent to the model
|
|
50
|
+
PermissionRequest: "block", // @255767400 runHooks reads permissionRequestResult -> buildDeny. NOTE exit 2 is IGNORED here (docs are wrong); we emit hookSpecificOutput.decision.behavior:"deny" which IS the honoured shape — do not "simplify" to exit 2
|
|
51
|
+
Stop: "block", // @254572503 blockingErrors -> stopHookActive + `continue` re-enters the loop. CAP: CLAUDE_CODE_STOP_HOOK_BLOCK_CAP (default 8); discarded on end-turn paths (@254511900)
|
|
52
|
+
SubagentStop: "block", // @259180818 VEe emits SubagentStop when agentId set -> same consumer @254572503. Same cap + same end-turn discard
|
|
53
|
+
PreCompact: "block", // MEe @259170280 blockedBy -> consumers @254317851 / @254328121 return early; compaction does not run
|
|
54
|
+
PostToolBatch: "block", // @254577316 -> yields hook_stopped_continuation and RETURNS from the query generator. Tools in the batch already ran
|
|
55
|
+
ConfigChange: "block", // slt @259171700 -> consumer @251465990 skips the reload. CAVEAT: source==="policy_settings" is force-unblocked (MDM immune)
|
|
56
|
+
UserPromptExpansion: "block", // producer @254678081, real consumer @254672418 `if("blocked" in C) return C.blocked`. Slash-command / MCP-prompt only
|
|
57
|
+
TaskCreated: "block", // TaskCreate.call @256302400: blockingError -> task deleted (yAo) + tool throws
|
|
58
|
+
TaskCompleted: "block", // @254518449 -> same honoured consumer @254572503. GATE: teammate mode only (oy() @249372028)
|
|
59
|
+
TeammateIdle: "block", // @254518714 -> same consumer @254572503. Same teammate-mode gate
|
|
60
|
+
Elicitation: "block", // oOt @259172011 -> $_o @253296308 -> MCP handler U_y @253294456 returns {action:"decline"} to the server
|
|
61
|
+
ElicitationResult: "block", // iOt @259172485 -> N_o @253296535 / Zbo @253568095 force {action:"decline"}. Vetoes the response, does NOT stop the agent
|
|
62
|
+
PostToolUse: "observe", // OVERTURNED from "block". We emit hookSpecificOutput.additionalContext (policy-evaluator.ts:520-534) -> man's additionalContexts branch @256141810, never blockingError; and the tool already ran either way
|
|
63
|
+
PostToolUseFailure: "observe", // OVERTURNED. han @256143680 yields only cancelled / blocking_error / additional_context; consumer @256840799 just appends messages after the failed tool result. No stop channel exists
|
|
64
|
+
SessionStart: "observe", // @254090768 blockingError -> Pur() = hook_non_blocking_error; session proceeds. Only additionalContext has effect
|
|
65
|
+
Setup: "observe", // @254092164 -> same Pur() downgrade
|
|
66
|
+
SubagentStart: "observe", // @254689000 -> same Pur() downgrade; the subagent launches anyway
|
|
67
|
+
SessionEnd: "observe", // TFt @259174994 reads only !succeeded -> stderr passthrough; `blocked` never read
|
|
68
|
+
StopFailure: "observe", // zpt @~259180670 discards the EM() return entirely. Docs concur: "output and exit code are ignored"
|
|
69
|
+
PermissionDenied: "observe", // @256829448 reads ONLY hookSpecificOutput.retry; also fires only for classifier==="auto-mode"
|
|
70
|
+
Notification: "observe", // h9 @259174090 — return value not assigned
|
|
71
|
+
InstructionsLoaded: "observe", // nPt @259173437 — return value not assigned
|
|
72
|
+
CwdChanged: "observe", // vtn @259173188 -> zop @259172968 reads only watchPaths/systemMessage
|
|
73
|
+
FileChanged: "observe", // Atn @259173298 -> same zop consumer
|
|
74
|
+
PostCompact: "observe", // xpt @259170833 builds a display message; `blocked` never read (contrast MEe/PreCompact directly above it)
|
|
75
|
+
WorktreeRemove: "observe", // Zor @259183361 only records whether some hook succeeded; a deny just logs
|
|
76
|
+
// WorktreeCreate DELIBERATELY OMITTED: ODt @259182432 is a path PROVIDER, not
|
|
77
|
+
// a gate — with no stdout it throws "hook succeeded but returned no worktree
|
|
78
|
+
// path", so an ALLOW aborts creation exactly like a deny. Degenerate, not a
|
|
79
|
+
// capability. Fix the bug (emit a path on allow) before classifying it.
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// ── codex ─────────────────────────────────────────────────────────────────
|
|
83
|
+
// openai/codex @fe01054a. Every verdict funnels through codex-rs/core/src/hook_runtime.rs.
|
|
84
|
+
codex: {
|
|
85
|
+
PreToolUse: "block", // tools/registry.rs:528 PreToolUseHookResult::Blocked -> FunctionCallError::RespondToModel; handler never runs. CAVEAT hook_runtime.rs:202 an empty reason silently degrades to Continue
|
|
86
|
+
PermissionRequest: "block", // tools/approvals.rs:220 Deny -> ReviewDecision::denied. CAVEAT only consulted when the call reaches the approval path (orchestrator.rs:168 Skip bypasses it)
|
|
87
|
+
UserPromptSubmit: "block", // events/user_prompt_submit.rs:226 Some(2)+stderr -> should_stop; turn.rs:588 skips record_pending_input, turn.rs:237 returns. CAVEAT turn.rs:603 — a co-submitted accepted message lets the turn proceed
|
|
88
|
+
Stop: "block", // events/stop.rs:303 Some(2) -> should_block + continuation_prompt; turn.rs:473-489 records it and `continue`s. CAVEAT turn.rs:490 a block with no prompt is warned + ignored
|
|
89
|
+
SubagentStop: "block", // shared parse_completed (stop.rs:202) + same consumer turn.rs:473. CAVEAT hook_runtime.rs:345 — only ThreadSpawn subagents dispatch it; all other SubAgent sources never run the hook
|
|
90
|
+
PostToolUse: "observe", // codex CAN block here (registry.rs:678) but we emit hookSpecificOutput.additionalContext with no top-level `decision`, and output_parser.rs:215 needs decision==="block". Inert by our choice; tool already ran regardless
|
|
91
|
+
SessionStart: "observe", // events/session_start.rs:243-322 has NO Some(2) arm — our exit-2 deny is logged as HookRunStatus::Failed. CLI blocks only on exit 0 + {"continue":false} (session_start.rs:273 -> turn.rs:233)
|
|
92
|
+
SubagentStart: "observe", // permanently unblockable: session_start.rs:273 guard excludes it, output schema has no `decision`, vendor doc says continue:false does not stop it
|
|
93
|
+
PreCompact: "observe", // compact.rs:246-260 no Some(2) arm; latent block needs {"continue":false} (-> core/src/compact.rs:190 TurnAborted). Inert as we emit
|
|
94
|
+
PostCompact: "observe", // compact.rs:349-363 same structure; latent block needs {"continue":false} (-> compact.rs:216). Inert as we emit
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// ── copilot ───────────────────────────────────────────────────────────────
|
|
98
|
+
// Closed source; vendor build ships readable JS. Offsets are BYTE offsets into
|
|
99
|
+
// the @github/copilot-linux-x64 app.js (c71/ = 1.0.71, copilot/ = 1.0.68).
|
|
100
|
+
// CRITICAL: exit 2 is NEVER a deny channel on copilot, for any event
|
|
101
|
+
// (runtime.node@60056587 "Hook command exited with code 2 (warning)").
|
|
102
|
+
copilot: {
|
|
103
|
+
PreToolUse: "block", // 1.0.68 app.js@2878277 permissionDecision==="deny" -> resultType:"denied"; 1.0.71 moved to Rust, runtime.node@60059067 "Denied by preToolUse hook". Corroborated LIVE on 1.0.71 (session events success:false code:denied) + daily integration-suite probe
|
|
104
|
+
Stop: "block", // 1.0.71 app.js@2855430 agentStop: decision==="block" && string reason -> enqueueUserMessage(reason). We emit exactly that shape
|
|
105
|
+
SubagentStop: "block", // 1.0.71 app.js@1074101 subagentStop: decision==="block" && reason -> `continue` re-runs the subagent turn. CAVEAT skipped entirely for isSidekick subagents
|
|
106
|
+
UserPromptSubmit: "block", // FIXED: we now emit {decision:"block",reason} at exit 0 (gate V$t @2547438, consumer @2823018). Was inert — we sent exit 2 + stderr, which copilot logs as a warning for EVERY event and never treats as a deny
|
|
107
|
+
PermissionRequest: "block", // FIXED: we now emit the FLAT {behavior,message} copilot parses (normalizer CMn @179042 -> mapper h4t @2686538). Was inert — the Codex-shaped nested hookSpecificOutput.decision normalized to {}
|
|
108
|
+
PostToolUse: "observe", // OVERTURNED. Gate vK @173853 needs top-level decision==="block"; we emit hookSpecificOutput.additionalContext. Post-hoc anyway (result-rewrite only). Vendor docs: "Can block? No"
|
|
109
|
+
SessionStart: "observe", // 1.0.71 app.js@2836633 reads ONLY additionalContext
|
|
110
|
+
SessionEnd: "observe", // 1.0.71 app.js@2627263 `.then(()=>{})` — return explicitly thrown away
|
|
111
|
+
PostToolUseFailure: "observe", // 1.0.68 app.js@2883424 and @3038111 destructure only additionalContext
|
|
112
|
+
PreCompact: "observe", // 1.0.71 app.js@2758186 and @2852753 — result unassigned at both call sites
|
|
113
|
+
Notification: "observe", // 1.0.71 app.js@2746820 reads only additionalContext
|
|
114
|
+
// ErrorOccurred is installed but has no canonical HookEventType, so it
|
|
115
|
+
// cannot be keyed here. It is observe: 1.0.71 app.js@2846297, result unassigned.
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// ── cursor ────────────────────────────────────────────────────────────────
|
|
119
|
+
// Closed source; local install ships webpack-minified JS. Offsets are CHAR
|
|
120
|
+
// offsets into the shipped cursor-agent 2026.07.16-899851b bundle.
|
|
121
|
+
cursor: {
|
|
122
|
+
PreToolUse: "block", // 3143.index.js char 26967 permission==="deny" -> {type:"rejected"}; second MCP consumer char 42084
|
|
123
|
+
Stop: "block", // 1931.index.js char 881820 followup_message queued as the next user message. CAPS: loop_limit default 5 (index.js char 4123300); consumed ONLY on the status:"completed" path — user-abort (char 870298) and turn-error (char 883549) discard it
|
|
124
|
+
UserPromptSubmit: "block", // FIXED: we now emit {continue:false,user_message} (1931.index.js char 887883 — the only block key). Was inert: {permission:"deny"} validates as an unknown-key object and is dropped on this event
|
|
125
|
+
PostToolUse: "observe", // 3143.index.js char 26339 fireSuccessAsync reads only additional_context; validator accepts nothing else (index.js char 4126700)
|
|
126
|
+
SessionStart: "observe", // 1931.index.js char 731566 consumes only env + additional_context; `continue` never read even though Cursor's own q() builds it
|
|
127
|
+
SessionEnd: "observe", // 1931.index.js char 724540 — result assigned to nothing
|
|
128
|
+
// SubagentStop OMITTED: producer exists (index.js char 3769676 forwards
|
|
129
|
+
// followupMessage) and vendor docs describe loop-style follow-ups, but no
|
|
130
|
+
// code in the shipped install ever CONSTRUCTS a subagentStop request, so
|
|
131
|
+
// the consumer is outside the bundle. unknown.
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// ── opencode ──────────────────────────────────────────────────────────────
|
|
135
|
+
// sst/opencode @7565e035 (v1.18.9); every finding re-checked at tag v1.14.33.
|
|
136
|
+
opencode: {
|
|
137
|
+
PreToolUse: "block", // session/tools.ts:106-110 trigger runs BEFORE item.execute (:111) inside run.promise; a throw is an Effect defect that kills the fiber. Upstream test test/tool/code-mode.test.ts:428 proves the tool body never executed
|
|
138
|
+
PostToolUse: "observe", // session/tools.ts:121-125 sits after item.execute; a throw replaces the output with a tool-error the model sees, but the write/command already happened
|
|
139
|
+
SessionStart: "observe", // plugin/index.ts:255 `void hook["event"]?.(…)` — fire-and-forget, return structurally unreadable
|
|
140
|
+
SessionEnd: "observe", // plugin/index.ts:255 — same void dispatch
|
|
141
|
+
UserPromptSubmit: "observe", // plugin/index.ts:255 same void dispatch; and message.updated is published from session.ts:631 AFTER the message is persisted. The real gate is chat.message, which we do not install (§4)
|
|
142
|
+
PermissionRequest: "observe", // ☠ DEAD HOOK: permission.ask is DECLARED (packages/plugin/src/index.ts:261) and documented upstream, but NEVER INVOKED — the only two dispatch sites are hook["event"] (:255) and Plugin.trigger (:288), and permission.ask is not among the 13 names passed to .trigger(). The policy does not even run. See §5.3 for the copy hazard
|
|
143
|
+
// Stop OMITTED: session.idle's plugin return is definitively discarded
|
|
144
|
+
// (plugin/index.ts:255), so we instead call client.session.prompt(...)
|
|
145
|
+
// out-of-band (integrations.ts:886-897). That SDK route does start a fresh
|
|
146
|
+
// turn, but the composite (alive at idle? one-shot `opencode run`?) is
|
|
147
|
+
// unverified end to end. unknown.
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// ── pi ────────────────────────────────────────────────────────────────────
|
|
151
|
+
// @earendil-works/pi-coding-agent 0.80.10 installed (d.ts read directly);
|
|
152
|
+
// source cross-read at repo cced6a2 (0.82.1). Identical result shapes.
|
|
153
|
+
pi: {
|
|
154
|
+
PreToolUse: "block", // tool_call: ToolCallEventResult{block,reason} (types.d.ts:766) -> runner.emitToolCall returns on result.block -> agent-loop.ts:626 `if(beforeResult?.block) return createErrorToolResult(...)`; tool never executes. user_bash also maps here and is now FIXED too: the shim returns a full-replacement {result:BashResult} (types.d.ts:772) so the command is not run — {block,reason} had no matching field
|
|
155
|
+
UserPromptSubmit: "block", // FIXED: the shim now returns {action:"handled"} (InputEventResult, types.d.ts:629) so the prompt is never submitted. Was inert — {block,reason} matched no branch. CAVEAT: `handled` drops the prompt SILENTLY; Pi shows the user nothing, so the shim logs the reason to stderr
|
|
156
|
+
PostToolUse: "observe", // tool_result: ToolResultEventResult{content,details,isError,usage} (types.d.ts:778) — no block field; mutation only
|
|
157
|
+
SessionStart: "observe", // on(event:"session_start", ExtensionHandler<SessionStartEvent>) — NO Result type param (types.d.ts:842); runner.emit() only consumes returns for session_before_* (runner.ts:787-813)
|
|
158
|
+
SessionEnd: "observe", // session_shutdown: same — handler declared with no Result type (types.d.ts:848)
|
|
159
|
+
Stop: "observe", // agent_end: handler has NO Result type (types.d.ts:857) — the turn cannot be retried. Our shim stashes the reason and injects it into the NEXT turn via before_agent_start.systemPrompt (index.ts:458). Real, but it is a next-turn instruction, not a gate
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
// ── hermes ────────────────────────────────────────────────────────────────
|
|
163
|
+
// hermes-agent @5771a6e. agent/shell_hooks.py _parse_response is EVENT-GATED:
|
|
164
|
+
// it returns a verdict only for pre_tool_call and pre_verify.
|
|
165
|
+
hermes: {
|
|
166
|
+
PreToolUse: "block", // pre_tool_call — the only tool gate; the tool does not run
|
|
167
|
+
PostToolUse: "observe", // post_tool_call — not in _parse_response's gate
|
|
168
|
+
SubagentStop: "observe", // OVERTURNED from CLAUDE.md's "✅ block". Not in _parse_response's gate, AND the call site (tools/delegate_tool.py) discards the return. Customers with a SubagentStop deny had ZERO enforcement
|
|
169
|
+
SessionStart: "observe", // on_session_start — not gated in
|
|
170
|
+
SessionEnd: "observe", // on_session_end — call site discards the return
|
|
171
|
+
// Stop: NOT KEYED — `pre_verify` is a real turn-end gate upstream but we do
|
|
172
|
+
// not install it, so no canonical Stop event ever fires for hermes.
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// ── openclaw ──────────────────────────────────────────────────────────────
|
|
176
|
+
// openclaw/openclaw @f8ed8ecf (v2026.7.2) + our own shipped shim
|
|
177
|
+
// openclaw-plugin/index.js, which returns undefined for 5 of the 8 hooks.
|
|
178
|
+
openclaw: {
|
|
179
|
+
PreToolUse: "block", // before_tool_call -> shim returns {block:true, blockReason}
|
|
180
|
+
UserPromptSubmit: "block", // before_agent_run -> shim returns {outcome:"block", reason}
|
|
181
|
+
Stop: "block", // before_agent_finalize -> shim returns {action:"revise", reason}; the turn re-runs
|
|
182
|
+
PostToolUse: "observe", // void upstream AND our shim returns undefined
|
|
183
|
+
SessionStart: "observe", // void upstream AND our shim returns undefined
|
|
184
|
+
SessionEnd: "observe", // void upstream AND our shim returns undefined
|
|
185
|
+
SubagentStop: "observe", // subagent_ended cannot veto upstream; our shim also returns undefined
|
|
186
|
+
PreCompact: "observe", // before_compaction — same
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// ── factory (droid) ───────────────────────────────────────────────────────
|
|
190
|
+
// droid 0.175.1 (the shipped droid binary, ELF not stripped, embedded JS readable).
|
|
191
|
+
// Anchors below are unique grep-able strings in that bundle.
|
|
192
|
+
factory: {
|
|
193
|
+
PreToolUse: "block", // grep `throwing ToolExecutionControlError`: exit 2 -> throw $SH(stderr); also hookSpecificOutput.permissionDecision==="deny" -> throw $SH, continue===false -> throw LSH, exit 3 -> AgentAbortError. We emit exit 2 + stderr
|
|
194
|
+
UserPromptSubmit: "block", // executeUserPromptSubmitHooks: `if(B.exitCode===2||B.continue===!1){ … return {prompt,hookContext,blocked:!0} }` — the turn never starts
|
|
195
|
+
Stop: "block", // grep `[Agent] Stop hook blocking with decision: block`: {decision:"block",reason} sets the continue flag; grep `[Agent] Stop hook blocking with exit code 2` shows exit 2 is a FALLBACK too (CLAUDE.md says otherwise — harmless, we emit the JSON)
|
|
196
|
+
PreCompact: "block", // grep `[Compaction] PreCompact hook blocked compaction`: exit 2 or 3 -> returns the uncompacted conversation
|
|
197
|
+
PostToolUse: "observe", // exit 2 -> ADD_MESSAGE(role:"system", visibility:"llm_only") with the postToolUseFeedbackPrefix — feedback, not a block; tool already ran. (continue===false / exit 3 WOULD stop, but we emit neither)
|
|
198
|
+
SubagentStop: "observe", // `await MR("SubagentStop",{…})` — the awaited result is not assigned to anything. Hermes-class: our SubagentStop deny is discarded
|
|
199
|
+
SessionStart: "observe", // applySessionStartHookResults consumes only the env file + additionalContext; `if(L.exitCode!==0) TH("[Session] SessionStart hook failed")` just logs
|
|
200
|
+
SessionEnd: "observe", // executeSessionEndHooks: `for(let r of n) if(r.exitCode!==0) TH("[Session] SessionEnd hook failed")` — logs only
|
|
201
|
+
Notification: "observe", // `await MR("Notification",{…})` — return unassigned
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// ── devin ─────────────────────────────────────────────────────────────────
|
|
205
|
+
// devin 3000.2.17 is a STRIPPED Rust ELF — no readable call sites. Tier is
|
|
206
|
+
// LIVE_PROBE: 11 recorded runs against the real binary with an isolated
|
|
207
|
+
// --config, each verified by side effect (marker file) AND by absence of the
|
|
208
|
+
// reason in ~/.local/share/devin/cli/{transcripts,sessions.db}.
|
|
209
|
+
devin: {
|
|
210
|
+
PreToolUse: "block", // probe L9: "Error: A tool was rejected by the user", RAN_MARKER absent — and it overrode --permission-mode dangerous
|
|
211
|
+
UserPromptSubmit: "block", // probe L10: "Prompt blocked: <reason>"; no PreToolUse/Stop hook fired; no model turn
|
|
212
|
+
Stop: "block", // probe L11 + transcript orchid-pillow.json /steps/8,/steps/10: the reason is injected as a USER message and the agent takes extra turns
|
|
213
|
+
PermissionRequest: "block", // probe L17: {"decision":"block"} -> marker absent, run aborted; L16: {"decision":"approve"} flipped a rejection into an execution, proving the channel is live. ⚠⚠ CONDITIONAL — see §5.1: never fires under --permission-mode dangerous (L8) and never for auto-approved read-only tools (L18, where a secret leaked)
|
|
214
|
+
PostToolUse: "observe", // probe L12: marker created, session finished normally, reason absent from transcript AND sessions.db
|
|
215
|
+
SessionStart: "observe", // probe L13: session ran normally, reason absent from transcript and DB
|
|
216
|
+
SessionEnd: "observe", // probe L14: session ended normally, reason absent
|
|
217
|
+
// Static corroboration (strings @82817071): "Ignoring pre_stop decision type:",
|
|
218
|
+
// "Ignoring permission decision type:", "Blocked by hook", "Denied by hook",
|
|
219
|
+
// "Prevented by hook" — agent-ext/src/hooks/event_handler.rs.
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
// ── antigravity (agy) ─────────────────────────────────────────────────────
|
|
223
|
+
// agy 1.1.8. Two independent kinds of evidence: live probes, plus the
|
|
224
|
+
// embedded FileDescriptorProto (binary @80315228, exa.hooks_pb) which is
|
|
225
|
+
// AUTHORITATIVE about which fields a response can even carry.
|
|
226
|
+
antigravity: {
|
|
227
|
+
PreToolUse: "block", // probe [1]: `touch PROBE_RAN` never ran; agent reported the tool "blocked with … PROBE: denied". Proto: PreToolHookResult{decision, reason, allow_tool, deny_reason}
|
|
228
|
+
Stop: "block", // probe [4]: {"decision":"continue",reason} re-entered the loop AND the agent obeyed the reason (STOP_GATE_PROOF created). Proto: StopHookResult{decision, reason}
|
|
229
|
+
PostToolUse: "observe", // probe [2]: agy logged `unknown field "decision"` (protojson unmarshal failure) 5x and continued. Proto: PostToolHookResult is an EMPTY MESSAGE — nothing is consumable
|
|
230
|
+
UserPromptSubmit: "observe", // PreInvocation. probe [3]: our {"decision":"deny"} caused `failed to call custom pre-invocation hook … unknown field "decision"`; 4 invocations ran unimpeded. Proto: PreInvocationHookResult carries ONLY inject_steps. (instruct works — probe [5] injectSteps honoured)
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
// ── goose ─────────────────────────────────────────────────────────────────
|
|
234
|
+
// block/goose, read AT TAG v1.43.0 (the installed version) as well as HEAD
|
|
235
|
+
// 0b234bdc. Two dispatchers: emit() is fire-and-forget (crates/goose/src/
|
|
236
|
+
// hooks/mod.rs:295 — the return only feeds a warn! log); emit_blocking()
|
|
237
|
+
// (:364) consumes exit 2 or {"decision":"block","reason"} via deny_reason(:429).
|
|
238
|
+
goose: {
|
|
239
|
+
PreToolUse: "block", // v1.43.0 agent.rs:1080 emit_blocking -> HookDecision::Deny -> returns Err(ErrorData "Tool call denied by policy hook `<plugin>`: …"); the tool is never dispatched
|
|
240
|
+
UserPromptSubmit: "observe", // v1.43.0 agent.rs:1609 and :1935 use emit() — verdict discarded
|
|
241
|
+
PostToolUse: "observe", // v1.43.0 agent.rs:~593 with_post_tool_hook uses emit() — verdict discarded, and it is post-hoc
|
|
242
|
+
SessionStart: "observe", // v1.43.0 agent.rs:1595 emit_hook -> emit() — verdict discarded
|
|
243
|
+
SessionEnd: "observe", // goose-cli/src/session/mod.rs:529 and :1294 emit_hook -> emit() — verdict discarded
|
|
244
|
+
// Goose HAS a blocking Stop event we do not install — see §4. CLAUDE.md and
|
|
245
|
+
// the policy-evaluator comment both claim "Goose has NO Stop event"; that is
|
|
246
|
+
// false at v1.43.0 (agent.rs:1956, :2840 emit_stop_hook_blocking).
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Capability for a (cli, event) pair, or `undefined` when not verified.
|
|
252
|
+
*
|
|
253
|
+
* Callers MUST branch on `undefined` and render nothing. Defaulting it to
|
|
254
|
+
* "block" would assert protection nobody confirmed; defaulting to "observe"
|
|
255
|
+
* would tell a user a working policy is inert. Silence is the only honest
|
|
256
|
+
* answer for a pair we have not traced.
|
|
257
|
+
*/
|
|
258
|
+
export function enforcementFor(
|
|
259
|
+
cli: IntegrationType | undefined,
|
|
260
|
+
event: HookEventType | string | undefined,
|
|
261
|
+
): EnforcementCapability | undefined {
|
|
262
|
+
if (!cli || !event) return undefined;
|
|
263
|
+
const perCli = ENFORCEMENT_CAPABILITY[cli];
|
|
264
|
+
if (!perCli) return undefined;
|
|
265
|
+
return perCli[event as HookEventType];
|
|
266
|
+
}
|
package/src/hooks/handler.ts
CHANGED
|
@@ -29,7 +29,7 @@ import type { PolicyFunction, PolicyResult } from "./policy-types";
|
|
|
29
29
|
import { readMergedHooksConfig } from "./hooks-config";
|
|
30
30
|
import { registerBuiltinPolicies } from "./builtin-policies";
|
|
31
31
|
import { evaluatePolicies } from "./policy-evaluator";
|
|
32
|
-
import { clearPolicies, registerPolicy } from "./policy-registry";
|
|
32
|
+
import { clearPolicies, registerPolicy, getPoliciesForEvent } from "./policy-registry";
|
|
33
33
|
import { loadAllCustomHooks } from "./custom-hooks-loader";
|
|
34
34
|
import type { CustomHook } from "./policy-types";
|
|
35
35
|
import { persistHookActivity } from "./hook-activity-store";
|
|
@@ -324,6 +324,16 @@ export async function handleHookEvent(
|
|
|
324
324
|
process.stderr.write(result.stderr);
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
// Which policies actually ran for this event, regardless of how they
|
|
328
|
+
// decided. `result.policyName` names only the decider — null on a plain
|
|
329
|
+
// allow — so without this a row cannot tell "your policy ran and allowed"
|
|
330
|
+
// from "no policy covers this event". The lookup is the same cached call
|
|
331
|
+
// the evaluator already made, so it costs nothing.
|
|
332
|
+
const matchedPolicies = getPoliciesForEvent(
|
|
333
|
+
canonicalEventType,
|
|
334
|
+
parsed.tool_name as string | undefined,
|
|
335
|
+
).map((p) => p.name);
|
|
336
|
+
|
|
327
337
|
// Persist activity to disk (visible in /policies activity tab)
|
|
328
338
|
const activityEntry = {
|
|
329
339
|
timestamp: Date.now(),
|
|
@@ -332,6 +342,7 @@ export async function handleHookEvent(
|
|
|
332
342
|
toolName: (parsed.tool_name as string) ?? null,
|
|
333
343
|
policyName: result.policyName,
|
|
334
344
|
policyNames: result.policyNames,
|
|
345
|
+
matchedPolicies,
|
|
335
346
|
decision: result.decision,
|
|
336
347
|
reason: result.reason,
|
|
337
348
|
durationMs,
|
|
@@ -47,6 +47,20 @@ export interface HookActivityEntry {
|
|
|
47
47
|
toolName: string | null;
|
|
48
48
|
policyName: string | null;
|
|
49
49
|
policyNames?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Every policy that MATCHED this event and ran, whichever way it decided.
|
|
52
|
+
*
|
|
53
|
+
* `policyName` only ever names the policy that *decided* — on a plain
|
|
54
|
+
* `allow()` it is null, so a row could not distinguish "no policy applies
|
|
55
|
+
* here" from "your policy ran and allowed". That is the whole reason a user
|
|
56
|
+
* cannot tell whether a policy fired, and why an observation-only event looks
|
|
57
|
+
* identical to an uncovered one.
|
|
58
|
+
*
|
|
59
|
+
* Optional because rows written before this existed have no such field; every
|
|
60
|
+
* consumer must tolerate `undefined` rather than render an empty list as
|
|
61
|
+
* "nothing ran".
|
|
62
|
+
*/
|
|
63
|
+
matchedPolicies?: string[];
|
|
50
64
|
decision: "allow" | "deny" | "instruct";
|
|
51
65
|
reason: string | null;
|
|
52
66
|
durationMs: number;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
5
5
|
import { resolve, dirname } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import type { HooksConfig } from "./policy-types";
|
|
7
|
+
import type { HooksConfig, ConventionPolicyRecord } from "./policy-types";
|
|
8
8
|
import type { HookScope } from "./types";
|
|
9
9
|
import { hookLogInfo, hookLogWarn } from "./hook-logger";
|
|
10
10
|
|
|
@@ -171,6 +171,64 @@ export function writeScopedHooksConfig(config: HooksConfig, scope: HookScope, cw
|
|
|
171
171
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Mirror the convention policies discovered at `scope` into that scope's
|
|
176
|
+
* config file, so `policies-config.json` shows what is installed rather than
|
|
177
|
+
* only what was explicitly enabled.
|
|
178
|
+
*
|
|
179
|
+
* Wholesale replace, not merge: the point is to reflect the directory as it is
|
|
180
|
+
* now, so a deleted file must vanish from the record. Writes only when the
|
|
181
|
+
* value actually changes, so repeated `failproofai policies` runs do not
|
|
182
|
+
* rewrite the file (and do not churn its mtime) for no reason.
|
|
183
|
+
*
|
|
184
|
+
* Callers must be one-shot commands. This is never safe to call from the hook
|
|
185
|
+
* path — see the `conventionPolicies` doc comment on `HooksConfig`.
|
|
186
|
+
*
|
|
187
|
+
* Returns true when the file was written.
|
|
188
|
+
*/
|
|
189
|
+
export function syncConventionPolicies(
|
|
190
|
+
entries: ConventionPolicyRecord[],
|
|
191
|
+
scope: HookScope,
|
|
192
|
+
cwd?: string,
|
|
193
|
+
): boolean {
|
|
194
|
+
const configPath = getConfigPathForScope(scope, cwd);
|
|
195
|
+
|
|
196
|
+
// Never bring a config file into existence just to record an empty list —
|
|
197
|
+
// that would litter every directory the CLI is run from.
|
|
198
|
+
if (!existsSync(configPath) && entries.length === 0) return false;
|
|
199
|
+
|
|
200
|
+
// NEVER write over a file we could not parse. `readScopedHooksConfig` fails
|
|
201
|
+
// soft, returning `{enabledPolicies: []}` on a syntax error — correct for the
|
|
202
|
+
// hook path, which must not die because a config is malformed, but fatal
|
|
203
|
+
// here: writing that default back silently DESTROYS the user's real settings.
|
|
204
|
+
// A hand-edited file with one stray comma would lose every enabled policy the
|
|
205
|
+
// moment someone ran `failproofai policies`, a read-only command. Bail and
|
|
206
|
+
// leave the file exactly as it is; the parse warning already surfaced.
|
|
207
|
+
if (existsSync(configPath)) {
|
|
208
|
+
try {
|
|
209
|
+
JSON.parse(readFileSync(configPath, "utf8"));
|
|
210
|
+
} catch {
|
|
211
|
+
hookLogWarn(
|
|
212
|
+
`not recording convention policies: ${configPath} is not valid JSON — ` +
|
|
213
|
+
`fix the syntax error and re-run. Nothing was written.`,
|
|
214
|
+
);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const config = readScopedHooksConfig(scope, cwd);
|
|
220
|
+
const sorted = [...entries].sort((a, b) => a.file.localeCompare(b.file));
|
|
221
|
+
const next: HooksConfig = { ...config };
|
|
222
|
+
if (sorted.length === 0) delete next.conventionPolicies;
|
|
223
|
+
else next.conventionPolicies = sorted;
|
|
224
|
+
|
|
225
|
+
if (JSON.stringify(next.conventionPolicies ?? null) === JSON.stringify(config.conventionPolicies ?? null)) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
writeScopedHooksConfig(next, scope, cwd);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
174
232
|
export interface ResolvedLlmConfig {
|
|
175
233
|
baseUrl: string;
|
|
176
234
|
apiKey: string;
|
|
@@ -12,8 +12,10 @@ import { resolve, dirname } from "node:path";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { parseDocument, type Document } from "yaml";
|
|
15
|
+
import { listHermesProfiles, hermesRoot } from "../../lib/hermes-profiles";
|
|
15
16
|
import {
|
|
16
17
|
HOOK_EVENT_TYPES,
|
|
18
|
+
CLAUDE_INSTALL_EVENT_TYPES,
|
|
17
19
|
HOOK_SCOPES,
|
|
18
20
|
CODEX_HOOK_EVENT_TYPES,
|
|
19
21
|
CODEX_HOOK_SCOPES,
|
|
@@ -118,6 +120,18 @@ export interface Integration {
|
|
|
118
120
|
/** Resolve the per-scope settings/hooks file path. */
|
|
119
121
|
getSettingsPath(scope: HookScope, cwd?: string): string;
|
|
120
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Every settings file this integration must write for a scope.
|
|
125
|
+
*
|
|
126
|
+
* Optional — defaults to `[getSettingsPath(scope, cwd)]`, which is right for
|
|
127
|
+
* every CLI whose scope maps to exactly one file. Hermes overrides it because
|
|
128
|
+
* each of its PROFILES is a separate home dir with its own `config.yaml`:
|
|
129
|
+
* writing only `~/.hermes/config.yaml` would leave every other profile running
|
|
130
|
+
* unhooked, silently. Use `settingsPathsFor(integration, …)` at call sites
|
|
131
|
+
* rather than calling this directly.
|
|
132
|
+
*/
|
|
133
|
+
getSettingsPaths?(scope: HookScope, cwd?: string): string[];
|
|
134
|
+
|
|
121
135
|
/** Read the raw settings/hooks file (returns {} when missing). */
|
|
122
136
|
readSettings(settingsPath: string): Record<string, unknown>;
|
|
123
137
|
|
|
@@ -143,13 +157,28 @@ export interface Integration {
|
|
|
143
157
|
detectInstalled(): boolean;
|
|
144
158
|
}
|
|
145
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Every settings file to write/remove for `(integration, scope)`.
|
|
162
|
+
*
|
|
163
|
+
* Single-file integrations (all but Hermes) return exactly one path, so callers
|
|
164
|
+
* can loop unconditionally.
|
|
165
|
+
*/
|
|
166
|
+
export function settingsPathsFor(
|
|
167
|
+
integration: Integration,
|
|
168
|
+
scope: HookScope,
|
|
169
|
+
cwd?: string,
|
|
170
|
+
): string[] {
|
|
171
|
+
const paths = integration.getSettingsPaths?.(scope, cwd);
|
|
172
|
+
return paths && paths.length > 0 ? paths : [integration.getSettingsPath(scope, cwd)];
|
|
173
|
+
}
|
|
174
|
+
|
|
146
175
|
// ── Claude Code integration ─────────────────────────────────────────────────
|
|
147
176
|
|
|
148
177
|
export const claudeCode: Integration = {
|
|
149
178
|
id: "claude",
|
|
150
179
|
displayName: "Claude Code",
|
|
151
180
|
scopes: HOOK_SCOPES,
|
|
152
|
-
eventTypes:
|
|
181
|
+
eventTypes: CLAUDE_INSTALL_EVENT_TYPES,
|
|
153
182
|
|
|
154
183
|
getSettingsPath(scope, cwd) {
|
|
155
184
|
const base = cwd ? resolve(cwd) : process.cwd();
|
|
@@ -196,7 +225,48 @@ export const claudeCode: Integration = {
|
|
|
196
225
|
const s = settings as ClaudeSettings;
|
|
197
226
|
if (!s.hooks) s.hooks = {};
|
|
198
227
|
|
|
199
|
-
|
|
228
|
+
// Drop our hook from any event we no longer install. Without this, an
|
|
229
|
+
// event removed from the list stays in settings.json forever on existing
|
|
230
|
+
// machines — reinstalling would not repair it — which is exactly the
|
|
231
|
+
// situation `WorktreeCreate` created: registered, silent on allow, and
|
|
232
|
+
// therefore breaking `claude --worktree` until hand-edited. Only OUR
|
|
233
|
+
// marked entries are touched; anyone else's hooks on the same event stay.
|
|
234
|
+
//
|
|
235
|
+
// This walks EVERY key the file happens to carry, including ones
|
|
236
|
+
// failproofai has never written — a newer Claude event, another tool's
|
|
237
|
+
// entry, a typo. Their values are therefore unvalidated input, and a
|
|
238
|
+
// non-array one (`"Foo": {}` or `"Foo": "bar"`) makes the iteration below
|
|
239
|
+
// throw. That aborts the whole install AFTER the policies were recorded as
|
|
240
|
+
// enabled, so the user is left believing they are covered while no hook was
|
|
241
|
+
// written at all — the silent non-enforcement this file exists to prevent.
|
|
242
|
+
// Skip anything not shaped like a matcher list and leave it untouched.
|
|
243
|
+
const installed = new Set<string>(CLAUDE_INSTALL_EVENT_TYPES);
|
|
244
|
+
for (const eventType of Object.keys(s.hooks)) {
|
|
245
|
+
if (installed.has(eventType)) continue;
|
|
246
|
+
const matchers = s.hooks[eventType];
|
|
247
|
+
if (!Array.isArray(matchers)) continue;
|
|
248
|
+
// Drop a matcher group only when WE emptied it. A group we never touched
|
|
249
|
+
// (no `hooks` array, or one that held nothing of ours) is somebody else's
|
|
250
|
+
// and is written back exactly as found — pruning is for our own entries,
|
|
251
|
+
// not a cleanup pass over the user's file.
|
|
252
|
+
const kept: ClaudeHookMatcher[] = [];
|
|
253
|
+
for (const matcher of matchers as ClaudeHookMatcher[]) {
|
|
254
|
+
if (!matcher || !Array.isArray(matcher.hooks)) {
|
|
255
|
+
if (matcher) kept.push(matcher);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const before = matcher.hooks.length;
|
|
259
|
+
matcher.hooks = matcher.hooks.filter(
|
|
260
|
+
(h) => !isMarkedHook(h as Record<string, unknown>),
|
|
261
|
+
);
|
|
262
|
+
const weEmptiedIt = matcher.hooks.length === 0 && before > 0;
|
|
263
|
+
if (!weEmptiedIt) kept.push(matcher);
|
|
264
|
+
}
|
|
265
|
+
s.hooks[eventType] = kept;
|
|
266
|
+
if (kept.length === 0) delete s.hooks[eventType];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const eventType of CLAUDE_INSTALL_EVENT_TYPES) {
|
|
200
270
|
const hookEntry = this.buildHookEntry(binaryPath, eventType, scope) as unknown as ClaudeHookEntry;
|
|
201
271
|
if (!s.hooks[eventType]) s.hooks[eventType] = [];
|
|
202
272
|
const matchers: ClaudeHookMatcher[] = s.hooks[eventType];
|
|
@@ -1315,10 +1385,19 @@ export const hermes: Integration = {
|
|
|
1315
1385
|
scopes: HERMES_HOOK_SCOPES,
|
|
1316
1386
|
eventTypes: HERMES_HOOK_EVENT_TYPES,
|
|
1317
1387
|
|
|
1318
|
-
// Hermes config is USER-scope only (
|
|
1319
|
-
// project/local file, so scope/cwd are irrelevant here.
|
|
1388
|
+
// Hermes config is USER-scope only (`<HERMES_HOME>/config.yaml`); there is no
|
|
1389
|
+
// project/local file, so scope/cwd are irrelevant here. This is the DEFAULT
|
|
1390
|
+
// profile's config — see getSettingsPaths for the rest.
|
|
1320
1391
|
getSettingsPath() {
|
|
1321
|
-
return resolve(
|
|
1392
|
+
return resolve(hermesRoot(), "config.yaml");
|
|
1393
|
+
},
|
|
1394
|
+
|
|
1395
|
+
// Every profile is a separate Hermes home with its own config.yaml, so a
|
|
1396
|
+
// single-file install would leave non-default profiles running unhooked —
|
|
1397
|
+
// silently, since Hermes never reports a missing hook. Install/uninstall
|
|
1398
|
+
// therefore cover them all.
|
|
1399
|
+
getSettingsPaths() {
|
|
1400
|
+
return listHermesProfiles().map((p) => resolve(p.home, "config.yaml"));
|
|
1322
1401
|
},
|
|
1323
1402
|
|
|
1324
1403
|
readSettings(settingsPath) {
|
|
@@ -1403,21 +1482,13 @@ export const hermes: Integration = {
|
|
|
1403
1482
|
return removed;
|
|
1404
1483
|
},
|
|
1405
1484
|
|
|
1485
|
+
// "Installed" means EVERY profile is hooked, not just some. A profile created
|
|
1486
|
+
// after install has no hooks, and reporting green there would hide exactly the
|
|
1487
|
+
// silent-unenforcement gap this integration exists to close; reporting "not
|
|
1488
|
+
// installed" instead prompts a re-install, which writes the missing profiles.
|
|
1406
1489
|
hooksInstalledInSettings(scope, cwd) {
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
try {
|
|
1410
|
-
const doc = readYamlDoc(settingsPath);
|
|
1411
|
-
const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
|
|
1412
|
-
const hooks = js.hooks;
|
|
1413
|
-
if (!hooks || typeof hooks !== "object") return false;
|
|
1414
|
-
for (const entries of Object.values(hooks)) {
|
|
1415
|
-
if (Array.isArray(entries) && entries.some((h) => isMarkedHook(h))) return true;
|
|
1416
|
-
}
|
|
1417
|
-
} catch {
|
|
1418
|
-
// Corrupt config — treat as not installed.
|
|
1419
|
-
}
|
|
1420
|
-
return false;
|
|
1490
|
+
const paths = settingsPathsFor(this, scope, cwd);
|
|
1491
|
+
return paths.length > 0 && paths.every((p) => hermesConfigHasHooks(p));
|
|
1421
1492
|
},
|
|
1422
1493
|
|
|
1423
1494
|
detectInstalled() {
|
|
@@ -1425,6 +1496,39 @@ export const hermes: Integration = {
|
|
|
1425
1496
|
},
|
|
1426
1497
|
};
|
|
1427
1498
|
|
|
1499
|
+
function hermesConfigHasHooks(settingsPath: string): boolean {
|
|
1500
|
+
if (!existsSync(settingsPath)) return false;
|
|
1501
|
+
try {
|
|
1502
|
+
const doc = readYamlDoc(settingsPath);
|
|
1503
|
+
const js = (doc.toJS() ?? {}) as { hooks?: Record<string, HermesHookEntry[]> };
|
|
1504
|
+
const hooks = js.hooks;
|
|
1505
|
+
if (!hooks || typeof hooks !== "object") return false;
|
|
1506
|
+
for (const entries of Object.values(hooks)) {
|
|
1507
|
+
if (Array.isArray(entries) && entries.some((h) => isMarkedHook(h))) return true;
|
|
1508
|
+
}
|
|
1509
|
+
} catch {
|
|
1510
|
+
// Corrupt config — treat as not installed.
|
|
1511
|
+
}
|
|
1512
|
+
return false;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* Profiles that exist but have no failproofai hooks — i.e. Hermes instances
|
|
1517
|
+
* currently running unenforced. Empty when Hermes isn't installed at all.
|
|
1518
|
+
*
|
|
1519
|
+
* `listHermesProfiles()` always seeds a `default` entry so install has a path to
|
|
1520
|
+
* write even on a fresh machine, so this filters to homes that actually exist —
|
|
1521
|
+
* otherwise every machine WITHOUT Hermes would report one unhooked profile and
|
|
1522
|
+
* the wizard would replace its accurate "not on PATH" hint with a phantom
|
|
1523
|
+
* warning.
|
|
1524
|
+
*/
|
|
1525
|
+
export function unhookedHermesProfiles(): string[] {
|
|
1526
|
+
return listHermesProfiles()
|
|
1527
|
+
.filter((p) => existsSync(p.home))
|
|
1528
|
+
.filter((p) => !hermesConfigHasHooks(resolve(p.home, "config.yaml")))
|
|
1529
|
+
.map((p) => p.name);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1428
1532
|
// ── OpenClaw integration ────────────────────────────────────────────────────
|
|
1429
1533
|
//
|
|
1430
1534
|
// OpenClaw is a self-hosted assistant gateway. Enforcement is via its in-process
|