failproofai 1.0.7-beta.2 → 1.0.8-beta.0

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 (225) 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 +3 -3
  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 +15 -15
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -5
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  31. package/.next/standalone/.next/server/app/audit/page.js +4 -4
  32. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/index.html +1 -1
  35. package/.next/standalone/.next/server/app/index.rsc +15 -15
  36. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  43. package/.next/standalone/.next/server/app/policies/page.js +6 -7
  44. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +18 -7
  58. package/.next/standalone/.next/server/app/settings/page.js +9 -7
  59. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__20kzpkf._.js} +1 -1
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  65. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +20 -20
  66. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +9 -9
  69. package/.next/standalone/.next/server/chunks/_1q5i8mb._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  73. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0qxnccm._.js → [root-of-the-server]__06pflha._.js} +2 -4
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
  82. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0soxz2z._.js → [root-of-the-server]__12e7nhs._.js} +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l44ual._.js → [root-of-the-server]__14o3ek1._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/{_1v-jvrv._.js → _0o4xkpl._.js} +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/{_1mel6y1._.js → _1-7sqrb._.js} +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  97. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  105. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +12 -0
  107. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  113. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  114. package/.next/standalone/.next/server/pages/404.html +1 -1
  115. package/.next/standalone/.next/server/pages/500.html +1 -1
  116. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  117. package/.next/standalone/.next/server/server-reference-manifest.json +33 -22
  118. package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +1 -0
  119. package/.next/standalone/.next/static/chunks/{3brze37td_wnc.js → 0ahfwmbkpgfw4.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/{043j99m8ykg__.css → 0bhidk90e-07f.css} +1 -1
  121. package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{3ugmd_7dyn0id.js → 1v_tp3hm8wyhe.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +1 -0
  124. package/.next/standalone/.next/static/chunks/{3yxro_r2_o9ad.js → 2vo7qbdbvnc0o.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{0fqd7m_u81mi5.js → 2z7i-yg59w4pn.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{129ag2bw93bdh.js → 3bgot5v6f6s3c.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{3otmypm6j_xfo.js → 3o3f1ibfci0p7.js} +2 -2
  129. package/.next/standalone/SECURITY.md +53 -0
  130. package/.next/standalone/app/actions/get-jev-config.ts +194 -14
  131. package/.next/standalone/app/actions/update-jev-config.ts +160 -11
  132. package/.next/standalone/app/policies/hooks-client.tsx +5 -3
  133. package/.next/standalone/app/settings/jev-panel.tsx +169 -12
  134. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  135. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  136. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  137. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  138. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  139. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  140. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  141. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  142. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  143. package/.next/standalone/package.json +9 -9
  144. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  145. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  146. package/.next/standalone/sdk/typescript/CHANGELOG.md +15 -1
  147. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +10 -30
  148. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +3 -0
  149. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -16
  150. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +3 -0
  151. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +13 -132
  152. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +4 -0
  153. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -142
  154. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +4 -0
  155. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +1396 -1016
  156. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +9 -0
  157. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  158. package/.next/standalone/server.js +1 -1
  159. package/README.md +1 -0
  160. package/bin/failproofai.mjs +37 -6
  161. package/dist/cli.mjs +2861 -1145
  162. package/dist/worker.mjs +815 -197
  163. package/package.json +9 -9
  164. package/src/hooks/builtin-policies.ts +24 -6
  165. package/src/hooks/cloud-connection.ts +190 -1
  166. package/src/hooks/cloud-enrollment-cli.ts +56 -7
  167. package/src/hooks/cloud-introspect.ts +6 -0
  168. package/src/hooks/configure-wizard.ts +26 -6
  169. package/src/hooks/custom-hooks-loader.ts +58 -8
  170. package/src/hooks/effective-reviewers.ts +172 -8
  171. package/src/hooks/flush-cli.ts +35 -8
  172. package/src/hooks/fp-config.ts +266 -1
  173. package/src/hooks/fp-reset.ts +22 -4
  174. package/src/hooks/handler.ts +44 -7
  175. package/src/hooks/hook-activity-store.ts +13 -5
  176. package/src/hooks/jev-cli.ts +1009 -86
  177. package/src/hooks/jev-cloud-connection.ts +392 -0
  178. package/src/hooks/manager.ts +44 -6
  179. package/src/hooks/pack-cli.ts +175 -37
  180. package/src/hooks/pack-failclosed.ts +3 -0
  181. package/src/hooks/pack-manifest.ts +11 -1
  182. package/src/hooks/pack-store.ts +47 -10
  183. package/src/hooks/policy-authority.ts +73 -20
  184. package/src/hooks/policy-catalog.ts +10 -8
  185. package/src/hooks/policy-evaluator.ts +31 -8
  186. package/src/hooks/policy-registry.ts +3 -2
  187. package/src/hooks/policy-reviewability.ts +41 -12
  188. package/src/hooks/policy-types.ts +5 -3
  189. package/src/hooks/semantic/combine.ts +124 -84
  190. package/src/hooks/semantic/decide.ts +16 -0
  191. package/src/hooks/semantic/envelope.ts +18 -59
  192. package/src/hooks/semantic/evaluator.ts +11 -2
  193. package/src/hooks/semantic/facts.ts +7 -1
  194. package/src/hooks/semantic/intent.ts +8 -8
  195. package/src/hooks/semantic/jev-client.ts +540 -28
  196. package/src/hooks/semantic/jev-config.ts +586 -38
  197. package/src/hooks/semantic/jev-review.ts +26 -11
  198. package/src/hooks/semantic/jev-throttle.ts +8 -2
  199. package/src/hooks/semantic/pack-policies.ts +58 -15
  200. package/src/hooks/semantic/policies.ts +4 -3
  201. package/src/hooks/semantic/redact.ts +28 -1
  202. package/src/hooks/semantic/session-root.ts +116 -0
  203. package/src/hooks/semantic/types.ts +11 -0
  204. package/src/hooks/tui.ts +4 -0
  205. package/src/hooks/worker-server.ts +2 -1
  206. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  207. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bf34x4._.js +0 -22
  208. package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
  209. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
  210. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
  211. package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
  212. package/.next/standalone/.next/server/chunks/ssr/_1w_5l7t._.js +0 -3
  213. package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
  214. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
  215. package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
  216. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
  217. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
  218. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  219. package/.next/standalone/.next/static/chunks/1qd741hzlmjbo.js +0 -1
  220. package/.next/standalone/.next/static/chunks/2_pltstd8-xgs.js +0 -1
  221. package/.next/standalone/.next/static/chunks/2c8j9l6j_b1ci.js +0 -1
  222. package/.next/standalone/.next/static/chunks/3-k569wzcli8q.js +0 -1
  223. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_buildManifest.js +0 -0
  224. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_clientMiddlewareManifest.js +0 -0
  225. /package/.next/standalone/.next/static/{gbEOjBgZAxF2UIUwZVHNu → O_b5R1axf5cb4NxIXq2kS}/_ssgManifest.js +0 -0
@@ -17,7 +17,7 @@ import { readFile } from "node:fs/promises";
17
17
  import { pathToFileURL } from "node:url";
18
18
  import { createHash } from "node:crypto";
19
19
  import { hookLogWarn, hookLogError, hookLogInfo } from "./hook-logger";
20
- import { customPolicies, getCustomHooks, clearCustomHooks } from "./custom-hooks-registry";
20
+ import { customPolicies, getCustomHooks, getSemanticRegistrations, clearCustomHooks } from "./custom-hooks-registry";
21
21
  import {
22
22
  findDistIndex,
23
23
  rewriteFileTree,
@@ -33,7 +33,8 @@ 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";
36
- import { warnAuthority, withMergedAuthority } from "./policy-authority";
36
+ import { effectiveReviewerNames } from "./effective-reviewers";
37
+ import { refusedAuthorityWarning, warnAuthority, withMergedAuthority } from "./policy-authority";
37
38
 
38
39
  const LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
39
40
 
@@ -179,6 +180,8 @@ export interface PolicyLoadFailure {
179
180
  reason: string;
180
181
  }
181
182
 
183
+ const warnedSemanticOutsidePack = new Set<string>();
184
+
182
185
  async function loadSingleFile(
183
186
  absPath: string,
184
187
  opts?: {
@@ -186,6 +189,12 @@ async function loadSingleFile(
186
189
  conventionScope?: "project" | "user";
187
190
  /** Cloud-managed policies pass their pinned digest for load-time re-verification. */
188
191
  verifyEntrySha?: string;
192
+ /**
193
+ * Not a pack artifact, so a `semanticPolicies.add` in it is never asked: a
194
+ * Jev check reaches a machine only through a pack's manifest. Said once per
195
+ * file, whether or not Jev is configured — the declaration is dead either way.
196
+ */
197
+ outsidePack?: boolean;
189
198
  },
190
199
  ): Promise<PolicyLoadFailure | null> {
191
200
  const g = globalThis as Record<string, unknown>;
@@ -221,7 +230,16 @@ async function loadSingleFile(
221
230
  const entryTmp = absPath + tmpSuffix;
222
231
  const fileUrl = pathToFileURL(entryTmp).href;
223
232
  const hooksBefore = getCustomHooks().length;
233
+ const semanticBefore = getSemanticRegistrations().length;
224
234
  await importWithDeadline(fileUrl);
235
+ const ignored = getSemanticRegistrations().slice(semanticBefore).map((d) => d?.name);
236
+ if (opts?.outsidePack && ignored.length > 0 && !warnedSemanticOutsidePack.has(absPath)) {
237
+ warnedSemanticOutsidePack.add(absPath);
238
+ hookLogWarn(
239
+ `${basename(absPath)}: semanticPolicies.add only takes effect in a pack published with \`failproofai publish\`; ` +
240
+ `${ignored.join(", ")} ${ignored.length === 1 ? "is" : "are"} never asked here`,
241
+ );
242
+ }
225
243
  if (policyModuleCache.size >= POLICY_MODULE_CACHE_MAX_ENTRIES) policyModuleCache.clear();
226
244
  policyModuleCache.set(absPath, {
227
245
  fingerprint,
@@ -453,14 +471,28 @@ export async function loadAllCustomHooks(
453
471
  // every assignment behind these bytes says so. Keeping one record's
454
472
  // declaration let the order of active.json decide, so a team's reviewable
455
473
  // assignment could clear what an org-wide hard one enforces.
474
+ //
475
+ // Judged against the checks THIS MACHINE can ask, which is what registration
476
+ // will judge these same declarations by. Against the builtin set instead, a
477
+ // `reviewedBy` naming a check an installed pack ships reads as unknown here,
478
+ // both assignments resolve hard, and the merge has no way to tell the
479
+ // reviewable one from the hard one. Read inside this branch, so a deployment
480
+ // with no duplicate artifact does not pay for the manifest read at all.
481
+ const knownReviewers = effectiveReviewerNames();
456
482
  const winner = existing.effect !== "enforce" && policy.effect === "enforce" ? policy : existing;
457
- const { merged, overruled } = withMergedAuthority(winner, [existing, policy]);
483
+ const { merged, overruled, refused } = withMergedAuthority(winner, [existing, policy], knownReviewers);
458
484
  if (overruled) {
459
485
  warnAuthority(
460
486
  `cloud-managed policies ${existing.id} and ${policy.id} share one artifact and do not all declare it ` +
461
487
  `reviewable, so it stays hard`,
462
488
  );
463
489
  }
490
+ // The merge hardened the record, so registration will not see the refused
491
+ // declaration and cannot report it. Said here instead, naming both
492
+ // assignments — which is more than registration could have said.
493
+ if (refused) {
494
+ warnAuthority(refusedAuthorityWarning(`cloud-managed policies ${existing.id} and ${policy.id}`, refused));
495
+ }
464
496
  cloudManagedByPath.set(key, merged);
465
497
  }
466
498
 
@@ -501,16 +533,27 @@ export async function loadAllCustomHooks(
501
533
  * the policy all the same, undeclared, and an undeclared pack policy is hard.
502
534
  * Winner precedence here would let one pack's manifest make another pack's
503
535
  * policy reviewable, which is the one thing a manifest may never do.
536
+ *
537
+ * `knownReviewers` is what makes that hold for a pack that ships BOTH tiers.
538
+ * Judged against the compiled-in set, a `reviewedBy` naming one of the pack's
539
+ * own checks is a name nothing here has, so the reviewable entry and its hard
540
+ * peer resolved alike and the merge could not tell them apart — while
541
+ * registration, which reads the manifest's own checks, honoured it. The set
542
+ * passed here is that same one.
504
543
  */
505
544
  const unionCatalog = (
506
545
  winner: PolicyCatalogEntry[],
507
546
  other: PolicyCatalogEntry[],
508
- ): { policies: PolicyCatalogEntry[]; overruled: string[] } => {
547
+ knownReviewers: ReadonlySet<string>,
548
+ ): { policies: PolicyCatalogEntry[]; overruled: string[]; refused: string[] } => {
509
549
  /** Names a pack asked to be reviewable and did not get, for the warning. */
510
550
  const overruled: string[] = [];
551
+ /** The refusals themselves, which registration will no longer see to report. */
552
+ const refused: string[] = [];
511
553
  const harden = (entry: PolicyCatalogEntry, peer: PolicyCatalogEntry | undefined) => {
512
- const r = withMergedAuthority(entry, [entry, peer ?? {}]);
554
+ const r = withMergedAuthority(entry, [entry, peer ?? {}], knownReviewers);
513
555
  if (r.overruled) overruled.push(entry.name);
556
+ if (r.refused) refused.push(refusedAuthorityWarning(entry.name, r.refused));
514
557
  return r.merged;
515
558
  };
516
559
  return {
@@ -519,6 +562,7 @@ export async function loadAllCustomHooks(
519
562
  ...other.filter((p) => !winner.some((w) => w.name === p.name)).map((p) => harden(p, undefined)),
520
563
  ],
521
564
  overruled,
565
+ refused,
522
566
  };
523
567
  };
524
568
 
@@ -566,7 +610,9 @@ export async function loadAllCustomHooks(
566
610
  // being intersected away.
567
611
  const winner = existing.effect !== "enforce" && pack.effect === "enforce" ? pack : existing;
568
612
  const other = winner === existing ? pack : existing;
569
- const catalog = unionCatalog(winner.policies, other.policies);
613
+ // Read here rather than at the top of the load: only a duplicate artifact
614
+ // needs it, and this is the branch that has one.
615
+ const catalog = unionCatalog(winner.policies, other.policies, effectiveReviewerNames());
570
616
  packByPath.set(key, {
571
617
  ...winner,
572
618
  policies: catalog.policies,
@@ -579,6 +625,9 @@ export async function loadAllCustomHooks(
579
625
  `${catalog.overruled.join(", ")} reviewable, so ${catalog.overruled.length === 1 ? "it stays" : "they stay"} hard`,
580
626
  );
581
627
  }
628
+ // A refused `reviewable` is now hardened here rather than passed on to
629
+ // registration, so this is the only place left that can name the reason.
630
+ for (const message of catalog.refused) warnAuthority(message);
582
631
  }
583
632
 
584
633
  // 1. Explicit custom policy paths. Accept a string for callers/configs using
@@ -604,6 +653,7 @@ export async function loadAllCustomHooks(
604
653
  const failure = await loadSingleFile(absPath, {
605
654
  verifyEntrySha:
606
655
  cloudManaged?.sha256 ?? pack?.sha256,
656
+ outsidePack: !pack,
607
657
  });
608
658
  // Every id behind these bytes. One artifact, one import, one failure —
609
659
  // but as many fail-closed guards as there are packs depending on it.
@@ -684,7 +734,7 @@ export async function loadAllCustomHooks(
684
734
  for (const file of projectFiles) {
685
735
  loadedPaths.add(file);
686
736
  const hooksBefore = getCustomHooks().length;
687
- await loadSingleFile(file, { conventionScope: projectScope });
737
+ await loadSingleFile(file, { conventionScope: projectScope, outsidePack: true });
688
738
  const newHooks = getCustomHooks().slice(hooksBefore);
689
739
  for (const hook of newHooks) {
690
740
  (hook as CustomHook & { __policyId?: string }).__policyId = conventionPolicyId(projectScope, basename(file), hook.name);
@@ -724,7 +774,7 @@ export async function loadAllCustomHooks(
724
774
  for (const file of userFiles) {
725
775
  loadedPaths.add(file);
726
776
  const hooksBefore = getCustomHooks().length;
727
- await loadSingleFile(file, { conventionScope: "user" });
777
+ await loadSingleFile(file, { conventionScope: "user", outsidePack: true });
728
778
  const newHooks = getCustomHooks().slice(hooksBefore);
729
779
  for (const hook of newHooks) {
730
780
  (hook as CustomHook & { __policyId?: string }).__policyId = conventionPolicyId("user", basename(file), hook.name);
@@ -38,15 +38,141 @@
38
38
  * — loudly here, but it is the shape of a change that would later break them
39
39
  * silently.
40
40
  */
41
- import { readInstalledPacks } from "./pack-manifest";
41
+ import { readInstalledPacks, type ResolvedPack } from "./pack-manifest";
42
42
  import { SEMANTIC_REVIEWER_NAMES } from "./policy-authority";
43
43
 
44
44
  let cached: ReadonlySet<string> | null = null;
45
+ let cachedContested: ReadonlyMap<string, string[]> = new Map();
46
+ /** The agent the current registration pass is for; see {@link forgetEffectiveReviewerNames}. */
47
+ let reviewerCli: string | undefined;
48
+
49
+ /**
50
+ * The packs whose Jev checks take part for `cli`, filtered the way the regex
51
+ * half already is: an `observe` pack blocks nothing, and a pack scoped to other
52
+ * agents guards none here. Its checks would otherwise replace the questions and
53
+ * supply reviewers that clear enforce packs' denies on every agent. Both the
54
+ * reviewer set and the question set start from this list, so they agree.
55
+ */
56
+ export function jevPacks<T extends Pick<ResolvedPack, "effect" | "clis">>(packs: ReadonlyArray<T>, cli?: string): T[] {
57
+ return packs.filter(
58
+ (p) => p.effect !== "observe" && !(cli && Array.isArray(p.clis) && p.clis.length > 0 && !p.clis.includes(cli)),
59
+ );
60
+ }
61
+
62
+ /**
63
+ * A stable serialization, for comparing two declarations of the same name.
64
+ * Key order is a manifest's business, not a difference in what is asked.
65
+ */
66
+ function canonical(value: unknown): string {
67
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
68
+ if (value !== null && typeof value === "object") {
69
+ const entries = Object.entries(value as Record<string, unknown>)
70
+ .filter(([, v]) => v !== undefined)
71
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
72
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
73
+ }
74
+ return JSON.stringify(value) ?? "null";
75
+ }
76
+
77
+ /**
78
+ * The semantic check names that more than one installed pack claims, with
79
+ * declarations that are not the same check — and which this machine must
80
+ * therefore honour for nobody. Keyed by name, valued with the packs that
81
+ * claimed it, for the warning.
82
+ *
83
+ * ## Why a contested name has to be refused rather than resolved
84
+ *
85
+ * The reviewer set is the whole protection: a policy is `reviewable` only when
86
+ * every name its `reviewedBy` lists is a check this machine can actually ask.
87
+ * The union across packs is deliberate and must keep working — the shipped
88
+ * configuration is exactly that, `FailproofAI/policies` naming checks that live
89
+ * in `FailproofAI/jev-policies` — but the union was AMBIGUOUS. Two packs may be
90
+ * installed at once, `semanticPoliciesFromPacks` keeps the first declaration of
91
+ * a duplicated name and drops the later one, and nothing tied a name to the pack
92
+ * that meant it. So a second pack declaring `production-infra-change` with a
93
+ * question that answers "no concern" to everything supplies the reviewer for the
94
+ * FIRST pack's policies: install it, and every policy reviewable by that name is
95
+ * cleared on every call. The pack doing it never declares a regex policy, and
96
+ * scoping reviewers per pack is not the fix — that would break the pairing the
97
+ * product ships.
98
+ *
99
+ * So the name leaves the set. The policies naming it resolve to `hard`, the
100
+ * question is not asked, and the pack that tried it gains nothing: stricter than
101
+ * either author asked for, with the regex deny simply standing. Nothing is
102
+ * failed closed over it — one policy being hard is a cost; a pack that denies
103
+ * every tool call is a different order of failure (`pack-failclosed.ts`).
104
+ *
105
+ * ## Why identical declarations are not a contest
106
+ *
107
+ * Content-addressed artifacts mean a fork or a re-publish arrives as two packs
108
+ * carrying the same semantic entries, and `custom-hooks-loader.ts` treats that
109
+ * shape as expected rather than as a corner. Both declarations are then the same
110
+ * question, so whichever the resolver keeps asks exactly what the other would
111
+ * have: nothing is ambiguous, and refusing the name would switch off the
112
+ * clearing half on a machine whose two packs agree to the byte.
113
+ *
114
+ * Pure, and takes the packs rather than reading them, so the two callers that
115
+ * already have them do not read `installed.json` again — and so both answer from
116
+ * the same list. They MUST agree: a name in the reviewer set whose question is
117
+ * another pack's is the bug this function exists to remove.
118
+ */
119
+ export function contestedSemanticNames(
120
+ packs: ReadonlyArray<Pick<ResolvedPack, "id" | "semantic"> & { source?: string }>,
121
+ ): ReadonlyMap<string, string[]> {
122
+ const claims = new Map<string, { form: string; ids: string[] }>();
123
+ const contested = new Map<string, string[]>();
124
+ for (const pack of packs) {
125
+ for (const entry of pack.semantic ?? []) {
126
+ // Void, so not a claim either: see `isReservedClaim`.
127
+ if (isReservedClaim(pack, entry.name)) continue;
128
+ const form = canonical(entry);
129
+ const claim = claims.get(entry.name);
130
+ if (claim === undefined) {
131
+ claims.set(entry.name, { form, ids: [pack.id] });
132
+ continue;
133
+ }
134
+ claim.ids.push(pack.id);
135
+ // The array is the live one, so a third claimant is named too.
136
+ if (claim.form !== form) contested.set(entry.name, claim.ids);
137
+ }
138
+ }
139
+ return contested;
140
+ }
141
+
142
+ /**
143
+ * Installed from a FailproofAI repository. Read from `source`, which this CLI
144
+ * wrote from the repository it fetched, never from the pack's self-declared `id`.
145
+ */
146
+ export function isFirstPartyPack(pack: { source?: string }): boolean {
147
+ return /^github:FailproofAI\//i.test(pack.source ?? "");
148
+ }
149
+
150
+ /**
151
+ * A builtin check name declared by a pack that is not FailproofAI's. Core and
152
+ * user policies name those checks in `reviewedBy`, so the third party's
153
+ * question would become the reviewer that clears them — an `instruct`-mode
154
+ * `destructive-deletion` beside the core pack turned `block-rm-rf` into a
155
+ * warning. The claim is void: never asked, never a reviewer, and never a
156
+ * second declaration that contests FailproofAI's own, which would switch off a
157
+ * deny the regex tier does not have. The reviewer set, the question set and
158
+ * the contest all apply it.
159
+ */
160
+ export function isReservedClaim(pack: { source?: string }, name: string): boolean {
161
+ return SEMANTIC_REVIEWER_NAMES.has(name) && !isFirstPartyPack(pack);
162
+ }
45
163
 
46
164
  /**
47
165
  * The installed packs' semantic policy names when any pack declares some, and
48
166
  * this build's compiled-in set otherwise.
49
167
  *
168
+ * A name two packs claim differently is left out — see
169
+ * {@link contestedSemanticNames}. When that leaves nothing, the compiled-in set
170
+ * stands, which is not a softening but the same rule
171
+ * `semanticPoliciesFromPacks` applies to the QUESTIONS: a declaring pack whose
172
+ * every entry was unusable leaves the compiled-in semantic set live, so the
173
+ * names honoured here are the names of the questions that will actually be
174
+ * asked, and each of those is one of ours.
175
+ *
50
176
  * Never throws: an unreadable manifest declares nothing, and the builtin set
51
177
  * stands. That is the same fail-open posture every other reader of the pack
52
178
  * manifest takes, and here it also fails in the safe direction — a pack's
@@ -56,12 +182,11 @@ let cached: ReadonlySet<string> | null = null;
56
182
  export function effectiveReviewerNames(): ReadonlySet<string> {
57
183
  if (cached) return cached;
58
184
  let names: ReadonlySet<string> = SEMANTIC_REVIEWER_NAMES;
185
+ cachedContested = new Map();
59
186
  try {
60
- const declared = readInstalledPacks().packs.flatMap((p) => (p.semantic ?? []).map((s) => s.name));
61
- // Only when a pack actually declared some. A machine whose packs carry only
62
- // the regex floor still runs the compiled-in semantic set, so its builtin
63
- // reviewer names are the live ones.
64
- if (declared.length > 0) names = new Set(declared);
187
+ const packs = jevPacks(readInstalledPacks().packs, reviewerCli);
188
+ names = reviewerNamesFor(packs);
189
+ cachedContested = contestedSemanticNames(packs);
65
190
  } catch {
66
191
  // See above: silence here is the builtin set, not an empty one.
67
192
  }
@@ -69,11 +194,50 @@ export function effectiveReviewerNames(): ReadonlySet<string> {
69
194
  return names;
70
195
  }
71
196
 
197
+ /** The contest behind {@link effectiveReviewerNames}' answer, so a refusal can name it. */
198
+ export function contestedReviewerNames(): ReadonlyMap<string, string[]> {
199
+ effectiveReviewerNames();
200
+ return cachedContested;
201
+ }
202
+
203
+ /**
204
+ * A first-party pack declares Jev checks, so they REPLACE the compiled-in set.
205
+ * Anyone else's checks are ADDED to it: a stranger's one check must not switch
206
+ * off `credential-exfiltration` and turn every core reviewable policy hard.
207
+ * `semanticPoliciesFromPacks` applies the same rule to the questions.
208
+ */
209
+ export function replacesBuiltinChecks(packs: ReadonlyArray<Pick<ResolvedPack, "semantic"> & { source?: string }>): boolean {
210
+ return packs.some((p) => isFirstPartyPack(p) && (p.semantic ?? []).length > 0);
211
+ }
212
+
213
+ /**
214
+ * The reviewer set for the packs taking part: see {@link replacesBuiltinChecks}. Pure.
215
+ *
216
+ * Manifest-only: it cannot see a check `semanticPoliciesFromPacks` drops for the
217
+ * question budget, because measuring questions means loading the semantic
218
+ * modules this hook-path file must not reach. That overcount is fail-safe — a
219
+ * dropped check is never answered, so it never clears — and the diagnostic
220
+ * (`surveyReviewableCoverage`) asks the resolver instead.
221
+ */
222
+ export function reviewerNamesFor(
223
+ packs: ReadonlyArray<Pick<ResolvedPack, "id" | "semantic"> & { source?: string }>,
224
+ ): ReadonlySet<string> {
225
+ const contested = contestedSemanticNames(packs);
226
+ const declared = packs
227
+ .flatMap((p) => (p.semantic ?? []).filter((s) => !isReservedClaim(p, s.name)).map((s) => s.name))
228
+ .filter((name) => !contested.has(name));
229
+ // Nothing usable declared: the compiled-in set is the one being asked.
230
+ if (declared.length === 0) return SEMANTIC_REVIEWER_NAMES;
231
+ return new Set(replacesBuiltinChecks(packs) ? declared : [...SEMANTIC_REVIEWER_NAMES, ...declared]);
232
+ }
233
+
72
234
  /**
73
- * Drop the cached answer. Called by `clearPolicies()`, which every evaluation
235
+ * Drop the cached answer, and set the agent the next one is for. Called by
236
+ * `clearPolicies(cli)`, which every evaluation
74
237
  * runs before it registers anything — so a pack installed under a long-lived
75
238
  * warm worker is picked up on the next event rather than at the next restart.
76
239
  */
77
- export function forgetEffectiveReviewerNames(): void {
240
+ export function forgetEffectiveReviewerNames(cli?: string): void {
78
241
  cached = null;
242
+ reviewerCli = cli;
79
243
  }
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import { existsSync, mkdirSync, writeFileSync, readdirSync } from "node:fs";
23
23
  import { dirname, join } from "node:path";
24
- import { failproofaiHome, spoolDir } from "./fp-home";
24
+ import { customAgentsEventsDir, failedDir, failproofaiHome, spoolDir } from "./fp-home";
25
25
  import { readConfig } from "./fp-config";
26
26
  import { readIngestCredential } from "./collector-config";
27
27
  import { daemonServiceStatus, isDaemonSupportedPlatform } from "./daemon-service";
@@ -49,19 +49,41 @@ export function flushRequestPath(home?: string): string {
49
49
  return join(failproofaiHome(home), "state", "flush-request.json");
50
50
  }
51
51
 
52
- /** Every directory the collector spools batches into. */
52
+ /** Every directory the collector spools batches into. The daemon writes its
53
+ * own batches FLAT into state/spool and the SDKs into custom-agents/events
54
+ * (`spool_dirs` in crates/fpai-collect/src/config.rs). Counting only
55
+ * subdirectories of state/spool, as this once did, found nothing on every
56
+ * machine, so flush always answered "everything already delivered". */
53
57
  function spoolDirs(home?: string): string[] {
54
58
  const root = spoolDir(home);
55
- if (!existsSync(root)) return [];
59
+ const dirs = [root, customAgentsEventsDir(home)];
60
+ if (!existsSync(root)) return dirs;
56
61
  try {
57
- return readdirSync(root, { withFileTypes: true })
58
- .filter((e) => e.isDirectory() && e.name !== "failed")
59
- .map((e) => join(root, e.name));
62
+ return dirs.concat(
63
+ readdirSync(root, { withFileTypes: true })
64
+ .filter((e) => e.isDirectory() && e.name !== "failed")
65
+ .map((e) => join(root, e.name)),
66
+ );
60
67
  } catch {
61
- return [];
68
+ return dirs;
62
69
  }
63
70
  }
64
71
 
72
+ /** Batches parked in state/failed after failed uploads, `.poison` excluded.
73
+ * Flush does not resend these: the daemon retries them about hourly and
74
+ * when it starts, so all this can do is not call them delivered. */
75
+ function parkedBatches(home?: string): number {
76
+ try {
77
+ return readdirSync(failedDir(home)).filter((f) => f.endsWith(".jsonl")).length;
78
+ } catch {
79
+ return 0;
80
+ }
81
+ }
82
+
83
+ function parkedLine(n: number, home?: string): string {
84
+ return `${n} batch${n === 1 ? "" : "es"} parked in ${failedDir(home)} after failed uploads; the daemon retries them about hourly and when it restarts.`;
85
+ }
86
+
65
87
  /** Batches awaiting delivery. `.tmp` files are half-written and not counted. */
66
88
  export function pendingBatches(home?: string): number {
67
89
  let n = 0;
@@ -138,8 +160,12 @@ export async function runFlushCommand(opts: FlushOptions = {}): Promise<FlushRes
138
160
  }
139
161
 
140
162
  const before = pendingBatches(home);
163
+ const parked = parkedBatches(home);
141
164
  if (before === 0) {
142
- return { exitCode: 0, pending: 0, lines: ["Nothing spooled — everything already delivered."] };
165
+ const nothing = parked
166
+ ? ["Nothing spooled.", parkedLine(parked, home)]
167
+ : ["Nothing spooled — everything already delivered."];
168
+ return { exitCode: 0, pending: 0, lines: nothing };
143
169
  }
144
170
 
145
171
  const path = flushRequestPath(home);
@@ -149,6 +175,7 @@ export async function runFlushCommand(opts: FlushOptions = {}): Promise<FlushRes
149
175
  });
150
176
 
151
177
  lines.push(`${before} batch${before === 1 ? "" : "es"} spooled. Requested delivery.`);
178
+ if (parked) lines.push(parkedLine(parked, home));
152
179
 
153
180
  if (!wait) {
154
181
  lines.push("The daemon picks this up within a few seconds.");