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.
Files changed (165) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
  22. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -3
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -3
  29. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
  31. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
  33. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  36. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/index.html +1 -1
  39. package/.next/standalone/.next/server/app/index.rsc +14 -14
  40. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  41. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  42. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  43. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  47. package/.next/standalone/.next/server/app/policies/page.js +4 -4
  48. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  58. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  61. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/chunks/[externals]__0-9ti7z._.js +3 -0
  64. package/.next/standalone/.next/server/chunks/{[externals]__1lt47k4._.js → [externals]__0sg9-3p._.js} +1 -1
  65. package/.next/standalone/.next/server/chunks/[externals]__0u3wrvq._.js +3 -0
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zlqo2t._.js +3 -0
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1anxb5k._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +5 -0
  73. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +27 -0
  74. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
  75. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/{_19uwam-._.js → _1-akg6w._.js} +2 -2
  78. package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -7
  80. 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
  81. 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
  82. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +10 -0
  85. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n_lxhg._.js +5 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1yh7hlv._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/_0ehudfr._.js +23 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1n2ywfz._.js → _1es2j7i._.js} +4 -4
  100. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/{_1h33neq._.js → _1un1pug._.js} +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +9 -9
  104. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  107. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  110. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  111. package/.next/standalone/.next/server/middleware.js +1 -1
  112. package/.next/standalone/.next/server/pages/404.html +1 -1
  113. package/.next/standalone/.next/server/pages/500.html +1 -1
  114. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  116. package/.next/standalone/.next/static/chunks/0nkpwjk01ei3t.js +1 -0
  117. package/.next/standalone/.next/static/chunks/{2iryw37qfqy5y.js → 0ofdelz65_517.js} +1 -1
  118. package/.next/standalone/.next/static/chunks/{34j7nmidlcuni.js → 12uhnpwgsmcev.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/{0i3a6wf-f_yky.js → 1_1eb_149xyt9.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/{3927pu4ss_l4k.js → 1tx9pnxyjrkoj.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{1n04wzsaes73g.js → 1x0jrl3a4_i2m.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/{3r0udrtpn78-e.js → 1zpv6omeln7al.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/{416zumre3fvfw.js → 20hbg-r3io_ip.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/{11a_m8yksthtf.js → 32bhi32ym8yrz.js} +2 -2
  125. package/.next/standalone/.next/static/chunks/{0u2ojcw25c_np.js → 3fr78ugeb4tjc.js} +1 -1
  126. package/.next/standalone/app/actions/get-hooks-config.ts +8 -1
  127. package/.next/standalone/app/actions/pack-actions.ts +12 -1
  128. package/.next/standalone/app/policies/hooks-client.tsx +41 -4
  129. package/.next/standalone/fp-cloud-cli/tests/test_pagination.py +204 -0
  130. package/.next/standalone/package.json +9 -9
  131. package/.next/standalone/server.js +1 -1
  132. package/README.md +58 -13
  133. package/bin/failproofai.mjs +10 -1
  134. package/dist/cli.mjs +22429 -21292
  135. package/dist/worker.mjs +1247 -540
  136. package/package.json +9 -9
  137. package/src/audit/index.ts +24 -4
  138. package/src/hooks/builtin-policies.ts +933 -8
  139. package/src/hooks/custom-hooks-loader.ts +98 -8
  140. package/src/hooks/handler.ts +26 -14
  141. package/src/hooks/manager.ts +36 -2
  142. package/src/hooks/pack-cli.ts +415 -42
  143. package/src/hooks/pack-failclosed.ts +100 -4
  144. package/src/hooks/pack-manifest.ts +29 -0
  145. package/src/hooks/pack-param-key.ts +36 -0
  146. package/src/hooks/pack-store.ts +101 -9
  147. package/src/hooks/policy-catalog.ts +9 -1
  148. package/src/hooks/policy-evaluator.ts +77 -1
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +0 -3
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +0 -3
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tg4pu6._.js +0 -3
  156. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
  157. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
  158. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mgrjnx._.js +0 -10
  159. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-wzs7f._.js +0 -3
  160. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wlvpe5._.js +0 -5
  161. package/.next/standalone/.next/server/chunks/ssr/_0l7zv8b._.js +0 -23
  162. package/.next/standalone/.next/static/chunks/1ch5l9t2p16y4.js +0 -1
  163. /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_buildManifest.js +0 -0
  164. /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_clientMiddlewareManifest.js +0 -0
  165. /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
- if (existing.effect !== "enforce" && pack.effect === "enforce") {
464
- packByPath.set(key, pack);
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
- if (failure && pack) packFailures.set(pack.id, failure);
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) packFailures.set(pack.id, { type: "path_missing", reason: `path missing: ${absPath}` });
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
- return { hooks: allHooks, conventionSources, packFailures };
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
  }
@@ -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
- const seen = registeredByPack.get(pack.id) ?? new Set<string>();
459
- seen.add(hook.name);
460
- registeredByPack.set(pack.id, seen);
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.length === 1
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
@@ -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) knownPolicyNames.add(policy.name);
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
- if (packCount === 0) {
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) {