failproofai 1.0.2-beta.3 → 1.0.2-beta.5
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 +1 -1
- 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.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 +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.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 +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -3
- 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 +5 -6
- 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 +2 -2
- 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 +3 -4
- 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 +4 -4
- 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 +4 -4
- 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.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 +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- 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.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 +14 -14
- 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 +2 -2
- 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 +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 +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.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 +3 -3
- 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]__0e32-7c._.js → [externals]__1qzqcio._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__03r-lqq._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.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]__0oy4kxb._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12021ot._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0igxt32._.js → [root-of-the-server]__1p8poxd._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1zkek3z._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +3 -0
- package/.next/standalone/.next/server/chunks/_12moi5-._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +6 -5
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.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_04op_ou._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js} +1 -1
- 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/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js} +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1m84s36._.js → [root-of-the-server]__00-s7h8._.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]__07gm3zl._.js +4 -4
- 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]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__08zpry-._.js → [root-of-the-server]__0stn0gx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{src_hooks_08o7bqo._.js → [root-of-the-server]__15r6r7w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0jzbk4_._.js → [root-of-the-server]__190won8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +1 -1
- 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]__0rsd3ll._.js → [root-of-the-server]__1sc84bb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tag8_-._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1db3-8n._.js → _1if9ixi._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_11opd9e._.js → _1kesqs-._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_15xtekm._.js → _1km9-gn._.js} +1 -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/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_1cf5ya2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_15bjxkm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
- 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/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/middleware.js +1 -1
- 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/0tyt-3a5rtzy8.js +1 -0
- package/.next/standalone/.next/static/chunks/{1mk3vmmc673hu.js → 21r7iw3rmixf5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{10pok-mrqv426.js → 26lfjhb2kzzgb.js} +1 -1
- package/.next/standalone/.next/static/chunks/2_27jraat038x.js +1 -0
- package/.next/standalone/.next/static/chunks/2hwc8e923g5df.js +1 -0
- package/.next/standalone/.next/static/chunks/{3cs_rf5p765v0.js → 33np9nrh1mvj9.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1oml7c7env4-g.js → 354-n7p7labpn.js} +1 -1
- package/.next/standalone/.next/static/chunks/36-8i_vbkh85w.js +1 -0
- package/.next/standalone/.next/static/chunks/{2jsomd79g7kn0.js → 391tpdqcas7rq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1yywl_a87108w.js → 3_b73tc78jwsq.js} +2 -2
- package/.next/standalone/.next/static/chunks/{1za08zjcctyil.js → 3i8rg2fv9nd6e.js} +2 -2
- package/.next/standalone/.next/static/chunks/{2ey_ih6j6cm15.js → 3jqm8ubpv3i-4.js} +1 -1
- package/.next/standalone/.next/static/chunks/turbopack-2x-o099q64i8h.js +1 -0
- package/.next/standalone/SECURITY.md +18 -3
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +7 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +6 -6
- package/.next/standalone/lib/opencode-db.ts +77 -0
- package/.next/standalone/lib/opencode-projects.ts +37 -41
- package/.next/standalone/lib/opencode-sessions.ts +59 -36
- 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/client/components/segment-cache/cache.js +23 -34
- 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/pages-turbo.runtime.prod.js +1 -1
- 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/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.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 +19 -19
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/uv.lock +23 -23
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +151 -60
- package/dist/cli.mjs +3670 -3005
- package/dist/worker.mjs +189 -164
- package/lib/opencode-db.ts +77 -0
- package/lib/opencode-projects.ts +37 -41
- package/lib/opencode-sessions.ts +59 -36
- package/package.json +19 -19
- package/src/audit/cache.ts +201 -40
- package/src/audit/cli-adapters/claude.ts +111 -1
- package/src/audit/cli-adapters/index.ts +31 -1
- package/src/audit/index.ts +131 -11
- package/src/audit/replay.ts +15 -91
- package/src/hooks/configure-wizard.ts +16 -48
- package/src/hooks/handler.ts +8 -1
- package/src/hooks/manager.ts +72 -3
- package/src/hooks/pack-cli.ts +793 -45
- package/src/hooks/pack-manifest.ts +28 -0
- package/src/hooks/pack-store.ts +8 -25
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l-khtb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mhmdzs._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +0 -3
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +0 -3
- package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +0 -12
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +0 -5
- package/.next/standalone/.next/static/chunks/01nuy-aqshahr.js +0 -1
- package/.next/standalone/.next/static/chunks/1pl7h8vllvoi2.js +0 -1
- package/.next/standalone/.next/static/chunks/2efnffq2ipgr_.js +0 -1
- package/.next/standalone/.next/static/chunks/2gd07pinz0i7f.js +0 -1
- package/.next/standalone/.next/static/chunks/turbopack-3kjgdgwci4tqf.js +0 -1
- /package/.next/standalone/.next/static/{G-faMkpZioETgewZ4X4wX → 59jsH11LoyXu9UBoHvgGM}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{G-faMkpZioETgewZ4X4wX → 59jsH11LoyXu9UBoHvgGM}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{G-faMkpZioETgewZ4X4wX → 59jsH11LoyXu9UBoHvgGM}/_ssgManifest.js +0 -0
package/src/audit/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { INTEGRATION_TYPES, type IntegrationType } from "../hooks/types";
|
|
|
15
15
|
import { ADAPTERS } from "./cli-adapters";
|
|
16
16
|
import { AUDIT_DETECTORS } from "./detectors";
|
|
17
17
|
import { severityForBuiltin } from "./features";
|
|
18
|
-
import {
|
|
18
|
+
import { readCachedTranscript, writeCachedTranscriptResult } from "./cache";
|
|
19
19
|
import { initReplay, replayEvent, restoreReplay } from "./replay";
|
|
20
20
|
import {
|
|
21
21
|
AUDIT_EXAMPLE_MAX_CHARS,
|
|
@@ -87,7 +87,24 @@ function parseSinceOpt(since: string | undefined): number | undefined {
|
|
|
87
87
|
throw new Error(`Invalid --since value: "${since}" (expected e.g. "7d", "30d", or "2026-04-01")`);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* What a scan covered, alongside the result — so the caller can record a resume
|
|
92
|
+
* point without re-deriving it.
|
|
93
|
+
*
|
|
94
|
+
* `bytesScanned` is 0 when the source cannot be resumed at all (a database, or
|
|
95
|
+
* an adapter with no `streamEventsFrom`), which is what stops a resume point
|
|
96
|
+
* being written for something that has no bytes to resume from.
|
|
97
|
+
*/
|
|
98
|
+
interface ScanOutcome {
|
|
99
|
+
result: TranscriptAuditResult;
|
|
100
|
+
bytesScanned: number;
|
|
101
|
+
detectorState: DetectorSessionState;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function scanOneTranscript(
|
|
105
|
+
meta: TranscriptMetadata,
|
|
106
|
+
resume?: { fromByte: number; detectorState: DetectorSessionState },
|
|
107
|
+
): Promise<ScanOutcome> {
|
|
91
108
|
const empty: TranscriptAuditResult = {
|
|
92
109
|
transcriptPath: meta.transcriptPath,
|
|
93
110
|
cli: meta.cli,
|
|
@@ -102,17 +119,54 @@ async function scanOneTranscript(meta: TranscriptMetadata): Promise<TranscriptAu
|
|
|
102
119
|
rangeByName: {},
|
|
103
120
|
};
|
|
104
121
|
|
|
122
|
+
const adapter = ADAPTERS[meta.cli];
|
|
105
123
|
// Stream failures must propagate so the orchestrator counts them in
|
|
106
124
|
// `errors` rather than silently returning an empty hits map.
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
let events: NormalizedToolEvent[];
|
|
126
|
+
let bytesScanned = 0;
|
|
127
|
+
// A resumed scan carries the stateful detectors' state forward across the
|
|
128
|
+
// boundary. Starting them empty would silently change what they find — the
|
|
129
|
+
// re-read detector's countdown spans tool calls, so an edit before the offset
|
|
130
|
+
// and its wasteful re-read after it would stop being a pair.
|
|
131
|
+
const sessionState: DetectorSessionState = resume?.detectorState ?? {};
|
|
132
|
+
|
|
133
|
+
if (adapter.streamEventsFrom) {
|
|
134
|
+
// A full scan is a resume from zero, deliberately — not a separate branch.
|
|
135
|
+
//
|
|
136
|
+
// The separate branch is what got this wrong: it recorded `meta.sizeBytes`
|
|
137
|
+
// as scanned, which is the whole FILE, including a trailing partial line
|
|
138
|
+
// that was never parsed. Transcripts are appended to while the audit reads
|
|
139
|
+
// them, so that tail is routinely half a line — and the next run then
|
|
140
|
+
// resumed past an event nobody had read. Only the reader knows where the
|
|
141
|
+
// last complete line ended, so only the reader gets to say.
|
|
142
|
+
const scan = await adapter.streamEventsFrom(meta, resume?.fromByte ?? 0);
|
|
143
|
+
// Null means it could not be read from there after all — truncated,
|
|
144
|
+
// unreadable, unparseable. A thrown error would count as a scan failure,
|
|
145
|
+
// so fall back to a full read instead and keep the transcript scannable.
|
|
146
|
+
if (!scan) {
|
|
147
|
+
events = await adapter.streamEvents(meta);
|
|
148
|
+
bytesScanned = 0;
|
|
149
|
+
} else {
|
|
150
|
+
events = scan.events;
|
|
151
|
+
bytesScanned = scan.bytesConsumed;
|
|
152
|
+
empty.cwd = scan.cwd ?? "";
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// A source with no byte offsets to speak of — a database. Scanned whole,
|
|
156
|
+
// every time, and given no resume point to mislead the next run with.
|
|
157
|
+
events = await adapter.streamEvents(meta);
|
|
158
|
+
bytesScanned = 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (events.length === 0) {
|
|
162
|
+
return { result: empty, bytesScanned, detectorState: sessionState };
|
|
163
|
+
}
|
|
109
164
|
|
|
110
165
|
const result = empty;
|
|
111
166
|
result.eventsScanned = events.length;
|
|
112
167
|
// Capture the session's cwd from the first event that carried one — every
|
|
113
168
|
// event in a single transcript shares the same cwd by construction.
|
|
114
|
-
result.cwd = events[0].cwd || "";
|
|
115
|
-
const sessionState: DetectorSessionState = {};
|
|
169
|
+
result.cwd = result.cwd || events[0].cwd || "";
|
|
116
170
|
|
|
117
171
|
for (const event of events) {
|
|
118
172
|
// Run audit detectors first (stateful, must see every event).
|
|
@@ -146,7 +200,7 @@ async function scanOneTranscript(meta: TranscriptMetadata): Promise<TranscriptAu
|
|
|
146
200
|
}
|
|
147
201
|
}
|
|
148
202
|
|
|
149
|
-
return result;
|
|
203
|
+
return { result, bytesScanned, detectorState: sessionState };
|
|
150
204
|
}
|
|
151
205
|
|
|
152
206
|
function formatPolicyExample(_policyName: string, event: NormalizedToolEvent): string {
|
|
@@ -159,6 +213,55 @@ function formatPolicyExample(_policyName: string, event: NormalizedToolEvent): s
|
|
|
159
213
|
return `${event.toolName}`;
|
|
160
214
|
}
|
|
161
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Fold a tail-scan into the result the cache already held for the same
|
|
218
|
+
* transcript.
|
|
219
|
+
*
|
|
220
|
+
* Mirrors `recordHit` exactly, and has to: counts add, examples stay capped at
|
|
221
|
+
* the same limit, ranges widen. The one asymmetry is `cwd` — the cached value
|
|
222
|
+
* came from the FIRST event in the file and the tail's came from the first
|
|
223
|
+
* event after the offset, so the older one wins.
|
|
224
|
+
*/
|
|
225
|
+
function mergeIncremental(
|
|
226
|
+
cached: TranscriptAuditResult,
|
|
227
|
+
tail: TranscriptAuditResult,
|
|
228
|
+
): TranscriptAuditResult {
|
|
229
|
+
const out: TranscriptAuditResult = {
|
|
230
|
+
...cached,
|
|
231
|
+
mtimeMs: tail.mtimeMs,
|
|
232
|
+
sizeBytes: tail.sizeBytes,
|
|
233
|
+
cwd: cached.cwd || tail.cwd || "",
|
|
234
|
+
eventsScanned: (cached.eventsScanned ?? 0) + (tail.eventsScanned ?? 0),
|
|
235
|
+
hitsByName: { ...cached.hitsByName },
|
|
236
|
+
examplesByName: {},
|
|
237
|
+
rangeByName: { ...cached.rangeByName },
|
|
238
|
+
};
|
|
239
|
+
for (const [name, list] of Object.entries(cached.examplesByName)) {
|
|
240
|
+
out.examplesByName[name] = [...list];
|
|
241
|
+
}
|
|
242
|
+
for (const [name, count] of Object.entries(tail.hitsByName)) {
|
|
243
|
+
out.hitsByName[name] = (out.hitsByName[name] ?? 0) + count;
|
|
244
|
+
}
|
|
245
|
+
for (const [name, list] of Object.entries(tail.examplesByName)) {
|
|
246
|
+
const exs = out.examplesByName[name] ?? [];
|
|
247
|
+
for (const ex of list) {
|
|
248
|
+
if (exs.length >= AUDIT_MAX_EXAMPLES_PER_NAME) break;
|
|
249
|
+
exs.push(ex);
|
|
250
|
+
}
|
|
251
|
+
out.examplesByName[name] = exs;
|
|
252
|
+
}
|
|
253
|
+
for (const [name, range] of Object.entries(tail.rangeByName)) {
|
|
254
|
+
const existing = out.rangeByName[name];
|
|
255
|
+
if (!existing) {
|
|
256
|
+
out.rangeByName[name] = { ...range };
|
|
257
|
+
} else {
|
|
258
|
+
if (range.first < existing.first) existing.first = range.first;
|
|
259
|
+
if (range.last > existing.last) existing.last = range.last;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
162
265
|
function recordHit(
|
|
163
266
|
result: TranscriptAuditResult,
|
|
164
267
|
name: string,
|
|
@@ -300,14 +403,31 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
|
|
|
300
403
|
let skipped = 0;
|
|
301
404
|
let errors = 0;
|
|
302
405
|
const tasks = allTranscripts.map((meta) => async (): Promise<TranscriptAuditResult> => {
|
|
406
|
+
let resume: { fromByte: number; detectorState: DetectorSessionState } | undefined;
|
|
407
|
+
let cachedPrefix: TranscriptAuditResult | null = null;
|
|
303
408
|
if (!opts.noCache) {
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
409
|
+
const found = readCachedTranscript(meta.transcriptPath, meta.mtimeMs, meta.sizeBytes);
|
|
410
|
+
if (found?.kind === "hit") return found.result;
|
|
411
|
+
resume = found?.kind === "resume"
|
|
412
|
+
? { fromByte: found.fromByte, detectorState: found.detectorState }
|
|
413
|
+
: undefined;
|
|
414
|
+
cachedPrefix = found?.kind === "resume" ? found.result : null;
|
|
306
415
|
}
|
|
307
416
|
try {
|
|
308
|
-
const
|
|
417
|
+
const scan = await scanOneTranscript(meta, resume);
|
|
418
|
+
// A resumed scan produced hits for the TAIL only; the cached result holds
|
|
419
|
+
// everything before it. Merging is what makes the two halves one answer.
|
|
420
|
+
const fresh = cachedPrefix ? mergeIncremental(cachedPrefix, scan.result) : scan.result;
|
|
309
421
|
if (!opts.noCache) {
|
|
310
|
-
writeCachedTranscriptResult(
|
|
422
|
+
writeCachedTranscriptResult(
|
|
423
|
+
meta.transcriptPath,
|
|
424
|
+
meta.mtimeMs,
|
|
425
|
+
meta.sizeBytes,
|
|
426
|
+
fresh,
|
|
427
|
+
scan.bytesScanned > 0
|
|
428
|
+
? { bytesScanned: scan.bytesScanned, detectorState: scan.detectorState }
|
|
429
|
+
: undefined,
|
|
430
|
+
);
|
|
311
431
|
}
|
|
312
432
|
return fresh;
|
|
313
433
|
} catch {
|
package/src/audit/replay.ts
CHANGED
|
@@ -13,19 +13,13 @@
|
|
|
13
13
|
* execSync against live git, so they never fire on PreToolUse/PostToolUse
|
|
14
14
|
* replay — no explicit skip needed.
|
|
15
15
|
*/
|
|
16
|
-
import { resolve } from "node:path";
|
|
17
|
-
import { existsSync } from "node:fs";
|
|
18
16
|
import type { EvaluationResult } from "../hooks/policy-evaluator";
|
|
19
17
|
import { evaluatePolicies } from "../hooks/policy-evaluator";
|
|
20
18
|
import { BUILTIN_POLICIES, registerBuiltinPolicies } from "../hooks/builtin-policies";
|
|
21
|
-
import { PACK_ENTRY_ASSET, bundledPackDir } from "../hooks/pack-store";
|
|
22
|
-
import { loadCustomHooks } from "../hooks/custom-hooks-loader";
|
|
23
|
-
import { clearCustomHooks } from "../hooks/custom-hooks-registry";
|
|
24
19
|
import {
|
|
25
20
|
clearPolicies,
|
|
26
21
|
getAllPolicies,
|
|
27
22
|
normalizePolicyName,
|
|
28
|
-
registerPolicy,
|
|
29
23
|
setAllPolicies,
|
|
30
24
|
} from "../hooks/policy-registry";
|
|
31
25
|
import type { RegisteredPolicy } from "../hooks/policy-types";
|
|
@@ -46,10 +40,20 @@ let initialized = false;
|
|
|
46
40
|
* (e.g. the Next.js dashboard) doesn't wipe any prior policy registrations. */
|
|
47
41
|
let savedSnapshot: RegisteredPolicy[] | null = null;
|
|
48
42
|
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
43
|
+
/**
|
|
44
|
+
* Register every builtin policy (regardless of user config) so the replay shows
|
|
45
|
+
* what *could* be caught, not just what is currently enabled. Called once per
|
|
46
|
+
* `runAudit` invocation. Snapshots the existing registry so `restoreReplay()`
|
|
47
|
+
* can put it back.
|
|
48
|
+
*
|
|
49
|
+
* The implementations compiled into this build, and ONLY those. The audit used
|
|
50
|
+
* to prefer a vendored `policy-pack/` copy where one existed, which was
|
|
51
|
+
* meaningful while the package shipped that directory; it no longer ships it, so
|
|
52
|
+
* that branch could not fire in any published build and existed only to be
|
|
53
|
+
* misread as "the audit scores against installed packs". It does not, and must
|
|
54
|
+
* not: an audit is a fixed yardstick, and one that changed shape with whatever
|
|
55
|
+
* pack a machine happened to have could not be compared against its own history.
|
|
56
|
+
*/
|
|
53
57
|
export async function initReplay(): Promise<void> {
|
|
54
58
|
if (initialized) return;
|
|
55
59
|
savedSnapshot = getAllPolicies();
|
|
@@ -57,90 +61,10 @@ export async function initReplay(): Promise<void> {
|
|
|
57
61
|
const enabled = BUILTIN_POLICIES
|
|
58
62
|
.map((p) => p.name)
|
|
59
63
|
.filter((n) => !SKIP_POLICIES.has(normalizePolicyName(n)));
|
|
60
|
-
|
|
64
|
+
registerBuiltinPolicies(enabled);
|
|
61
65
|
initialized = true;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
/**
|
|
65
|
-
* Register the audit's policies from the pack that ships inside the package,
|
|
66
|
-
* rather than from implementations compiled into this build.
|
|
67
|
-
*
|
|
68
|
-
* The audit scores by RUNNING the policies — three of its four penalty buckets
|
|
69
|
-
* are replay hits — so the day the builtins stop being compiled in, this is
|
|
70
|
-
* what keeps `failproofai audit` reporting the same findings and the same score
|
|
71
|
-
* for the same transcripts. The pack carries the identical functions:
|
|
72
|
-
* `builtin-pack-conformance.test.ts` replays a corpus through both and asserts
|
|
73
|
-
* the verdicts do not diverge.
|
|
74
|
-
*
|
|
75
|
-
* Two things it must get right, or the audit quietly changes:
|
|
76
|
-
*
|
|
77
|
-
* - The pack cannot carry `block-failproofai-commands`, because `alwaysOn` is
|
|
78
|
-
* refused by the pack loader by design — a downloaded file that no local
|
|
79
|
-
* command can switch off. It is registered from the compiled side, so the
|
|
80
|
-
* replayed set is the same 39 either way.
|
|
81
|
-
* - The skip list still applies. `warn-repeated-tool-calls` writes a per-session
|
|
82
|
-
* sidecar into the user's real transcript directory on every evaluation.
|
|
83
|
-
*
|
|
84
|
-
* Returns false when there is no vendored pack to read — a source checkout that
|
|
85
|
-
* has not run `build:pack`, or a tarball packed without it — and the caller
|
|
86
|
-
* falls back to the compiled implementations. An audit that silently scored on
|
|
87
|
-
* fewer policies would be worse than either.
|
|
88
|
-
*/
|
|
89
|
-
async function registerFromVendoredPack(enabled: string[]): Promise<boolean> {
|
|
90
|
-
const dir = bundledPackDir();
|
|
91
|
-
if (!dir) return false;
|
|
92
|
-
const entry = resolve(dir, PACK_ENTRY_ASSET);
|
|
93
|
-
if (!existsSync(entry)) return false;
|
|
94
|
-
|
|
95
|
-
const wanted = new Set(enabled);
|
|
96
|
-
let hooks;
|
|
97
|
-
try {
|
|
98
|
-
clearCustomHooks();
|
|
99
|
-
hooks = await loadCustomHooks(entry, { strict: true });
|
|
100
|
-
} catch {
|
|
101
|
-
return false;
|
|
102
|
-
} finally {
|
|
103
|
-
clearCustomHooks();
|
|
104
|
-
}
|
|
105
|
-
if (hooks.length === 0) return false;
|
|
106
|
-
const fromPack = new Map(hooks.map((hook) => [hook.name, hook]));
|
|
107
|
-
|
|
108
|
-
// Registered in CATALOG ORDER, taking the pack's function where it has one and
|
|
109
|
-
// the compiled one for the guard the pack may not carry.
|
|
110
|
-
//
|
|
111
|
-
// Order is not cosmetic here. `getPoliciesForEvent` sorts by priority only and
|
|
112
|
-
// V8's sort is stable, so registration order decides EVALUATION order, and
|
|
113
|
-
// `evaluatePolicies` stops at the first deny — which means the order decides
|
|
114
|
-
// which policy is CREDITED for a hit. Appending the always-on guard after the
|
|
115
|
-
// pack instead of at its catalog position moved it from index 11 to last, and
|
|
116
|
-
// re-attributed 3 events in a 23,477-event corpus: 456 -> 453 for the guard,
|
|
117
|
-
// with the denies it used to shadow surfacing as block-force-push and
|
|
118
|
-
// block-gh-pipeline. The audit's per-policy counts are what a user reads, so
|
|
119
|
-
// that is a changed audit, not a changed internal.
|
|
120
|
-
let registered = 0;
|
|
121
|
-
for (const policy of BUILTIN_POLICIES) {
|
|
122
|
-
if (!wanted.has(policy.name)) continue;
|
|
123
|
-
const packHook = fromPack.get(policy.name);
|
|
124
|
-
if (policy.alwaysOn || !packHook) {
|
|
125
|
-
registerPolicy(policy.name, policy.description, policy.fn, policy.match, 0, policy.params);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
registerPolicy(
|
|
129
|
-
packHook.name,
|
|
130
|
-
packHook.description ?? policy.description,
|
|
131
|
-
packHook.fn,
|
|
132
|
-
packHook.match ?? policy.match,
|
|
133
|
-
0,
|
|
134
|
-
policy.params,
|
|
135
|
-
);
|
|
136
|
-
registered += 1;
|
|
137
|
-
}
|
|
138
|
-
// Nothing came from the pack: it loaded but carried none of the names the
|
|
139
|
-
// audit replays. Falling back is more honest than scoring on the compiled set
|
|
140
|
-
// while claiming the pack lane ran.
|
|
141
|
-
return registered > 0;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
68
|
/** Restore the registry to whatever was there before `initReplay()`. Safe to
|
|
145
69
|
* call when not initialized (no-op). Always paired with `initReplay()` in a
|
|
146
70
|
* try/finally inside `runAudit()`. */
|
|
@@ -32,7 +32,6 @@ import { dirname, resolve, sep } from "node:path";
|
|
|
32
32
|
|
|
33
33
|
import {
|
|
34
34
|
selectOne,
|
|
35
|
-
multiSelect,
|
|
36
35
|
promptText,
|
|
37
36
|
intro,
|
|
38
37
|
outro,
|
|
@@ -223,7 +222,6 @@ export function buildAgentChoices(scope: HookScope, cwd: string): MultiChoice<In
|
|
|
223
222
|
});
|
|
224
223
|
}
|
|
225
224
|
|
|
226
|
-
const ALL_CLIS = "__all_clis__";
|
|
227
225
|
|
|
228
226
|
const DIM_NOTE = "(auto-loaded)";
|
|
229
227
|
|
|
@@ -938,54 +936,24 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
938
936
|
// An "Everything available" row protects every supported CLI (detected +
|
|
939
937
|
// set-up-ahead); when ticked it wins over the individual boxes.
|
|
940
938
|
//
|
|
941
|
-
//
|
|
942
|
-
// cannot make for you: which agents on this machine should be guarded. The
|
|
943
|
-
// detected ones are pre-ticked, so the common answer is a single keypress.
|
|
939
|
+
// Every supported agent, detected or not — setup asks nothing about this.
|
|
944
940
|
//
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
// under project scope is what crashed the apply on Hermes.
|
|
957
|
-
hint: `protect all ${clisSupportingScope(primaryScope).length} CLIs configurable here`,
|
|
958
|
-
// A selector, not a harness. Counting it gave "13 harnesses" for
|
|
959
|
-
// the 12 supported CLIs, and listed "Everything available" among them.
|
|
960
|
-
summaryExclude: true,
|
|
961
|
-
},
|
|
962
|
-
...buildAgentChoices(primaryScope, cwd),
|
|
963
|
-
],
|
|
964
|
-
minSelected: 1,
|
|
965
|
-
summaryNoun: "harnesses",
|
|
966
|
-
hint: "detected CLIs are pre-selected · space toggles · ctrl+a all · ↵ confirm",
|
|
967
|
-
stdin,
|
|
968
|
-
stdout,
|
|
969
|
-
});
|
|
970
|
-
if (picked === null) return cancel();
|
|
971
|
-
clisSel = picked;
|
|
972
|
-
}
|
|
973
|
-
// Filter to what the chosen scopes support in BOTH branches: "Everything
|
|
974
|
-
// available" must not expand to CLIs that cannot take any selected scope,
|
|
975
|
-
// and a locked row can't be ticked but belt-and-braces keeps the invariant
|
|
976
|
-
// local to the one place `clis` is built.
|
|
941
|
+
// It used to be the one question left, on the reasoning that which agents to
|
|
942
|
+
// guard is a real choice. It stopped being one when this package stopped
|
|
943
|
+
// shipping policies: hooks alone enforce nothing, so wiring them everywhere
|
|
944
|
+
// costs a config entry and changes no behaviour until a pack arrives. What it
|
|
945
|
+
// buys is that an agent installed NEXT WEEK is guarded from its first tool
|
|
946
|
+
// call, instead of running unguarded until somebody remembers to re-run setup
|
|
947
|
+
// — and nobody remembers, because nothing tells them to.
|
|
948
|
+
//
|
|
949
|
+
// ALL twelve, not the detected ones, for exactly that reason. The cost is
|
|
950
|
+
// honest and worth naming: hook config appears under `~/.cursor/`,
|
|
951
|
+
// `~/.factory/` and the rest for agents that may never be installed.
|
|
977
952
|
//
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
//
|
|
981
|
-
|
|
982
|
-
// skips what a given scope cannot take.
|
|
983
|
-
const supported = new Set(scopes.flatMap((s) => clisSupportingScope(s)));
|
|
984
|
-
const clis: IntegrationType[] = (
|
|
985
|
-
clisSel.includes(ALL_CLIS)
|
|
986
|
-
? [...INTEGRATION_TYPES]
|
|
987
|
-
: (clisSel.filter((v) => v !== ALL_CLIS) as IntegrationType[])
|
|
988
|
-
).filter((id) => supported.has(id));
|
|
953
|
+
// Which policies run, and on which of these agents, is chosen at
|
|
954
|
+
// `failproofai policies add` — where the user is looking at a real list
|
|
955
|
+
// instead of answering in the abstract.
|
|
956
|
+
const clis: IntegrationType[] = [...clisSupportingScope(primaryScope)];
|
|
989
957
|
|
|
990
958
|
// 4 — Connect this machine? Last, because by this point the user has decided
|
|
991
959
|
// what to protect, so "would you like to see it in a dashboard?" follows
|
package/src/hooks/handler.ts
CHANGED
|
@@ -425,9 +425,16 @@ export async function evaluateHookEvent(
|
|
|
425
425
|
if (!cloudManaged && policyId && disabledCustomPolicies.has(policyId)) continue;
|
|
426
426
|
// Same rule for a session pause — it suspends local policy, never cloud.
|
|
427
427
|
if (!cloudManaged && activePause) continue;
|
|
428
|
+
// A pack narrowed to particular agents does not fire on the others.
|
|
429
|
+
// Setup wires hooks into every supported CLI — hooks alone enforce
|
|
430
|
+
// nothing — so the choice of which agents a pack guards is made when the
|
|
431
|
+
// pack is installed, and this is where it takes effect. `clis: null`
|
|
432
|
+
// means every agent, which is what an install with no narrowing writes
|
|
433
|
+
// and what every pack installed before this field existed reads as.
|
|
434
|
+
if (pack?.clis && !pack.clis.includes(cli)) continue;
|
|
428
435
|
// A pack's artifact registers everything it contains; the user may have
|
|
429
436
|
// taken only some of it. `enabled: null` means the whole pack, which is
|
|
430
|
-
// what `
|
|
437
|
+
// what `policies add` writes when no selection was made.
|
|
431
438
|
if (pack?.enabled && !pack.enabled.includes(hook.name)) continue;
|
|
432
439
|
// A pack policy whose name is an ENABLED BUILTIN is the same guard
|
|
433
440
|
// twice. The two register under different keys — `failproofai/block-sudo`
|
package/src/hooks/manager.ts
CHANGED
|
@@ -9,12 +9,12 @@ import { execSync } from "node:child_process";
|
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { resolve, basename } from "node:path";
|
|
11
11
|
import { platform, arch, release, hostname } from "node:os";
|
|
12
|
-
import {
|
|
12
|
+
import { INTEGRATION_TYPES,
|
|
13
13
|
HOOK_SCOPES,
|
|
14
14
|
type HookScope,
|
|
15
15
|
type IntegrationType,
|
|
16
16
|
} from "./types";
|
|
17
|
-
import { claudeCode, getIntegration, settingsPathsFor } from "./integrations";
|
|
17
|
+
import { claudeCode, getIntegration, settingsPathsFor, type Integration } from "./integrations";
|
|
18
18
|
import { promptPolicySelection } from "./install-prompt";
|
|
19
19
|
import { configuredCustomPolicyPaths, readMergedHooksConfig, readScopedHooksConfig, writeScopedHooksConfig, syncConventionPolicies, findProjectConfigDir } from "./hooks-config";
|
|
20
20
|
import type { HooksConfig, ConventionPolicyRecord } from "./policy-types";
|
|
@@ -256,8 +256,77 @@ function deduplicateScopes(scopes: readonly HookScope[], cwd?: string): HookScop
|
|
|
256
256
|
});
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Is ANY agent CLI wired to call failproofai at this scope?
|
|
261
|
+
*
|
|
262
|
+
* Asked Claude Code and only Claude Code, which made it wrong for every machine
|
|
263
|
+
* guarded through one of the other eleven. A user with hooks in `~/.codex/` was
|
|
264
|
+
* told nothing was installed — quietly, while this only tinted a subtitle, and
|
|
265
|
+
* loudly once the listing started warning that every policy shown was inert.
|
|
266
|
+
* Reported from a real machine set up for codex.
|
|
267
|
+
*
|
|
268
|
+
* Any one integration answering yes is enough: the question is whether
|
|
269
|
+
* enforcement can reach this machine at all, not whether a particular agent is
|
|
270
|
+
* covered. Which agents specifically is a different question, and the per-CLI
|
|
271
|
+
* rows of the listing already answer it.
|
|
272
|
+
*/
|
|
259
273
|
export function hooksInstalledInSettings(scope: HookScope, cwd?: string): boolean {
|
|
260
|
-
return
|
|
274
|
+
return integrationsInstalledAt(scope, cwd).length > 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Which integrations are wired at EXACTLY this scope — the honest form of the
|
|
279
|
+
* question above, and the one the multi-scope warning needs.
|
|
280
|
+
*
|
|
281
|
+
* Two ways an integration answers yes for a scope it is not actually installed
|
|
282
|
+
* at, both of which made `failproofai policies` warn about "hooks in multiple
|
|
283
|
+
* scopes" on a machine whose hooks are in exactly one file:
|
|
284
|
+
*
|
|
285
|
+
* - It does not SUPPORT the scope. Hermes and OpenClaw are user-scope only
|
|
286
|
+
* (`HERMES_HOOK_SCOPES = ["user"]`) and their `getSettingsPath` ignores the
|
|
287
|
+
* scope argument entirely, so they hand back the user file — and report it
|
|
288
|
+
* installed — for `project` and `local` alike. `scopes` already declares
|
|
289
|
+
* this; nothing was reading it.
|
|
290
|
+
* - Its project path RESOLVES to its user path. Run this from `$HOME` and
|
|
291
|
+
* `<cwd>/.claude/settings.json` is `~/.claude/settings.json`: one file,
|
|
292
|
+
* counted as two scopes. Cheap to detect and impossible to get right by
|
|
293
|
+
* asking each integration separately.
|
|
294
|
+
*/
|
|
295
|
+
export function integrationsInstalledAt(scope: HookScope, cwd?: string): IntegrationType[] {
|
|
296
|
+
return INTEGRATION_TYPES.filter((id) => {
|
|
297
|
+
try {
|
|
298
|
+
const integration = getIntegration(id);
|
|
299
|
+
if (!integration.scopes.includes(scope)) return false;
|
|
300
|
+
if (!integration.hooksInstalledInSettings(scope, cwd)) return false;
|
|
301
|
+
// A narrower scope that lands on the same file as a wider one is that
|
|
302
|
+
// wider one, seen twice. Attribute it to the widest scope that resolves
|
|
303
|
+
// there so exactly one of them counts.
|
|
304
|
+
const here = integration.getSettingsPath(scope, cwd);
|
|
305
|
+
return !HOOK_SCOPES.some(
|
|
306
|
+
(other) =>
|
|
307
|
+
other !== scope &&
|
|
308
|
+
HOOK_SCOPES.indexOf(other) < HOOK_SCOPES.indexOf(scope) &&
|
|
309
|
+
integration.scopes.includes(other) &&
|
|
310
|
+
safeSettingsPath(integration, other, cwd) === here,
|
|
311
|
+
);
|
|
312
|
+
} catch {
|
|
313
|
+
// An integration whose settings file is unreadable is not evidence that
|
|
314
|
+
// nothing is installed — keep asking the rest.
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function safeSettingsPath(
|
|
321
|
+
integration: Integration,
|
|
322
|
+
scope: HookScope,
|
|
323
|
+
cwd?: string,
|
|
324
|
+
): string | null {
|
|
325
|
+
try {
|
|
326
|
+
return integration.getSettingsPath(scope, cwd);
|
|
327
|
+
} catch {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
261
330
|
}
|
|
262
331
|
|
|
263
332
|
export interface InstallHooksOptions {
|