failproofai 1.0.2-beta.7 → 1.0.2-beta.9
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 +5 -5
- 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 +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -3
- 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 +4 -3
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +6 -5
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__0-9ti7z._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[externals]__1qzqcio._.js → [externals]__0sg9-3p._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__0u3wrvq._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zlqo2t._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1anxb5k._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +5 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +27 -0
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
- package/.next/standalone/.next/server/chunks/{_19uwam-._.js → _1-akg6w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -7
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.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_0iu54mz._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +10 -0
- 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]__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]__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]__0n_lxhg._.js +5 -0
- 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]__1jjjg6g._.js → [root-of-the-server]__1_7n3xe._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1uvml5y._.js → _0b6xubz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07gm3zl._.js → _1es2j7i._.js} +7 -21
- package/.next/standalone/.next/server/chunks/ssr/{_0_nsohj._.js → _1pj-r7q._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +17 -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_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/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- 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/{3swqstog1cd9u.js → 06zoac_7ffkus.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0icodhlwr9x_e.js → 0bshgqm5waivd.js} +2 -2
- package/.next/standalone/.next/static/chunks/{2-9lvhuto1iwo.js → 13grq7pifzvu5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3i8rg2fv9nd6e.js → 16pmxmg9x48ob.js} +1 -1
- package/.next/standalone/.next/static/chunks/{44oamw33aso19.js → 1da7-a7ek66uy.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1d6c85605lsgp.js → 1fg7drro5kle-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0tvmd0p_1i_g6.js → 1opyu56l4cdfk.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1d7o3wnt3h6se.js → 1p-o1i0lyt9un.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vqddiy9tvlwf.js → 2mfojtsy214sf.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3jt6n16p2qpex.js → 32jj4qy40hln_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1vzyn9h2kvj-i.js → 3ww5ik9eo_1b_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1mfzsja6ftim5.css → 40nr6ehj4p3cw.css} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-1gehh2qpjyj5_.js} +1 -1
- package/.next/standalone/app/actions/get-hooks-config.ts +8 -1
- package/.next/standalone/app/actions/pack-actions.ts +12 -1
- package/.next/standalone/app/policies/hooks-client.tsx +41 -4
- package/.next/standalone/fp-cloud-cli/tests/test_pagination.py +204 -0
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +57 -13
- package/dist/cli.mjs +23076 -20000
- package/dist/worker.mjs +1252 -542
- package/package.json +9 -9
- package/scripts/build-policy-pack.mjs +28 -4
- package/src/audit/index.ts +24 -4
- package/src/hooks/builtin-policies.ts +933 -8
- package/src/hooks/custom-hooks-loader.ts +98 -8
- package/src/hooks/handler.ts +26 -14
- package/src/hooks/manager.ts +36 -2
- package/src/hooks/pack-cli.ts +1224 -121
- package/src/hooks/pack-failclosed.ts +100 -4
- package/src/hooks/pack-manifest.ts +63 -1
- package/src/hooks/pack-param-key.ts +36 -0
- package/src/hooks/pack-store.ts +309 -25
- package/src/hooks/policy-catalog.ts +9 -1
- package/src/hooks/policy-evaluator.ts +77 -1
- package/src/hooks/tui.ts +21 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tg4pu6._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mgrjnx._.js +0 -10
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1opxw-7._.js +0 -5
- /package/.next/standalone/.next/static/{PKwyBPAHjy3DB8qqapaH4 → BFSuuqOY0YU7NpThROqre}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{PKwyBPAHjy3DB8qqapaH4 → BFSuuqOY0YU7NpThROqre}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{PKwyBPAHjy3DB8qqapaH4 → BFSuuqOY0YU7NpThROqre}/_ssgManifest.js +0 -0
|
@@ -59,16 +59,40 @@ export const PERMANENT_LOAD_FAILURES: ReadonlySet<string> = new Set([
|
|
|
59
59
|
"path_missing",
|
|
60
60
|
]);
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A declared list, or null when it cannot be read as one.
|
|
64
|
+
*
|
|
65
|
+
* Null means UNKNOWN, and unknown has to widen the deny rather than narrow it —
|
|
66
|
+
* this whole function exists to say where a missing guard applied, and a
|
|
67
|
+
* narrowing derived from metadata nobody can parse says nothing true.
|
|
68
|
+
*
|
|
69
|
+
* A malformed list is not hypothetical: `match` was validated only as
|
|
70
|
+
* `typeof === "object"`, so a manifest declaring `events: "PreToolUse"` reached
|
|
71
|
+
* here as a STRING, was iterated character by character, and narrowed the
|
|
72
|
+
* fail-closed deny to `["P","r","e","T","o","l","U","s"]` — a set matching no
|
|
73
|
+
* event that exists. The pack failed to load, the guard fired on nothing, and
|
|
74
|
+
* the machine went on running unguarded with a deny it never used. `events: 5`
|
|
75
|
+
* was worse still: `for (const e of 5)` throws, on the hook path.
|
|
76
|
+
*/
|
|
77
|
+
function declaredList(value: unknown): string[] | null {
|
|
78
|
+
if (value === undefined || value === null) return null;
|
|
79
|
+
if (!Array.isArray(value)) return null;
|
|
80
|
+
if (value.some((entry) => typeof entry !== "string" || entry.length === 0)) return null;
|
|
81
|
+
return value as string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
function unionMatch(matchers: PolicyMatcher[]): PolicyMatcher {
|
|
63
85
|
const events = new Set<HookEventType>();
|
|
64
86
|
const toolNames = new Set<string>();
|
|
65
87
|
let anyEvent = false;
|
|
66
88
|
let anyTool = false;
|
|
67
89
|
for (const m of matchers) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!
|
|
71
|
-
else for (const
|
|
90
|
+
const declaredEvents = declaredList(m?.events);
|
|
91
|
+
const declaredTools = declaredList(m?.toolNames);
|
|
92
|
+
if (!declaredEvents || declaredEvents.length === 0) anyEvent = true;
|
|
93
|
+
else for (const e of declaredEvents) events.add(e as HookEventType);
|
|
94
|
+
if (!declaredTools || declaredTools.length === 0) anyTool = true;
|
|
95
|
+
else for (const t of declaredTools) toolNames.add(t);
|
|
72
96
|
}
|
|
73
97
|
const scoped: PolicyMatcher = {};
|
|
74
98
|
// Every declared event is kept, INCLUDING UserPromptSubmit. This says where
|
|
@@ -88,6 +112,26 @@ function unionMatch(matchers: PolicyMatcher[]): PolicyMatcher {
|
|
|
88
112
|
* user took 3 of 40 from is not a failure because the other 37 did not load.
|
|
89
113
|
* `disabled` is the `disabledCustomPolicies` set, keyed `pack:<id>@<ver>:<name>`.
|
|
90
114
|
*/
|
|
115
|
+
/**
|
|
116
|
+
* True when a pack scoped to specific agents does not cover the one running.
|
|
117
|
+
*
|
|
118
|
+
* The registration path already skips a pack whose `clis` excludes this agent;
|
|
119
|
+
* the fail-closed path did not, so a pack scoped to `codex` that failed to load
|
|
120
|
+
* denied on `claude` too — locking an agent out over enforcement it was never
|
|
121
|
+
* configured to have, until a human repaired a pack it does not use. `null` or
|
|
122
|
+
* absent means every agent, as it does everywhere else.
|
|
123
|
+
*/
|
|
124
|
+
function outOfScope(clis: string[] | null | undefined, cli: string | undefined): boolean {
|
|
125
|
+
if (!cli) return false;
|
|
126
|
+
// `Array.isArray`, not truthiness. A `clis` of `"codex"` is a truthy value
|
|
127
|
+
// with a `length` and an `includes`, so a string scope silently narrowed this
|
|
128
|
+
// to nothing and skipped the guard for every agent — a check meant to stop
|
|
129
|
+
// over-denying turning into the thing that under-denies. Unreadable scope
|
|
130
|
+
// means every agent, exactly as an unreadable `match` means every event.
|
|
131
|
+
if (!Array.isArray(clis) || clis.length === 0) return false;
|
|
132
|
+
return !clis.every((c) => typeof c === "string") ? false : !clis.includes(cli);
|
|
133
|
+
}
|
|
134
|
+
|
|
91
135
|
export function missingGuards(input: {
|
|
92
136
|
errors: PackError[];
|
|
93
137
|
packs: ResolvedPack[];
|
|
@@ -96,6 +140,8 @@ export function missingGuards(input: {
|
|
|
96
140
|
/** Packs handed to the loader whose entry artifact failed to import. */
|
|
97
141
|
failed: Map<string, PolicyLoadFailure>;
|
|
98
142
|
disabled: ReadonlySet<string>;
|
|
143
|
+
/** The agent this evaluation is for, so a pack scoped away from it stays quiet. */
|
|
144
|
+
cli?: string;
|
|
99
145
|
}): MissingGuard[] {
|
|
100
146
|
const out: MissingGuard[] = [];
|
|
101
147
|
|
|
@@ -104,6 +150,7 @@ export function missingGuards(input: {
|
|
|
104
150
|
// An observe pack denying on failure would deny for something that, had it
|
|
105
151
|
// loaded, would have allowed.
|
|
106
152
|
if (err.effect === "observe") continue;
|
|
153
|
+
if (outOfScope(err.clis, input.cli)) continue;
|
|
107
154
|
const declared = err.declared ?? [];
|
|
108
155
|
out.push({
|
|
109
156
|
packId: err.id ?? "(unnamed pack)",
|
|
@@ -119,6 +166,7 @@ export function missingGuards(input: {
|
|
|
119
166
|
// (c) Resolved, but registered less than it declared.
|
|
120
167
|
for (const pack of input.packs) {
|
|
121
168
|
if (pack.effect === "observe") continue;
|
|
169
|
+
if (outOfScope(pack.clis, input.cli)) continue;
|
|
122
170
|
const loadFailure = input.failed.get(pack.id);
|
|
123
171
|
if (loadFailure && PERMANENT_LOAD_FAILURES.has(loadFailure.type)) {
|
|
124
172
|
const taken = pack.enabled ?? pack.policies.map((p) => p.name);
|
|
@@ -172,6 +220,54 @@ export function missingGuards(input: {
|
|
|
172
220
|
* because the agent cannot run it: `block-failproofai-commands` denies every
|
|
173
221
|
* `failproofai` invocation from a tool call, deliberately and unconditionally.
|
|
174
222
|
*/
|
|
223
|
+
/**
|
|
224
|
+
* One matcher covering every missing guard, for the single policy that stands
|
|
225
|
+
* in for all of them.
|
|
226
|
+
*
|
|
227
|
+
* Narrow only where EVERY guard is narrow: a guard that declared no events
|
|
228
|
+
* applied everywhere, so a union that dropped it would deny less than the packs
|
|
229
|
+
* did. Both axes by the same rule — `toolNames` used to be left out of the
|
|
230
|
+
* combined object entirely, which reads as "every tool", so two failed packs
|
|
231
|
+
* each scoped to Bash denied Write and Read as well. One failed pack narrowed
|
|
232
|
+
* correctly and two widened to everything, which is the wrong way round:
|
|
233
|
+
* combining two limited scopes cannot produce a larger one.
|
|
234
|
+
*/
|
|
235
|
+
/**
|
|
236
|
+
* True when at least one guard actually applied to this event and tool.
|
|
237
|
+
*
|
|
238
|
+
* `combinedGuardMatch` unions the two axes independently, and the registry ANDs
|
|
239
|
+
* them — so guards scoped to (PreToolUse, Bash) and (PostToolUse, Write) produce
|
|
240
|
+
* a matcher that also catches (PreToolUse, Write) and (PostToolUse, Bash), pairs
|
|
241
|
+
* neither pack ever asked to guard. The union cannot be tightened without
|
|
242
|
+
* losing dispatch: the matcher has to be a SUPERSET or the policy is never
|
|
243
|
+
* reached. So the matcher stays wide enough to be called, and the pairing is
|
|
244
|
+
* checked here, where the real event and tool are known.
|
|
245
|
+
*/
|
|
246
|
+
export function guardsCover(
|
|
247
|
+
guards: MissingGuard[],
|
|
248
|
+
eventType: string,
|
|
249
|
+
toolName: string | undefined,
|
|
250
|
+
): boolean {
|
|
251
|
+
return guards.some((g) => {
|
|
252
|
+
const events = g.match.events;
|
|
253
|
+
if (events && events.length > 0 && !events.includes(eventType as (typeof events)[number])) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const tools = g.match.toolNames;
|
|
257
|
+
if (tools && tools.length > 0 && (!toolName || !tools.includes(toolName))) return false;
|
|
258
|
+
return true;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function combinedGuardMatch(guards: MissingGuard[]): PolicyMatcher {
|
|
263
|
+
if (guards.length === 1) return guards[0].match;
|
|
264
|
+
const axis = <K extends "events" | "toolNames">(key: K): PolicyMatcher[K] | undefined =>
|
|
265
|
+
guards.every((g) => g.match[key]?.length)
|
|
266
|
+
? ([...new Set(guards.flatMap((g) => g.match[key] ?? []))] as PolicyMatcher[K])
|
|
267
|
+
: undefined;
|
|
268
|
+
return { events: axis("events"), toolNames: axis("toolNames") };
|
|
269
|
+
}
|
|
270
|
+
|
|
175
271
|
export function packFailureReason(guards: MissingGuard[]): string {
|
|
176
272
|
const named = guards
|
|
177
273
|
.map((g) => {
|
|
@@ -54,6 +54,24 @@ const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
|
54
54
|
export const PACK_ID_RE = /^[A-Za-z0-9._-]{1,64}\/[A-Za-z0-9._-]{1,64}$/;
|
|
55
55
|
/** A version string, kept loose enough for semver and a tag. */
|
|
56
56
|
export const PACK_VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
|
|
57
|
+
/**
|
|
58
|
+
* The git commit a pack was built from — PROVENANCE, and deliberately not the
|
|
59
|
+
* same thing as `sha256`.
|
|
60
|
+
*
|
|
61
|
+
* `sha256` is the digest of the entry artifact: the security pin, re-verified
|
|
62
|
+
* before every import, and the reason a pack cannot change under a machine
|
|
63
|
+
* after it was installed. This answers a different question — WHICH SOURCE
|
|
64
|
+
* produced that artifact — which no digest can, because the digest is of the
|
|
65
|
+
* bundled output and the output names no input.
|
|
66
|
+
*
|
|
67
|
+
* Always optional. A pack published from a directory that is not a git checkout
|
|
68
|
+
* has no commit, and that is a supported way to publish, so every surface that
|
|
69
|
+
* reads this must render its absence rather than treat it as a defect.
|
|
70
|
+
*
|
|
71
|
+
* Abbreviated forms are accepted from 7 characters, which is what `git log`
|
|
72
|
+
* prints and therefore what somebody copying one will have.
|
|
73
|
+
*/
|
|
74
|
+
export const PACK_COMMIT_RE = /^[0-9a-f]{7,40}$/;
|
|
57
75
|
/**
|
|
58
76
|
* A pack policy's own name. `/` is REFUSED, and that is the important character:
|
|
59
77
|
* `normalizePolicyName` passes any name containing one straight through, and
|
|
@@ -72,6 +90,8 @@ export interface InstalledPackRecord {
|
|
|
72
90
|
/** Path to the single entry artifact, relative to `packsDir()`. */
|
|
73
91
|
entry: string;
|
|
74
92
|
sha256: string;
|
|
93
|
+
/** The git commit this pack was built from, when its publisher had one. */
|
|
94
|
+
commit?: string;
|
|
75
95
|
effect?: string;
|
|
76
96
|
policies?: unknown;
|
|
77
97
|
/**
|
|
@@ -106,6 +126,8 @@ export interface ResolvedPack {
|
|
|
106
126
|
/** Absolute, real, verified-to-be-inside-the-root path to the entry module. */
|
|
107
127
|
path: string;
|
|
108
128
|
sha256: string;
|
|
129
|
+
/** The git commit this pack was built from, when its publisher had one. */
|
|
130
|
+
commit?: string;
|
|
109
131
|
effect: PolicyEffect;
|
|
110
132
|
/** The pack's own catalog, in declared order. */
|
|
111
133
|
policies: PolicyCatalogEntry[];
|
|
@@ -135,6 +157,14 @@ export interface PackError {
|
|
|
135
157
|
* Lets the deny name what is missing instead of being blanket.
|
|
136
158
|
*/
|
|
137
159
|
declared?: PolicyCatalogEntry[];
|
|
160
|
+
/**
|
|
161
|
+
* The agents this pack was scoped to, when the manifest got far enough to say.
|
|
162
|
+
*
|
|
163
|
+
* Carried for the same reason `effect` is. A pack scoped to one CLI that fails
|
|
164
|
+
* to load must not deny on the others: they were never guarded by it, so a
|
|
165
|
+
* deny on their behalf locks an agent out over enforcement it never had.
|
|
166
|
+
*/
|
|
167
|
+
clis?: string[] | null;
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
export interface PackReadResult {
|
|
@@ -152,7 +182,8 @@ export function parsePackIdentity(value: {
|
|
|
152
182
|
id?: unknown;
|
|
153
183
|
version?: unknown;
|
|
154
184
|
effect?: unknown;
|
|
155
|
-
|
|
185
|
+
commit?: unknown;
|
|
186
|
+
}): { id: string; version: string; effect: PolicyEffect; commit?: string } {
|
|
156
187
|
if (typeof value.id !== "string" || !PACK_ID_RE.test(value.id)) {
|
|
157
188
|
throw new Error(`unsafe pack id ${JSON.stringify(value.id)}`);
|
|
158
189
|
}
|
|
@@ -162,10 +193,20 @@ export function parsePackIdentity(value: {
|
|
|
162
193
|
if (value.effect !== undefined && value.effect !== "enforce" && value.effect !== "observe") {
|
|
163
194
|
throw new Error(`unknown effect ${JSON.stringify(value.effect)} for pack ${value.id}`);
|
|
164
195
|
}
|
|
196
|
+
// Malformed provenance is DROPPED, never fatal. It is a label on the artifact,
|
|
197
|
+
// not part of what makes it safe to run — so a publisher who wrote something
|
|
198
|
+
// odd here gets a pack that installs and simply says nothing about its
|
|
199
|
+
// origin, rather than a pack nobody can install. The digest still decides
|
|
200
|
+
// whether the bytes are the ones that were published.
|
|
201
|
+
const commit =
|
|
202
|
+
typeof value.commit === "string" && PACK_COMMIT_RE.test(value.commit.trim().toLowerCase())
|
|
203
|
+
? value.commit.trim().toLowerCase()
|
|
204
|
+
: undefined;
|
|
165
205
|
return {
|
|
166
206
|
id: value.id,
|
|
167
207
|
version: value.version,
|
|
168
208
|
effect: (value.effect as PolicyEffect | undefined) ?? "enforce",
|
|
209
|
+
...(commit ? { commit } : {}),
|
|
169
210
|
};
|
|
170
211
|
}
|
|
171
212
|
|
|
@@ -220,6 +261,19 @@ export function parsePackPolicy(packId: string, value: unknown, index: number):
|
|
|
220
261
|
if (!raw.match || typeof raw.match !== "object") {
|
|
221
262
|
throw new Error(`${where} is missing a match`);
|
|
222
263
|
}
|
|
264
|
+
// The SHAPE, not just the presence. `match` was accepted as any object, so a
|
|
265
|
+
// manifest declaring `events: "PreToolUse"` installed cleanly and then
|
|
266
|
+
// narrowed the fail-closed deny to the letters of that string — a guard
|
|
267
|
+
// matching no event that exists. Refused at install now, where the publisher
|
|
268
|
+
// can still fix it, rather than surviving as metadata nothing can read.
|
|
269
|
+
const matchShape = raw.match as { events?: unknown; toolNames?: unknown };
|
|
270
|
+
for (const key of ["events", "toolNames"] as const) {
|
|
271
|
+
const value = matchShape[key];
|
|
272
|
+
if (value === undefined) continue;
|
|
273
|
+
if (!Array.isArray(value) || value.some((e) => typeof e !== "string" || e.length === 0)) {
|
|
274
|
+
throw new Error(`${where} has a match.${key} that is not a list of names`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
223
277
|
return raw as unknown as PolicyCatalogEntry;
|
|
224
278
|
}
|
|
225
279
|
|
|
@@ -357,6 +411,14 @@ export function readInstalledPacks(): PackReadResult {
|
|
|
357
411
|
// Best effort: an entry too malformed to list policies yields nothing
|
|
358
412
|
// here, and a deny built from it is unavoidably blanket.
|
|
359
413
|
...(Array.isArray(rec?.policies) ? { declared: safeDeclared(rec.policies) } : {}),
|
|
414
|
+
// A list of names, or nothing. Anything else is unreadable scope, and
|
|
415
|
+
// unreadable scope has to mean "every agent" — the same reasoning that
|
|
416
|
+
// widens an unreadable `match`: a narrowing nobody can parse says
|
|
417
|
+
// nothing true, and here the narrowing would be the thing that lets an
|
|
418
|
+
// unguarded agent through.
|
|
419
|
+
...(Array.isArray(rec?.clis) && rec.clis.every((c) => typeof c === "string")
|
|
420
|
+
? { clis: rec.clis }
|
|
421
|
+
: {}),
|
|
360
422
|
});
|
|
361
423
|
}
|
|
362
424
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The config key a pack policy's parameters are stored under.
|
|
3
|
+
*
|
|
4
|
+
* A LEAF module on purpose: no imports at all, so it can be reached from the
|
|
5
|
+
* browser. `packPolicyParamKey` used to live in `policy-evaluator.ts`, which
|
|
6
|
+
* the dashboard's client component then imported — dragging `pack-store` and
|
|
7
|
+
* its `node:fs` into the browser bundle and failing the Next build with
|
|
8
|
+
* `Module not found: Can't resolve 'fs/promises'`. The key is pure string
|
|
9
|
+
* formatting and belongs where both sides can have it.
|
|
10
|
+
*
|
|
11
|
+
* VERSION-LESS, and that is the whole design. A pack policy registers as
|
|
12
|
+
* `pack/<id>@<version>/<name>`, so a key carrying the version would be orphaned
|
|
13
|
+
* by the publisher's next release — a parameter somebody set would silently
|
|
14
|
+
* revert to its default on upgrade, which is the same failure `enabled` avoids
|
|
15
|
+
* by outliving the version it was chosen against.
|
|
16
|
+
*/
|
|
17
|
+
export function packPolicyParamKey(packId: string, policyName: string): string {
|
|
18
|
+
return `pack/${packId}/${policyName}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `pack/<id>@<version>/<name>` split back into the parts a key is built from,
|
|
22
|
+
* or null when the name is not a pack policy's. */
|
|
23
|
+
export function parsePackPolicyName(
|
|
24
|
+
canonicalName: string,
|
|
25
|
+
): { packId: string; name: string } | null {
|
|
26
|
+
if (!canonicalName.startsWith("pack/")) return null;
|
|
27
|
+
const rest = canonicalName.slice("pack/".length);
|
|
28
|
+
const slash = rest.lastIndexOf("/");
|
|
29
|
+
if (slash <= 0) return null;
|
|
30
|
+
const idWithVersion = rest.slice(0, slash);
|
|
31
|
+
const at = idWithVersion.lastIndexOf("@");
|
|
32
|
+
return {
|
|
33
|
+
packId: at > 0 ? idWithVersion.slice(0, at) : idWithVersion,
|
|
34
|
+
name: rest.slice(slash + 1),
|
|
35
|
+
};
|
|
36
|
+
}
|