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
@@ -91,7 +91,7 @@ export const SEMANTIC_POLICY_NAMES = [
91
91
  * A pack that declares its own `semantic` entries replaces
92
92
  * `SEMANTIC_POLICIES` wholesale on the machine that installed it, so the names
93
93
  * a `reviewedBy` may use there are the pack's. Callers that know which set is
94
- * live pass it (`effectiveReviewerNames()` in `pack-manifest.ts`, which reads
94
+ * live pass it (`effectiveReviewerNames()` in `effective-reviewers.ts`, which reads
95
95
  * the manifest and imports nothing from `semantic/`); everyone else gets this
96
96
  * one, which is what a machine with no pack runs.
97
97
  */
@@ -124,6 +124,8 @@ const isName = (n: unknown): n is string => typeof n === "string" && n.length >
124
124
  export function resolvePolicyAuthority(
125
125
  decl: AuthorityDeclaration | undefined,
126
126
  knownReviewers: ReadonlySet<string> = SEMANTIC_REVIEWER_NAMES,
127
+ /** Names left out of `knownReviewers` because packs declare them differently, with those packs. */
128
+ contested?: ReadonlyMap<string, string[]>,
127
129
  ): ResolvedAuthority {
128
130
  const declaredReviewable = decl?.authority === "reviewable";
129
131
  if (!decl || effectiveAuthority(decl) === "hard") {
@@ -136,16 +138,28 @@ export function resolvePolicyAuthority(
136
138
  }
137
139
  const unknown = names.filter((n) => !knownReviewers.has(n));
138
140
  if (unknown.length > 0) {
139
- return {
140
- authority: "hard",
141
- downgraded:
142
- `reviewedBy names ${unknown.map((n) => JSON.stringify(n)).join(", ")}, ` +
143
- `which ${unknown.length === 1 ? "is not a semantic policy" : "are not semantic policies"} in this build`,
144
- };
141
+ return { authority: "hard", downgraded: `reviewedBy names ${unknown.map((n) => whyUnknown(n, knownReviewers, contested)).join("; ")}` };
145
142
  }
146
143
  return { authority: "reviewable", reviewedBy: [...new Set(names)] };
147
144
  }
148
145
 
146
+ /**
147
+ * The rule that kept one name out. "Not in this build" is only true of the
148
+ * compiled-in set: a pack's own checks, or a name two packs disagree on, is a
149
+ * different reason, and the author reading it has a different fix.
150
+ */
151
+ function whyUnknown(name: string, known: ReadonlySet<string>, contested?: ReadonlyMap<string, string[]>): string {
152
+ // Quoted with JSON.stringify: a pack's name reaches the hook's stderr, so a
153
+ // control character in it must not reach the terminal raw.
154
+ const quoted = JSON.stringify(name);
155
+ const claimants = contested?.get(name);
156
+ if (claimants) return `${quoted}, which packs ${claimants.join(" and ")} declare differently, so it is asked for neither`;
157
+ if (known === SEMANTIC_REVIEWER_NAMES) return `${quoted}, which is not a semantic policy in this build`;
158
+ const sorted = [...known].sort();
159
+ const listed = sorted.length > 6 ? `${sorted.slice(0, 6).join(", ")} and ${sorted.length - 6} more` : sorted.join(", ");
160
+ return `${quoted}, which is not among the Jev checks it is judged against (${listed})`;
161
+ }
162
+
149
163
  function whyHard(decl: AuthorityDeclaration): string {
150
164
  if (decl.alwaysOn === true) return "it is alwaysOn, and the self-protection guard is never reviewable";
151
165
  if (!Array.isArray(decl.reviewedBy) || decl.reviewedBy.length === 0) {
@@ -238,37 +252,76 @@ export function manifestAuthority(
238
252
  * So it resolves toward HARD, the way the collapse resolves the effect toward
239
253
  * enforce: reviewable only when EVERY declaration resolves to reviewable, and
240
254
  * then through the UNION of their checks (more checks that must all come back
241
- * clear is stricter, never weaker). Otherwise it takes the first declaration
242
- * that is not reviewable, so a refused one is still reported at registration.
255
+ * clear is stricter, never weaker).
256
+ *
257
+ * ## What the merged record may carry, and why it is the RESOLUTION
258
+ *
259
+ * Nothing here copies a declaration through. It used to keep the first one that
260
+ * was not reviewable, so that a refused `reviewable` would be reported again at
261
+ * registration — and that was the hole: a declaration is only ever resolved
262
+ * against the reviewer set of whoever is looking, so a `reviewable` naming a
263
+ * PACK's own check resolved to hard here, against the builtin set, and was
264
+ * copied forward verbatim. `registerPolicy` then resolved that same raw
265
+ * declaration against `effectiveReviewerNames()`, which does have the pack's
266
+ * check — and the policy registered reviewable, with a hard peer behind the same
267
+ * artifact overruled by nothing more than which pack was listed first.
268
+ *
269
+ * Two changes close it. The reviewer set is now a PARAMETER, so the merge judges
270
+ * by the same names registration will; and a non-reviewable merge emits
271
+ * `{ authority: "hard" }` — the resolved value — rather than the fields it was
272
+ * asked for. Either alone would have been enough for the case above; both,
273
+ * because the first is an agreement between two callers and the second holds
274
+ * whether or not they agree.
275
+ *
276
+ * The reason a refused declaration carried is not lost: it comes back as
277
+ * `refused`, for the caller to say through {@link refusedAuthorityWarning} at
278
+ * the point where it knows which artifact and which packs are involved.
243
279
  *
244
280
  * Returns `record` itself when its own fields already say that, and a copy with
245
281
  * only the two authority fields replaced otherwise. `overruled` is true when
246
282
  * some declaration asked for reviewable, validly, and did not get it.
283
+ *
284
+ * @param knownReviewers - the checks that can be asked on this machine. Callers
285
+ * that merge what will be REGISTERED must pass `effectiveReviewerNames()`;
286
+ * the default is this build's compiled-in set, which is what a machine with no
287
+ * pack runs.
247
288
  */
248
289
  export function withMergedAuthority<T extends AuthorityFields>(
249
290
  record: T,
250
291
  declarations: ReadonlyArray<AuthorityFields>,
251
- ): { merged: T; overruled: boolean } {
252
- const resolved = declarations.map((d) => resolvePolicyAuthority(d));
292
+ knownReviewers: ReadonlySet<string> = SEMANTIC_REVIEWER_NAMES,
293
+ ): { merged: T; overruled: boolean; refused?: string } {
294
+ const resolved = declarations.map((d) => resolvePolicyAuthority(d, knownReviewers));
253
295
  const allReviewable = resolved.length > 0 && resolved.every((r) => r.authority === "reviewable");
296
+ // The declaration that decided it is hard. Its own fields are not carried —
297
+ // only whether it said anything at all, because a record that declared
298
+ // nothing is already hard and rewriting it to say so would touch a record the
299
+ // collapse has no business touching.
300
+ const decided = declarations[resolved.findIndex((r) => r.authority !== "reviewable")] ?? {};
254
301
  const fields: AuthorityFields = allReviewable
255
302
  ? { authority: "reviewable", reviewedBy: [...new Set(resolved.flatMap((r) => r.reviewedBy ?? []))] }
256
- : fieldsOf(declarations[resolved.findIndex((r) => r.authority !== "reviewable")] ?? {});
303
+ : decided.authority === undefined
304
+ ? {}
305
+ : { authority: "hard" };
257
306
  const overruled = !allReviewable && resolved.some((r) => r.authority === "reviewable");
307
+ const refused = resolved.find((r) => r.downgraded !== undefined)?.downgraded;
308
+ const extra = refused === undefined ? {} : { refused };
258
309
  const same =
259
310
  record.authority === fields.authority &&
260
311
  JSON.stringify(record.reviewedBy) === JSON.stringify(fields.reviewedBy);
261
- if (same) return { merged: record, overruled };
312
+ if (same) return { merged: record, overruled, ...extra };
262
313
  const { authority: _a, reviewedBy: _r, ...rest } = record;
263
- return { merged: { ...rest, ...fields } as T, overruled };
314
+ return { merged: { ...rest, ...fields } as T, overruled, ...extra };
264
315
  }
265
316
 
266
- /** The authority fields a record actually carries, and nothing else. */
267
- function fieldsOf(d: AuthorityFields): AuthorityFields {
268
- return {
269
- ...(d.authority !== undefined ? { authority: d.authority } : {}),
270
- ...(d.reviewedBy !== undefined ? { reviewedBy: d.reviewedBy } : {}),
271
- };
317
+ /**
318
+ * The warning for a `reviewable` declaration that was not honored. One
319
+ * phrasing, wherever it is noticed — registration for a policy that reaches it
320
+ * with its declaration intact, and the loader for one the collapse hardened
321
+ * before it got there.
322
+ */
323
+ export function refusedAuthorityWarning(subject: string, reason: string): string {
324
+ return `${subject} asks to be reviewable, but ${reason} — it stays hard`;
272
325
  }
273
326
 
274
327
  const warnedAuthority = new Set<string>();
@@ -457,13 +457,14 @@ export const POLICY_CATALOG: PolicyCatalogEntry[] = [
457
457
  // policy off on every machine that configured Jev, while the mark said
458
458
  // otherwise.
459
459
  //
460
- // The test that matters is not "can the reviewer keep this block" but "is
461
- // there anything left that can deny". `block-read-outside-cwd` also has only
462
- // an instruct reviewer and stays reviewable, because when it clears,
463
- // `secret-exposure` and `credential-exfiltration` are still asked about the
464
- // same read and still deny on their own through the most-severe merge. Here
465
- // nothing else covers committing on a protected branch, so a clear leaves
466
- // the concern unenforced by anything.
460
+ // The engine has since changed under that argument: a reviewer that FIRES
461
+ // with nobody consenting now keeps the block (`combine.ts`, "A check that
462
+ // fired without consent keeps the floor"), so an instruct-only reviewer is
463
+ // no longer "off". `block-read-outside-cwd`, which also has only an
464
+ // instruct reviewer, stays reviewable on exactly that rule: it clears when
465
+ // the read is requested or the check finds nothing, and keeps the deny on
466
+ // an unrequested read it flags. Making this one reviewable again is a
467
+ // product call nobody has made.
467
468
  authority: "hard",
468
469
  defaultEnabled: false,
469
470
  category: "Git",
@@ -526,7 +527,8 @@ export const POLICY_CATALOG: PolicyCatalogEntry[] = [
526
527
  // question as written.
527
528
  //
528
529
  // A named check that is asked and does not fire answers "no concern", and
529
- // that CLEARS (`combine.ts`, "A warning-level answer clears the deny"). So
530
+ // that CLEARS (`combine.ts`, "A check that fired without consent keeps the
531
+ // floor"). So
530
532
  // pairing this policy with `destructive-deletion` would not hand the
531
533
  // decision to Jev — it would switch the policy off on every machine that
532
534
  // configured Jev, which is exactly the `block-work-on-main` mistake. The
@@ -17,7 +17,16 @@ export { packPolicyParamKey } from "./pack-param-key";
17
17
  import { DEFAULT_POLICY_NAMESPACE, getPoliciesForEvent } from "./policy-registry";
18
18
  // Type-only apart from the two pure functions: the semantic evaluator itself is
19
19
  // loaded by handler.ts, lazily, and only when a Jev config exists.
20
- import { combineTwoTier, regexOnly, type FinalVerdict, type JevActivityFields, type JevReview, type RegexVerdict, type TwoTierReview } from "./semantic/combine";
20
+ import {
21
+ combineTwoTier,
22
+ regexOnly,
23
+ type FinalVerdict,
24
+ type JevActivityFields,
25
+ type JevReview,
26
+ type RegexVerdict,
27
+ type ShadowVerdict,
28
+ type TwoTierReview,
29
+ } from "./semantic/combine";
21
30
  // The code, not a copy of the string: the log line below turns on it, and a
22
31
  // rename should break the build here rather than quietly print "unavailable"
23
32
  // for a call Jev actually answered.
@@ -51,6 +60,10 @@ export interface EvaluationResult {
51
60
  activity: JevActivityFields;
52
61
  /** True when Jev's own verdict is what `policyName` names. */
53
62
  decidedByJev: boolean;
63
+ /** The pack a deciding Jev check came from; only with `decidedByJev`. */
64
+ jevOrigin?: { packId: string; packVersion?: string };
65
+ /** Shadow mode: Jev's own deny/instruct, recorded as a "would have" (see `ShadowVerdict`). */
66
+ shadowVerdict?: ShadowVerdict;
54
67
  };
55
68
  }
56
69
 
@@ -233,8 +246,13 @@ export async function evaluatePolicies(
233
246
  );
234
247
  }
235
248
  return {
236
- ...formatVerdict(eventType, session, toolName, combined.final),
237
- twoTier: { activity: combined.activity, decidedByJev: combined.decidedByJev },
249
+ ...formatVerdict(eventType, session, toolName, combined.final, combined.decidedByJev),
250
+ twoTier: {
251
+ activity: combined.activity,
252
+ decidedByJev: combined.decidedByJev,
253
+ ...(combined.decidedByJev && review.kind === "answered" && review.origin ? { jevOrigin: review.origin } : {}),
254
+ ...(combined.shadowVerdict ? { shadowVerdict: combined.shadowVerdict } : {}),
255
+ },
238
256
  };
239
257
  }
240
258
 
@@ -350,16 +368,17 @@ async function collectVerdicts(
350
368
  return { verdicts, hardDenied: false };
351
369
  }
352
370
 
353
- /** A verdict → the calling CLI's response. Which engine decided is irrelevant here. */
371
+ /** A verdict → the calling CLI's response. Which engine decided only changes whom a deny credits. */
354
372
  function formatVerdict(
355
373
  eventType: HookEventType,
356
374
  session: SessionMetadata | undefined,
357
375
  toolName: string | undefined,
358
376
  final: FinalVerdict,
377
+ byJev = false,
359
378
  ): EvaluationResult {
360
379
  if (final.decision === "deny") {
361
380
  const [decider] = final.entries;
362
- return formatDeny(eventType, session, toolName, decider.policyName, decider.reason);
381
+ return formatDeny(eventType, session, toolName, decider.policyName, decider.reason, byJev);
363
382
  }
364
383
  if (final.decision === "instruct") return formatInstruct(eventType, session, final.entries);
365
384
  if (final.entries.length > 0) return formatAllow(eventType, session, final.entries);
@@ -372,6 +391,7 @@ function formatDeny(
372
391
  toolName: string | undefined,
373
392
  policyName: string,
374
393
  reason: string,
394
+ byJev = false,
375
395
  ): EvaluationResult {
376
396
  // Pick a noun for the deny message that fits the event type. Tool events
377
397
  // get the tool name; non-tool events (UserPromptSubmit, SessionStart,
@@ -391,7 +411,10 @@ function formatDeny(
391
411
  } else {
392
412
  displayTool = "operation";
393
413
  }
394
- const blockedMessage = `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`;
414
+ // Jev's reason is its own sentences, ending in "."; no user configured the check.
415
+ const blockedMessage = byJev
416
+ ? `Blocked ${displayTool} by failproofai because: ${reason.replace(/[.\s]+$/, "")}, as flagged by Jev semantic review`
417
+ : `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`;
395
418
 
396
419
  // Cursor's hook protocol expects a flat `{permission, user_message,
397
420
  // agent_message}` shape for any blocking decision, regardless of which
@@ -722,7 +745,7 @@ function formatDeny(
722
745
  hookEventName: eventType,
723
746
  decision: {
724
747
  behavior: "deny",
725
- message: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`,
748
+ message: blockedMessage,
726
749
  },
727
750
  },
728
751
  };
@@ -777,7 +800,7 @@ function formatDeny(
777
800
  const response = {
778
801
  hookSpecificOutput: {
779
802
  hookEventName: eventType,
780
- additionalContext: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`,
803
+ additionalContext: blockedMessage,
781
804
  },
782
805
  };
783
806
  return {
@@ -127,14 +127,15 @@ export function getPoliciesForEvent(
127
127
  return result;
128
128
  }
129
129
 
130
- export function clearPolicies(): void {
130
+ /** `cli`: the agent this pass registers for, which scopes the Jev reviewer set. */
131
+ export function clearPolicies(cli?: string): void {
131
132
  const g = globalThis as GlobalWithRegistry;
132
133
  g[REGISTRY_KEY] = [];
133
134
  setIndexCache(null);
134
135
  // The reviewer set describes the policies that are about to be registered, so
135
136
  // it is rebuilt with them. Dropping it here is also what keeps one read per
136
137
  // evaluation instead of one per policy.
137
- forgetEffectiveReviewerNames();
138
+ forgetEffectiveReviewerNames(cli);
138
139
  }
139
140
 
140
141
  /**
@@ -49,18 +49,26 @@
49
49
  * - **Policies from the user's own files.** A custom hook declares its
50
50
  * authority inside the module, so counting one means importing and running
51
51
  * somebody's JavaScript — from a settings page, on every read. The count of
52
- * configured custom policy PATHS is reported instead, and the summary says
53
- * in words that those policies were not counted.
52
+ * custom policy FILES the hook loads (configured paths and the project and
53
+ * user convention files) is reported instead, the summary says in words that
54
+ * those policies were not counted, and no "never" is diagnosed beside them.
54
55
  * - **A session pause.** It suspends local policy for minutes, and a warning
55
56
  * about a policy set that is coming back shortly would be noise.
56
57
  */
57
58
  import { readActiveCloudManagedPolicies } from "./cloud-managed-policies";
58
- import { configuredCustomPolicyPaths, readMergedHooksConfig } from "./hooks-config";
59
- import { hasInstalledPacks, packSemantic, readInstalledPacks } from "./pack-manifest";
60
- import { resolvePolicyAuthority } from "./policy-authority";
59
+ import { jevPacks } from "./effective-reviewers";
60
+ import { resolve } from "node:path";
61
+ import { discoverPolicyFiles } from "./custom-hooks-loader";
62
+ import { customPoliciesDir } from "./fp-home";
63
+ import { configuredCustomPolicyPaths, findProjectConfigDir, readMergedHooksConfig } from "./hooks-config";
64
+ import { hasInstalledPacks, readInstalledPacks } from "./pack-manifest";
65
+ import { resolvePolicyAuthority, SEMANTIC_REVIEWER_NAMES } from "./policy-authority";
61
66
  import { POLICY_CATALOG } from "./policy-catalog";
62
67
  import { normalizePolicyName } from "./policy-registry";
63
68
  import type { HooksConfig } from "./policy-types";
69
+ // A CLI and dashboard module, never on the hook path, so it may ask the
70
+ // resolver itself (`pack-cli.ts` does the same).
71
+ import { semanticPoliciesFromPacks } from "./semantic/pack-policies";
64
72
 
65
73
  /** Anything that carries an authority declaration: a catalog entry, a pack entry, a cloud record. */
66
74
  export interface AuthorityRecord {
@@ -74,7 +82,10 @@ export interface ReviewableCoverage {
74
82
  enabled: number;
75
83
  /** Of those, the ones Jev may clear a deny or an instruct from. */
76
84
  reviewable: number;
77
- /** Configured custom policy paths, whose policies are not in the counts above. */
85
+ /**
86
+ * Custom policy files the hook loads — configured paths and the project and
87
+ * user convention files — whose policies are not in the counts above.
88
+ */
78
89
  customFiles: number;
79
90
  }
80
91
 
@@ -144,14 +155,21 @@ export function surveyReviewableCoverage(cwd?: string): ReviewableCoverage {
144
155
  * A pack's `enabled` selection is deliberately not applied: it narrows which
145
156
  * of its REGEX policies register, and its semantic set is not selectable.
146
157
  */
147
- const packReviewers = new Set<string>();
158
+ let reviewers: ReadonlySet<string> | undefined;
148
159
  try {
149
160
  packsInstalled = hasInstalledPacks();
150
- for (const pack of readInstalledPacks().packs) {
161
+ const packs = readInstalledPacks().packs;
162
+ for (const pack of packs) {
151
163
  const selected = pack.enabled;
152
164
  records.push(...(selected ? pack.policies.filter((p) => selected.includes(p.name)) : pack.policies));
153
- for (const entry of packSemantic(pack)) packReviewers.add(entry.name);
154
165
  }
166
+ // The names of the questions a request will actually carry: only the packs
167
+ // whose checks take part (an observe pack's do not), minus a reserved or
168
+ // contested name AND minus a check the question budget drops — which the
169
+ // hook path's manifest-only `reviewerNamesFor` cannot see. Anything else and
170
+ // the panel and `jev status` promise a clear that cannot happen.
171
+ const asked = semanticPoliciesFromPacks(jevPacks(packs));
172
+ reviewers = asked.fromPack ? new Set(asked.policies.map((p) => p.name)) : SEMANTIC_REVIEWER_NAMES;
155
173
  } catch {
156
174
  // An unreadable manifest enforces nothing; `readInstalledPacks` already
157
175
  // reports that to the hook log on the path that cares.
@@ -178,14 +196,23 @@ export function surveyReviewableCoverage(cwd?: string): ReviewableCoverage {
178
196
  // Same fail-open as the handler's own read of this file.
179
197
  }
180
198
 
199
+ // Every file `loadAllCustomHooks` would load, by resolved path so a file
200
+ // both named and discovered (or a project root that IS home) counts once.
181
201
  let customFiles = 0;
182
202
  try {
183
- customFiles = configuredCustomPolicyPaths(config).length;
203
+ // Relative paths against the project root, as the loader resolves them.
204
+ const projectRoot = findProjectConfigDir(cwd ?? process.cwd());
205
+ const files = new Set(configuredCustomPolicyPaths(config).map((p) => resolve(projectRoot, p)));
206
+ if (config.customPoliciesEnabled !== false) {
207
+ const projectDir = resolve(projectRoot, ".failproofai", "policies");
208
+ for (const dir of [projectDir, customPoliciesDir()]) for (const f of discoverPolicyFiles(dir)) files.add(f);
209
+ }
210
+ customFiles = files.size;
184
211
  } catch {
185
212
  customFiles = 0;
186
213
  }
187
214
 
188
- return { ...countReviewable(records, packReviewers.size > 0 ? packReviewers : undefined), customFiles };
215
+ return { ...countReviewable(records, reviewers), customFiles };
189
216
  }
190
217
 
191
218
  /**
@@ -219,7 +246,9 @@ export function reviewableSummary(coverage: ReviewableCoverage): string {
219
246
  * did not ask.
220
247
  */
221
248
  export function reviewableProblem(coverage: ReviewableCoverage): string | null {
222
- if (coverage.enabled === 0 || coverage.reviewable > 0) return null;
249
+ // Policies from the user's own files were not counted and may be reviewable,
250
+ // so "never" cannot be said honestly; the summary already says what was skipped.
251
+ if (coverage.enabled === 0 || coverage.reviewable > 0 || coverage.customFiles > 0) return null;
223
252
  return (
224
253
  "Jev can add a deny or an instruction on this machine, but it can never clear one. " +
225
254
  "No enabled policy is marked reviewable — a policy pack published before this release carries no such marks — " +
@@ -47,7 +47,8 @@ export interface RegisteredPolicy {
47
47
  authority?: PolicyAuthority;
48
48
  /**
49
49
  * The semantic policies (`src/hooks/semantic/policies.ts`) that must all be
50
- * asked, and none of which may answer `deny`, before Jev clears this policy.
50
+ * asked, and each clear (see {@link PolicyAuthority}), before Jev clears this
51
+ * policy.
51
52
  */
52
53
  reviewedBy?: string[];
53
54
  }
@@ -58,8 +59,9 @@ export interface RegisteredPolicy {
58
59
  * - `hard`: final. Jev can never clear it.
59
60
  * - `reviewable`: Jev may clear it, but only through the semantic policies
60
61
  * named in `reviewedBy`, and only when every one of them was actually asked
61
- * and none of them answered `deny` — a `none`, `overridden` or `instruct`
62
- * answer clears it (`combine.ts`, "A warning-level answer clears the deny").
62
+ * and each answered `none`, `overridden`, or a deny the human's task
63
+ * softened to a warning (`combine.ts`, "A check that fired without consent
64
+ * keeps the floor").
63
65
  * A named check that was NOT asked always keeps the verdict standing.
64
66
  */
65
67
  export type PolicyAuthority = "hard" | "reviewable";