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.
Files changed (176) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
  24. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -3
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -3
  31. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
  33. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
  35. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  38. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  39. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/index.html +1 -1
  42. package/.next/standalone/.next/server/app/index.rsc +15 -15
  43. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  44. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  48. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  52. package/.next/standalone/.next/server/app/policies/page.js +6 -5
  53. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  65. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  66. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  69. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  70. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[externals]__0-9ti7z._.js +3 -0
  73. package/.next/standalone/.next/server/chunks/{[externals]__1qzqcio._.js → [externals]__0sg9-3p._.js} +1 -1
  74. package/.next/standalone/.next/server/chunks/[externals]__0u3wrvq._.js +3 -0
  75. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zlqo2t._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1anxb5k._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +5 -0
  82. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +27 -0
  83. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/{_19uwam-._.js → _1-akg6w._.js} +2 -2
  87. package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
  88. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -7
  89. 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
  90. 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
  91. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +10 -0
  94. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n_lxhg._.js +5 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  104. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1jjjg6g._.js → [root-of-the-server]__1_7n3xe._.js} +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  106. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/{_1uvml5y._.js → _0b6xubz._.js} +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07gm3zl._.js → _1es2j7i._.js} +7 -21
  109. package/.next/standalone/.next/server/chunks/ssr/{_0_nsohj._.js → _1pj-r7q._.js} +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +17 -0
  113. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  116. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
  118. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +3 -0
  120. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  121. package/.next/standalone/.next/server/middleware.js +1 -1
  122. package/.next/standalone/.next/server/pages/404.html +1 -1
  123. package/.next/standalone/.next/server/pages/500.html +1 -1
  124. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  125. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  126. package/.next/standalone/.next/static/chunks/{3swqstog1cd9u.js → 06zoac_7ffkus.js} +1 -1
  127. package/.next/standalone/.next/static/chunks/{0icodhlwr9x_e.js → 0bshgqm5waivd.js} +2 -2
  128. package/.next/standalone/.next/static/chunks/{2-9lvhuto1iwo.js → 13grq7pifzvu5.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/{3i8rg2fv9nd6e.js → 16pmxmg9x48ob.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/{44oamw33aso19.js → 1da7-a7ek66uy.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/{1d6c85605lsgp.js → 1fg7drro5kle-.js} +1 -1
  132. package/.next/standalone/.next/static/chunks/{0tvmd0p_1i_g6.js → 1opyu56l4cdfk.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{1d7o3wnt3h6se.js → 1p-o1i0lyt9un.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/{3vqddiy9tvlwf.js → 2mfojtsy214sf.js} +1 -1
  135. package/.next/standalone/.next/static/chunks/{3jt6n16p2qpex.js → 32jj4qy40hln_.js} +1 -1
  136. package/.next/standalone/.next/static/chunks/{1vzyn9h2kvj-i.js → 3ww5ik9eo_1b_.js} +1 -1
  137. package/.next/standalone/.next/static/chunks/{1mfzsja6ftim5.css → 40nr6ehj4p3cw.css} +1 -1
  138. package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-1gehh2qpjyj5_.js} +1 -1
  139. package/.next/standalone/app/actions/get-hooks-config.ts +8 -1
  140. package/.next/standalone/app/actions/pack-actions.ts +12 -1
  141. package/.next/standalone/app/policies/hooks-client.tsx +41 -4
  142. package/.next/standalone/fp-cloud-cli/tests/test_pagination.py +204 -0
  143. package/.next/standalone/package.json +9 -9
  144. package/.next/standalone/server.js +1 -1
  145. package/bin/failproofai.mjs +57 -13
  146. package/dist/cli.mjs +23076 -20000
  147. package/dist/worker.mjs +1252 -542
  148. package/package.json +9 -9
  149. package/scripts/build-policy-pack.mjs +28 -4
  150. package/src/audit/index.ts +24 -4
  151. package/src/hooks/builtin-policies.ts +933 -8
  152. package/src/hooks/custom-hooks-loader.ts +98 -8
  153. package/src/hooks/handler.ts +26 -14
  154. package/src/hooks/manager.ts +36 -2
  155. package/src/hooks/pack-cli.ts +1224 -121
  156. package/src/hooks/pack-failclosed.ts +100 -4
  157. package/src/hooks/pack-manifest.ts +63 -1
  158. package/src/hooks/pack-param-key.ts +36 -0
  159. package/src/hooks/pack-store.ts +309 -25
  160. package/src/hooks/policy-catalog.ts +9 -1
  161. package/src/hooks/policy-evaluator.ts +77 -1
  162. package/src/hooks/tui.ts +21 -1
  163. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +0 -3
  164. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
  165. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
  166. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +0 -3
  167. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
  168. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
  169. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tg4pu6._.js +0 -3
  170. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
  171. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
  172. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mgrjnx._.js +0 -10
  173. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1opxw-7._.js +0 -5
  174. /package/.next/standalone/.next/static/{PKwyBPAHjy3DB8qqapaH4 → BFSuuqOY0YU7NpThROqre}/_buildManifest.js +0 -0
  175. /package/.next/standalone/.next/static/{PKwyBPAHjy3DB8qqapaH4 → BFSuuqOY0YU7NpThROqre}/_clientMiddlewareManifest.js +0 -0
  176. /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
- if (!m.events || m.events.length === 0) anyEvent = true;
69
- else for (const e of m.events) events.add(e);
70
- if (!m.toolNames || m.toolNames.length === 0) anyTool = true;
71
- else for (const t of m.toolNames) toolNames.add(t);
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
- }): { id: string; version: string; effect: PolicyEffect } {
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
+ }