failproofai 1.0.2-beta.8 → 1.0.2
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 +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/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/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 +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/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/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/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +4 -4
- 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/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/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/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/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]__1lt47k4._.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]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1yh7hlv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0ehudfr._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1n2ywfz._.js → _1es2j7i._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1h33neq._.js → _1un1pug._.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 +9 -9
- 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/middleware-build-manifest.js +3 -3
- 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/0nkpwjk01ei3t.js +1 -0
- package/.next/standalone/.next/static/chunks/{2iryw37qfqy5y.js → 0ofdelz65_517.js} +1 -1
- package/.next/standalone/.next/static/chunks/{34j7nmidlcuni.js → 12uhnpwgsmcev.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0i3a6wf-f_yky.js → 1_1eb_149xyt9.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3927pu4ss_l4k.js → 1tx9pnxyjrkoj.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1n04wzsaes73g.js → 1x0jrl3a4_i2m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3r0udrtpn78-e.js → 1zpv6omeln7al.js} +1 -1
- package/.next/standalone/.next/static/chunks/{416zumre3fvfw.js → 20hbg-r3io_ip.js} +1 -1
- package/.next/standalone/.next/static/chunks/{11a_m8yksthtf.js → 32bhi32ym8yrz.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0u2ojcw25c_np.js → 3fr78ugeb4tjc.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/README.md +58 -13
- package/bin/failproofai.mjs +10 -1
- package/dist/cli.mjs +22429 -21292
- package/dist/worker.mjs +1247 -540
- package/package.json +9 -9
- 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 +415 -42
- package/src/hooks/pack-failclosed.ts +100 -4
- package/src/hooks/pack-manifest.ts +29 -0
- package/src/hooks/pack-param-key.ts +36 -0
- package/src/hooks/pack-store.ts +101 -9
- package/src/hooks/policy-catalog.ts +9 -1
- package/src/hooks/policy-evaluator.ts +77 -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]__1-wzs7f._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wlvpe5._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_0l7zv8b._.js +0 -23
- package/.next/standalone/.next/static/chunks/1ch5l9t2p16y4.js +0 -1
- /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_ssgManifest.js +0 -0
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
import { findProjectConfigDir } from "./hooks-config";
|
|
30
30
|
import { trackHookEvent } from "./hook-telemetry";
|
|
31
31
|
import { getInstanceId } from "../../lib/telemetry-id";
|
|
32
|
-
import type { CustomHook } from "./policy-types";
|
|
32
|
+
import type { CustomHook, PolicyCatalogEntry } from "./policy-types";
|
|
33
33
|
import type { CloudManagedPolicyArtifact } from "./cloud-managed-policies";
|
|
34
34
|
import type { ResolvedPack } from "./pack-manifest";
|
|
35
35
|
import { customPoliciesDir, shimsDir } from "./fp-home";
|
|
@@ -303,6 +303,18 @@ export interface LoadAllResult {
|
|
|
303
303
|
conventionSources: ConventionSource[];
|
|
304
304
|
/** Import failures keyed by pack id, including artifacts that registered no hooks. */
|
|
305
305
|
packFailures: Map<string, PolicyLoadFailure>;
|
|
306
|
+
/**
|
|
307
|
+
* Every pack id that shares one artifact, keyed by the id the collapse kept.
|
|
308
|
+
*
|
|
309
|
+
* Packs with identical entry bytes load once, and every hook that comes back
|
|
310
|
+
* carries only the surviving record's id — so a registration recorded under
|
|
311
|
+
* it says nothing about the other pack. `missingGuards` skips a pack that
|
|
312
|
+
* appears in neither the failure map nor the registered map, which means an
|
|
313
|
+
* artifact that imports FINE but omits a policy only the non-winning record
|
|
314
|
+
* selected produced no guard at all: not registered, and not denied either.
|
|
315
|
+
* Only present for a path more than one pack resolved to.
|
|
316
|
+
*/
|
|
317
|
+
packAliases: Map<string, string[]>;
|
|
306
318
|
}
|
|
307
319
|
|
|
308
320
|
export function customPolicyId(file: string, name: string): string {
|
|
@@ -448,9 +460,54 @@ export async function loadAllCustomHooks(
|
|
|
448
460
|
// Resolved the same way and for the same reason — toward ENFORCEMENT, because
|
|
449
461
|
// over-enforcing is visible to whoever hits it and under-enforcing is the
|
|
450
462
|
// silent failure — and announced, so an operator can act on it.
|
|
463
|
+
/** `null` means "all of it" on both `enabled` and `clis`, so it absorbs any
|
|
464
|
+
* list. Two lists become their union — never their intersection, which is
|
|
465
|
+
* what dropping one of them amounted to. */
|
|
466
|
+
const unionSelection = (a: string[] | null, b: string[] | null): string[] | null =>
|
|
467
|
+
a === null || b === null ? null : [...new Set([...a, ...b])];
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* The CATALOGS have to merge for the same reason the selections do, and the
|
|
471
|
+
* union of `enabled` is what made it load-bearing: it lets the loser's policy
|
|
472
|
+
* run, and the catalog is where a pack declares that policy's PARAMS SCHEMA.
|
|
473
|
+
* `registerPolicy` reads the schema off `pack.policies` by name, so a policy
|
|
474
|
+
* present only in the loser's catalog registered with none — and a policy with
|
|
475
|
+
* no schema gets no defaults, so every value its publisher declared silently
|
|
476
|
+
* arrived as `undefined` in `ctx.params`. Enforcement running on the wrong
|
|
477
|
+
* numbers is the same silent-wrong as enforcement not running.
|
|
478
|
+
*
|
|
479
|
+
* Two packs sharing bytes is most likely a fork or a re-publish, and a fork
|
|
480
|
+
* that only re-declares a default changes no source at all — so differing
|
|
481
|
+
* catalogs behind one artifact is the EXPECTED shape here, not a corner.
|
|
482
|
+
*
|
|
483
|
+
* Winner precedence on a name both declare, because the merged record carries
|
|
484
|
+
* the winner's id and version and there is no merging two different defaults.
|
|
485
|
+
*/
|
|
486
|
+
const unionCatalog = (
|
|
487
|
+
winner: PolicyCatalogEntry[],
|
|
488
|
+
other: PolicyCatalogEntry[],
|
|
489
|
+
): PolicyCatalogEntry[] => [
|
|
490
|
+
...winner,
|
|
491
|
+
...other.filter((p) => !winner.some((w) => w.name === p.name)),
|
|
492
|
+
];
|
|
493
|
+
|
|
451
494
|
const packByPath = new Map<string, ResolvedPack>();
|
|
495
|
+
/**
|
|
496
|
+
* Every pack id behind each artifact path, not just the one that survived
|
|
497
|
+
* the collapse.
|
|
498
|
+
*
|
|
499
|
+
* A failure is recorded per PACK ID, and the collapse leaves one id holding
|
|
500
|
+
* the merged record — so an artifact with a syntax error marked the winner
|
|
501
|
+
* failed and said nothing about the other. `missingGuards` skips a pack that
|
|
502
|
+
* appears in neither the failure map nor the registered map, so the second
|
|
503
|
+
* pack's selected policies were absent, unguarded, and unreported: the exact
|
|
504
|
+
* silent under-enforcement the selection union was written to stop, arriving
|
|
505
|
+
* through the failure path instead.
|
|
506
|
+
*/
|
|
507
|
+
const packIdsByPath = new Map<string, string[]>();
|
|
452
508
|
for (const pack of opts?.packs ?? []) {
|
|
453
509
|
const key = resolve(pack.path);
|
|
510
|
+
packIdsByPath.set(key, [...(packIdsByPath.get(key) ?? []), pack.id]);
|
|
454
511
|
const existing = packByPath.get(key);
|
|
455
512
|
if (!existing) {
|
|
456
513
|
packByPath.set(key, pack);
|
|
@@ -458,11 +515,32 @@ export async function loadAllCustomHooks(
|
|
|
458
515
|
}
|
|
459
516
|
hookLogWarn(
|
|
460
517
|
`packs ${existing.id} and ${pack.id} have identical source, so they share one artifact ` +
|
|
461
|
-
`and load as one pack; enforcing it if either asks to enforce
|
|
518
|
+
`and load as one pack; enforcing it if either asks to enforce, and taking ` +
|
|
519
|
+
`the union of what each has enabled and of the agents each guards`,
|
|
462
520
|
);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
521
|
+
// The EFFECT was resolved toward enforcement here and the SELECTIONS were
|
|
522
|
+
// not, which made the collapse silently subtractive.
|
|
523
|
+
//
|
|
524
|
+
// Only the winner's `enabled` reached the tag, and `handler.ts` gates every
|
|
525
|
+
// hook on that one list — so two packs sharing bytes, one having taken
|
|
526
|
+
// `foo` and the other `bar`, registered whichever list won and dropped the
|
|
527
|
+
// other's policy entirely. `pack-failclosed.ts` ignores a pack absent from
|
|
528
|
+
// the registered map, so nothing reported it: a policy the user had
|
|
529
|
+
// installed and enabled simply never ran. `clis` had the same shape — a
|
|
530
|
+
// pack scoped to one agent decided the scope for both.
|
|
531
|
+
//
|
|
532
|
+
// Unioned, for the reason the effect is resolved toward enforcement: over-
|
|
533
|
+
// enforcing is visible to whoever hits it, under-enforcing is silent. `null`
|
|
534
|
+
// means "everything" on both fields, so it absorbs any list rather than
|
|
535
|
+
// being intersected away.
|
|
536
|
+
const winner = existing.effect !== "enforce" && pack.effect === "enforce" ? pack : existing;
|
|
537
|
+
const other = winner === existing ? pack : existing;
|
|
538
|
+
packByPath.set(key, {
|
|
539
|
+
...winner,
|
|
540
|
+
policies: unionCatalog(winner.policies, other.policies),
|
|
541
|
+
enabled: unionSelection(winner.enabled, other.enabled),
|
|
542
|
+
clis: unionSelection(winner.clis, other.clis),
|
|
543
|
+
});
|
|
466
544
|
}
|
|
467
545
|
|
|
468
546
|
// 1. Explicit custom policy paths. Accept a string for callers/configs using
|
|
@@ -489,7 +567,11 @@ export async function loadAllCustomHooks(
|
|
|
489
567
|
verifyEntrySha:
|
|
490
568
|
cloudManaged?.sha256 ?? pack?.sha256,
|
|
491
569
|
});
|
|
492
|
-
|
|
570
|
+
// Every id behind these bytes. One artifact, one import, one failure —
|
|
571
|
+
// but as many fail-closed guards as there are packs depending on it.
|
|
572
|
+
if (failure && pack) {
|
|
573
|
+
for (const id of packIdsByPath.get(absPath) ?? [pack.id]) packFailures.set(id, failure);
|
|
574
|
+
}
|
|
493
575
|
for (const hook of getCustomHooks().slice(hooksBefore)) {
|
|
494
576
|
const tagged = hook as CustomHook & {
|
|
495
577
|
__policyId?: string;
|
|
@@ -510,7 +592,10 @@ export async function loadAllCustomHooks(
|
|
|
510
592
|
}
|
|
511
593
|
} else {
|
|
512
594
|
hookLogWarn(`custom policy path not found: ${absPath}`);
|
|
513
|
-
if (pack)
|
|
595
|
+
if (pack) {
|
|
596
|
+
const missing = { type: "path_missing", reason: `path missing: ${absPath}` } as const;
|
|
597
|
+
for (const id of packIdsByPath.get(absPath) ?? [pack.id]) packFailures.set(id, missing);
|
|
598
|
+
}
|
|
514
599
|
}
|
|
515
600
|
}
|
|
516
601
|
|
|
@@ -652,5 +737,10 @@ export async function loadAllCustomHooks(
|
|
|
652
737
|
}
|
|
653
738
|
}
|
|
654
739
|
|
|
655
|
-
|
|
740
|
+
const packAliases = new Map<string, string[]>();
|
|
741
|
+
for (const [path, ids] of packIdsByPath) {
|
|
742
|
+
const winner = packByPath.get(path);
|
|
743
|
+
if (winner && ids.length > 1) packAliases.set(winner.id, ids);
|
|
744
|
+
}
|
|
745
|
+
return { hooks: allHooks, conventionSources, packFailures, packAliases };
|
|
656
746
|
}
|
package/src/hooks/handler.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { hookLogInfo, hookLogWarn } from "./hook-logger";
|
|
|
44
44
|
import { readStdinPayload } from "./read-stdin";
|
|
45
45
|
import { readActiveCloudManagedPolicies, type CloudManagedPolicyArtifact } from "./cloud-managed-policies";
|
|
46
46
|
import { hasInstalledPacks, readInstalledPacks, type PackError, type ResolvedPack } from "./pack-manifest";
|
|
47
|
-
import { missingGuards, packFailureReason } from "./pack-failclosed";
|
|
47
|
+
import { missingGuards, packFailureReason, combinedGuardMatch, guardsCover } from "./pack-failclosed";
|
|
48
48
|
import { readActivePause, type ActivePause } from "./session-pause";
|
|
49
49
|
import { layoutWarningForHook } from "./fp-reset";
|
|
50
50
|
|
|
@@ -455,9 +455,18 @@ export async function evaluateHookEvent(
|
|
|
455
455
|
// a pack too; once the shim stops, nothing else registers by name.
|
|
456
456
|
if (pack && enabledBuiltinNames.has(hook.name)) continue;
|
|
457
457
|
if (pack) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
458
|
+
// Recorded against EVERY pack behind these bytes, not just the id the
|
|
459
|
+
// collapse kept. A shared artifact loads once and every hook carries
|
|
460
|
+
// the surviving record's id, so registering under it alone left the
|
|
461
|
+
// other pack in neither map — and `missingGuards` skips a pack in
|
|
462
|
+
// neither map, so an artifact that imported fine while omitting a
|
|
463
|
+
// policy only that pack had selected produced no guard: not
|
|
464
|
+
// registered, and not denied either.
|
|
465
|
+
for (const id of loadResult.packAliases?.get(pack.id) ?? [pack.id]) {
|
|
466
|
+
const seen = registeredByPack.get(id) ?? new Set<string>();
|
|
467
|
+
seen.add(hook.name);
|
|
468
|
+
registeredByPack.set(id, seen);
|
|
469
|
+
}
|
|
461
470
|
}
|
|
462
471
|
const hookName = hook.name;
|
|
463
472
|
const conventionScope = (hook as CustomHook & { __conventionScope?: string }).__conventionScope;
|
|
@@ -570,16 +579,11 @@ export async function evaluateHookEvent(
|
|
|
570
579
|
registered: registeredByPack,
|
|
571
580
|
failed: loadResult.packFailures,
|
|
572
581
|
disabled: disabledCustomPolicies,
|
|
582
|
+
cli,
|
|
573
583
|
});
|
|
574
584
|
if (guards.length > 0) {
|
|
575
585
|
const reason = packFailureReason(guards);
|
|
576
|
-
const match = guards
|
|
577
|
-
? guards[0].match
|
|
578
|
-
: {
|
|
579
|
-
events: guards.every((g) => g.match.events)
|
|
580
|
-
? [...new Set(guards.flatMap((g) => g.match.events ?? []))]
|
|
581
|
-
: undefined,
|
|
582
|
-
};
|
|
586
|
+
const match = combinedGuardMatch(guards);
|
|
583
587
|
const name = "pack/failproofai-pack-unavailable";
|
|
584
588
|
policyAttribution.set(name, {
|
|
585
589
|
source: "pack",
|
|
@@ -589,13 +593,21 @@ export async function evaluateHookEvent(
|
|
|
589
593
|
registerPolicy(
|
|
590
594
|
name,
|
|
591
595
|
"A policy pack this machine enforces could not be loaded",
|
|
592
|
-
async (): Promise<PolicyResult> =>
|
|
596
|
+
async (ctx): Promise<PolicyResult> => {
|
|
597
|
+
// The matcher is a union of both axes and the registry ANDs them,
|
|
598
|
+
// so being CALLED is not proof any guard covered this pair. Two
|
|
599
|
+
// packs scoped to (PreToolUse, Bash) and (PostToolUse, Write)
|
|
600
|
+
// produce a matcher that also catches (PreToolUse, Write) —
|
|
601
|
+
// neither pack's business. The pairing is decided here, where the
|
|
602
|
+
// real event and tool are known.
|
|
603
|
+
if (!guardsCover(guards, canonicalEventType, ctx.toolName)) return { decision: "allow" };
|
|
593
604
|
// UserPromptSubmit instructs rather than denies, whatever the
|
|
594
605
|
// missing policies declared: a deny there locks the user out of
|
|
595
606
|
// their own agent, which is the one thing that stops them fixing it.
|
|
596
|
-
canonicalEventType === "UserPromptSubmit"
|
|
607
|
+
return canonicalEventType === "UserPromptSubmit"
|
|
597
608
|
? { decision: "instruct", reason }
|
|
598
|
-
: { decision: "deny", reason }
|
|
609
|
+
: { decision: "deny", reason };
|
|
610
|
+
},
|
|
599
611
|
match,
|
|
600
612
|
// Above builtins (0) and custom (-1), so the short-circuit attributes
|
|
601
613
|
// the deny to the missing pack rather than to whichever surviving
|
package/src/hooks/manager.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { readActiveCloudManagedPolicies } from "./cloud-managed-policies";
|
|
|
28
28
|
import { CORE_SOURCE, addPack, setPackPolicyEnabled } from "./pack-store";
|
|
29
29
|
import type { ResolvedPack } from "./pack-manifest";
|
|
30
30
|
import { hasInstalledPacks, readInstalledPacks } from "./pack-manifest";
|
|
31
|
+
import { packPolicyParamKey } from "./policy-evaluator";
|
|
31
32
|
import {
|
|
32
33
|
chip,
|
|
33
34
|
note,
|
|
@@ -951,10 +952,21 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
951
952
|
// Names a `policyParams` key may legitimately use: every policy an installed
|
|
952
953
|
// pack carries. Previously the compiled catalog, which no longer describes
|
|
953
954
|
// what runs.
|
|
955
|
+
//
|
|
956
|
+
// BOTH spellings, because both are read at runtime. The dashboard writes the
|
|
957
|
+
// pack-qualified `packPolicyParamKey` and the evaluator prefers it; the bare
|
|
958
|
+
// name is the legacy key still honoured for our own pack. Knowing only the
|
|
959
|
+
// bare one made this command call every parameter saved through the UI a
|
|
960
|
+
// "possible typo" — and fire a `policy_params_validation_warning` for it —
|
|
961
|
+
// the moment the dashboard started qualifying its keys. Built with the shared
|
|
962
|
+
// helper rather than a third copy of the `pack/<id>/<name>` format.
|
|
954
963
|
const knownPolicyNames = new Set<string>();
|
|
955
964
|
try {
|
|
956
965
|
for (const pack of readInstalledPacks().packs) {
|
|
957
|
-
for (const policy of pack.policies)
|
|
966
|
+
for (const policy of pack.policies) {
|
|
967
|
+
knownPolicyNames.add(policy.name);
|
|
968
|
+
knownPolicyNames.add(packPolicyParamKey(pack.id, policy.name));
|
|
969
|
+
}
|
|
958
970
|
}
|
|
959
971
|
} catch {
|
|
960
972
|
// Unreadable manifest: skip the typo warning rather than invent one.
|
|
@@ -1032,7 +1044,20 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
1032
1044
|
// finished `failproofai config` saw a header, a config path, and no next
|
|
1033
1045
|
// step. Ours is named in full, the same way anyone else's pack is typed,
|
|
1034
1046
|
// because it IS one.
|
|
1035
|
-
|
|
1047
|
+
// `packCount` counts ENABLED policies, so a pack installed with everything
|
|
1048
|
+
// switched off reaches zero the same way an empty machine does — and the
|
|
1049
|
+
// advice for the two could not be more different. Telling somebody who has
|
|
1050
|
+
// just unticked all 38 to `policies add FailproofAI/policies` sends them to
|
|
1051
|
+
// install what they already have, and doing it would change nothing: the
|
|
1052
|
+
// selection is what is empty, not the shelf.
|
|
1053
|
+
const packsInstalled = (() => {
|
|
1054
|
+
try {
|
|
1055
|
+
return readInstalledPacks().packs.length;
|
|
1056
|
+
} catch {
|
|
1057
|
+
return 0;
|
|
1058
|
+
}
|
|
1059
|
+
})();
|
|
1060
|
+
if (packCount === 0 && packsInstalled === 0) {
|
|
1036
1061
|
footer.unshift(
|
|
1037
1062
|
nextStep(
|
|
1038
1063
|
`failproofai policies add ${CORE_SOURCE}`,
|
|
@@ -1042,6 +1067,15 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
1042
1067
|
note("Someone else's: failproofai policies add <owner>/<repo>", opts),
|
|
1043
1068
|
note("Look first: failproofai policies show <owner>/<repo>", opts),
|
|
1044
1069
|
);
|
|
1070
|
+
} else if (packCount === 0) {
|
|
1071
|
+
footer.unshift(
|
|
1072
|
+
nextStep(
|
|
1073
|
+
"failproofai policies add",
|
|
1074
|
+
`Nothing is enforcing: ${packsInstalled === 1 ? "the pack above is" : "the packs above are"} installed with everything switched off. Pick what should be on:`,
|
|
1075
|
+
opts,
|
|
1076
|
+
),
|
|
1077
|
+
note("Or by name: failproofai policies add <policy>", opts),
|
|
1078
|
+
);
|
|
1045
1079
|
}
|
|
1046
1080
|
|
|
1047
1081
|
if (installedScopes.length > 1) {
|