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
@@ -21,12 +21,12 @@ import {
21
21
  readInstalledPacks,
22
22
  type SemanticManifestEntry,
23
23
  } from "./pack-manifest";
24
- import { parseSemver } from "./semver-precedence";
24
+ import { compareVersions, parseSemver } from "./semver-precedence";
25
25
  // The build step measures a pack's question set against the envelope's own
26
26
  // budget, which means reaching the semantic side. This is a CLI module — loaded
27
27
  // by `failproofai publish`, never by a hook — so the rule that keeps those
28
28
  // modules off an unconfigured machine's hook path does not apply here.
29
- import { MAX_PACK_QUESTION_CHARS, questionChars } from "./semantic/pack-policies";
29
+ import { BUILTIN_QUESTION_CHARS, MAX_PACK_QUESTION_CHARS, questionChars, semanticPoliciesFromPacks } from "./semantic/pack-policies";
30
30
  import {
31
31
  AmbiguousPackId,
32
32
  PACK_CHECKSUMS_ASSET,
@@ -40,10 +40,12 @@ import {
40
40
  removePack,
41
41
  setPackPolicyEnabled,
42
42
  slugifyCategory,
43
+ type PackPreview,
43
44
  } from "./pack-store";
44
45
  import type { PolicyEffect } from "./cloud-managed-policies";
45
46
  import { loadCustomHooks } from "./custom-hooks-loader";
46
47
  import { getSemanticRegistrations } from "./custom-hooks-registry";
48
+ import { isFirstPartyPack, isReservedClaim, jevPacks } from "./effective-reviewers";
47
49
  import { authorityFieldsOf, authorityProblem, resolvePolicyAuthority } from "./policy-authority";
48
50
  import type { PolicyCatalogEntry, SemanticPolicyDeclaration } from "./policy-types";
49
51
  import type { MultiChoice, RenderOpts, TTYIn, TTYOut } from "./tui";
@@ -80,8 +82,20 @@ export interface PackBuildMeta {
80
82
  policies: number;
81
83
  defaultOn: number;
82
84
  commit?: string;
85
+ /** Jev checks and no regex half: `publish` repeats {@link JEV_ONLY_ROLLBACK_REMINDER}. */
86
+ semanticOnly?: boolean;
83
87
  }
84
88
 
89
+ /**
90
+ * A build from before `semanticOnly` refuses such a pack as blanket-deny, so its
91
+ * author has to tell users. Shared by build's lines and publish's success
92
+ * message, which is the only one an author actually reads.
93
+ */
94
+ const JEV_ONLY_ROLLBACK_REMINDER = [
95
+ " Jev checks alone: tell users to remove it before rolling a machine back to an older failproofai,",
96
+ " which can deny every tool call over a pack it will not load (see the publish-a-pack docs).",
97
+ ];
98
+
85
99
  const ok = (lines: string[], meta?: PackBuildMeta): PackCliResult => ({
86
100
  lines,
87
101
  exitCode: 0,
@@ -89,12 +103,30 @@ const ok = (lines: string[], meta?: PackBuildMeta): PackCliResult => ({
89
103
  });
90
104
  const fail = (lines: string[]): PackCliResult => ({ lines, exitCode: 1 });
91
105
 
92
- function parseList(rest: string[], flag: string): string[] | undefined {
93
- const idx = rest.findIndex((a) => a === flag || a.startsWith(`${flag}=`));
94
- if (idx === -1) return undefined;
95
- const raw = rest[idx].includes("=") ? rest[idx].split("=").slice(1).join("=") : rest[idx + 1];
96
- if (!raw || raw.startsWith("--")) return [];
97
- return raw.split(",").map((s) => s.trim()).filter(Boolean);
106
+ /**
107
+ * The first failproofai that runs a pack's Jev checks as published: 1.0.7
108
+ * ignores `semantic` (and `minCliVersion`), and 1.0.7-beta.x replaces the
109
+ * built-in checks with a pack's. A pack with checks is held to at least this.
110
+ */
111
+ const JEV_PACK_MIN_CLI = "1.0.8-beta.0";
112
+
113
+ /**
114
+ * Every occurrence of any of `flags`, comma-split and de-duplicated: a repeated
115
+ * flag adds to the list rather than being silently dropped. `[]` when any
116
+ * occurrence has no value (callers refuse that), undefined when none appears.
117
+ */
118
+ function parseList(rest: string[], ...flags: string[]): string[] | undefined {
119
+ let found = false;
120
+ const out: string[] = [];
121
+ for (let i = 0; i < rest.length; i += 1) {
122
+ const flag = flags.find((f) => rest[i] === f || rest[i].startsWith(`${f}=`));
123
+ if (!flag) continue;
124
+ found = true;
125
+ const raw = rest[i] === flag ? rest[i + 1] : rest[i].slice(flag.length + 1);
126
+ if (!raw || raw.startsWith("--")) return [];
127
+ out.push(...raw.split(",").map((s) => s.trim()).filter(Boolean));
128
+ }
129
+ return found ? [...new Set(out)] : undefined;
98
130
  }
99
131
 
100
132
  /**
@@ -184,17 +216,25 @@ export function packAddSource(rest: string[]): string | undefined {
184
216
  * a third would be the one that drifts.
185
217
  */
186
218
  function parseCliList(rest: string[]): { clis?: string[] } | { error: string[] } {
187
- const idx = rest.findIndex((a) => a === "--cli" || a.startsWith("--cli="));
188
- if (idx === -1) return {};
219
+ // Every `--cli`, not just the first: a repeat adds agents. One that names
220
+ // none still refuses the whole selection, below.
189
221
  const values: string[] = [];
190
- if (rest[idx].startsWith("--cli=")) {
191
- values.push(...rest[idx].slice("--cli=".length).split(","));
192
- } else {
193
- for (let j = idx + 1; j < rest.length && looksLikeCliName(rest[j]); j += 1) {
194
- values.push(...rest[j].split(","));
195
- }
222
+ let found = false;
223
+ let emptyOccurrence = false;
224
+ for (let i = 0; i < rest.length; i += 1) {
225
+ const before = values.length;
226
+ if (rest[i].startsWith("--cli=")) {
227
+ values.push(...rest[i].slice("--cli=".length).split(",").filter((v) => v.trim()));
228
+ } else if (rest[i] === "--cli") {
229
+ for (let j = i + 1; j < rest.length && looksLikeCliName(rest[j]); j += 1) {
230
+ values.push(...rest[j].split(",").filter((v) => v.trim()));
231
+ }
232
+ } else continue;
233
+ found = true;
234
+ if (values.length === before) emptyOccurrence = true;
196
235
  }
197
- const names = values.map((v) => v.trim()).filter(Boolean);
236
+ if (!found) return {};
237
+ const names = emptyOccurrence ? [] : [...new Set(values.map((v) => v.trim()))];
198
238
  // `--cli` with nothing after it is a typo, not a scope.
199
239
  //
200
240
  // It used to become `clis: []`, which `installed.json` stores verbatim and
@@ -254,7 +294,7 @@ function selectionFrom(rest: string[]): {
254
294
  } {
255
295
  // `--policy` reads right for one ("give me this policy"), `--only` for a set.
256
296
  // They are the same switch; taking both means neither is the wrong guess.
257
- const only = parseList(rest, "--policy") ?? parseList(rest, "--only");
297
+ const only = parseList(rest, "--policy", "--only");
258
298
  const categories = parseList(rest, "--category");
259
299
  const parsedClis = parseCliList(rest);
260
300
  if ("error" in parsedClis) return { error: parsedClis.error };
@@ -312,6 +352,10 @@ async function build(rest: string[]): Promise<PackCliResult> {
312
352
  const effect = flag("effect") ?? "enforce";
313
353
  const minCliVersion = flag("min-cli-version");
314
354
  const outDir = resolve(flag("out") ?? "dist-pack");
355
+ // Where it will be installed from, which is what a machine judges a pack by
356
+ // (`isFirstPartyPack` reads `source`, never the id). `publish` passes its
357
+ // repository; `pack build` has only the id to go on.
358
+ const repo = flag("repo");
315
359
 
316
360
  if (!entry || !id || !version) {
317
361
  return fail([
@@ -400,6 +444,7 @@ async function build(rest: string[]): Promise<PackCliResult> {
400
444
  // same deal `parsePackPolicy` gets above, so a semantic policy that could
401
445
  // never load fails here, where the author can fix it.
402
446
  const semantic: SemanticManifestEntry[] = [];
447
+ const packSource = { source: `github:${repo ?? identity.id}` };
403
448
  for (const [index, declaration] of semanticDeclarations.entries()) {
404
449
  try {
405
450
  const parsed = parsePackSemanticPolicy(identity.id, declaration, index);
@@ -409,21 +454,49 @@ async function build(rest: string[]): Promise<PackCliResult> {
409
454
  if (semantic.some((s) => s.name === parsed.name)) {
410
455
  throw new Error(`two semantic policies are called ${JSON.stringify(parsed.name)}`);
411
456
  }
457
+ // The loader's own rule: a built-in check name from anyone but FailproofAI
458
+ // is void on every machine, never asked and never a reviewer.
459
+ if (isReservedClaim(packSource, parsed.name)) {
460
+ throw new Error(
461
+ `${JSON.stringify(parsed.name)} is a name reserved for FailproofAI's own Jev checks, so a pack from ` +
462
+ `${repo ?? identity.id} would never have it asked. Pick a name of your own.`,
463
+ );
464
+ }
412
465
  semantic.push(parsed);
413
466
  } catch (err) {
414
467
  return fail([err instanceof Error ? err.message : String(err)]);
415
468
  }
416
469
  }
417
470
 
471
+ // A pack with Jev checks needs a build that runs them, so its minimum is at
472
+ // least the first one — written when the author gave none, refused when lower.
473
+ if (semantic.length > 0 && minCliVersion !== undefined && (compareVersions(minCliVersion, JEV_PACK_MIN_CLI) ?? 0) < 0) {
474
+ return fail([
475
+ `--min-cli-version ${minCliVersion} is older than the first failproofai that runs a pack's Jev checks (${JEV_PACK_MIN_CLI}):`,
476
+ "1.0.7 ignores them, and 1.0.7-beta.x replaces the built-in checks with them.",
477
+ `Pass --min-cli-version ${JEV_PACK_MIN_CLI} or newer, or leave it out and ${JEV_PACK_MIN_CLI} is written.`,
478
+ ]);
479
+ }
480
+ const requiredCli = semantic.length > 0 ? (minCliVersion ?? JEV_PACK_MIN_CLI) : minCliVersion;
481
+
418
482
  // The question budget, checked against what one Jev request can carry. A pack
419
483
  // over it installs and then has its overflow policies dropped one by one on
420
484
  // the user's machine, in manifest order — a pack that enforces less than it
421
485
  // says, which is the failure this whole lane is built to avoid.
486
+ //
487
+ // A pack from outside FailproofAI is ADDED to the built-in checks, which every
488
+ // machine spends the budget on first (`semanticPoliciesFromPacks`), so it gets
489
+ // only what they leave.
422
490
  const questionCost = semantic.reduce((total, entry) => total + questionChars(entry), 0);
423
- if (questionCost > MAX_PACK_QUESTION_CHARS) {
491
+ const firstParty = isFirstPartyPack(packSource);
492
+ const questionBudget = MAX_PACK_QUESTION_CHARS - (firstParty ? 0 : BUILTIN_QUESTION_CHARS);
493
+ if (questionCost > questionBudget) {
424
494
  return fail([
425
495
  `This pack's ${semantic.length} semantic policies compile to ${questionCost} characters of questions, ` +
426
- `over the ${MAX_PACK_QUESTION_CHARS} one Jev request has room for.`,
496
+ (firstParty
497
+ ? `over the ${MAX_PACK_QUESTION_CHARS} one Jev request has room for.`
498
+ : `over the ${questionBudget} a machine leaves a pack from outside FailproofAI: one Jev request has room ` +
499
+ `for ${MAX_PACK_QUESTION_CHARS}, and the 16 built-in checks every machine asks take ${BUILTIN_QUESTION_CHARS} of it first.`),
427
500
  "Shorten the probe instructions and criteria, or ship fewer policies per pack.",
428
501
  ]);
429
502
  }
@@ -452,6 +525,9 @@ async function build(rest: string[]): Promise<PackCliResult> {
452
525
  ? "One policy declares an authority this build cannot publish:"
453
526
  : `${authorityProblems.length} policies declare an authority this build cannot publish:`,
454
527
  ...authorityProblems,
528
+ ...(reviewers
529
+ ? ["This pack declares Jev checks of its own, so reviewedBy may name only those."]
530
+ : []),
455
531
  'Fix the declaration, or leave authority out and the policy is "hard". See https://docs.befailproof.ai/policies/authority',
456
532
  ]);
457
533
  }
@@ -464,7 +540,9 @@ async function build(rest: string[]): Promise<PackCliResult> {
464
540
  // FALSE: a pack's declared defaults are what `pack add` switches on with no
465
541
  // flags, and switching on a stranger's every policy unattended is the
466
542
  // installer opinion this lane already refused once.
467
- const extra = hook as unknown as { category?: unknown; defaultEnabled?: unknown; params?: unknown };
543
+ const extra = hook as unknown as {
544
+ category?: unknown; defaultEnabled?: unknown; params?: unknown; alwaysOn?: unknown;
545
+ };
468
546
  // `params` is read off the registration for the same reason, and leaving it
469
547
  // out was not cosmetic: `registerPolicy` takes a pack policy's schema from the
470
548
  // MANIFEST by name, so a pack published without it evaluates every one of its
@@ -489,6 +567,9 @@ async function build(rest: string[]): Promise<PackCliResult> {
489
567
  defaultEnabled: extra.defaultEnabled === true,
490
568
  match: hook.match ?? {},
491
569
  ...(extra.params !== undefined ? { params: extra.params } : {}),
570
+ // Carried so parsePackPolicy refuses it as the loader would; left off, an
571
+ // alwaysOn registration published as an ordinary (even reviewable) policy.
572
+ ...("alwaysOn" in extra ? { alwaysOn: extra.alwaysOn } : {}),
492
573
  // Whether Jev may clear this policy's verdict. A pack's MANIFEST is what
493
574
  // a machine reads it from, so a declaration left on the registration
494
575
  // alone would be published as nothing — silently hard. Checked above, so
@@ -516,7 +597,7 @@ async function build(rest: string[]): Promise<PackCliResult> {
516
597
  {
517
598
  id: identity.id,
518
599
  version: identity.version,
519
- ...(minCliVersion ? { minCliVersion } : {}),
600
+ ...(requiredCli ? { minCliVersion: requiredCli } : {}),
520
601
  effect: identity.effect,
521
602
  ...(identity.commit ? { commit: identity.commit } : {}),
522
603
  policies,
@@ -538,25 +619,35 @@ async function build(rest: string[]): Promise<PackCliResult> {
538
619
  );
539
620
 
540
621
  const on = policies.filter((p) => (p as { defaultEnabled?: boolean }).defaultEnabled).length;
622
+ const semanticOnly = policies.length === 0 && semantic.length > 0;
541
623
  return ok([
542
624
  `Built ${identity.id}@${identity.version} — ${policies.length} policies, ${on} on by default.`,
543
- // Said separately, and said at all, because this half replaces the compiled-in
544
- // semantic set on every machine that installs the pack. An author who did not
545
- // mean to ship it should find that out here.
625
+ // Said separately, and said at all, because this half changes what Jev asks
626
+ // on every machine that installs the pack. An author who did not mean to
627
+ // ship it should find that out here.
546
628
  ...(semantic.length > 0
547
629
  ? [
548
630
  ` ${semantic.length} semantic ${semantic.length === 1 ? "policy" : "policies"} for Jev ` +
549
- `(${questionCost} characters of questions), replacing this build's own set where it installs.`,
631
+ `(${questionCost} characters of questions), ` +
632
+ (identity.effect === "observe"
633
+ ? "not asked where it installs: Jev asks only the checks of packs that enforce."
634
+ : `${firstParty ? "replacing" : "added to"} the built-in checks where it installs.`),
550
635
  ]
551
636
  : []),
552
- ...(minCliVersion ? [` Requires failproofai ${minCliVersion} or newer.`] : []),
637
+ ...(requiredCli ? [` Requires failproofai ${requiredCli} or newer.`] : []),
638
+ ...(semanticOnly ? JEV_ONLY_ROLLBACK_REMINDER : []),
553
639
  ` ${outDir}/${PACK_MANIFEST_ASSET}`,
554
640
  ` ${outDir}/${PACK_ENTRY_ASSET}`,
555
641
  ` ${outDir}/${PACK_CHECKSUMS_ASSET}`,
556
642
  "",
557
643
  `Publish: attach all three to a GitHub release tagged ${identity.version}, then anyone runs:`,
558
644
  ` failproofai policies add <owner>/<repo>`,
559
- ], { policies: policies.length, defaultOn: on, ...(identity.commit ? { commit: identity.commit } : {}) });
645
+ ], {
646
+ policies: policies.length,
647
+ defaultOn: on,
648
+ ...(identity.commit ? { commit: identity.commit } : {}),
649
+ ...(semanticOnly ? { semanticOnly } : {}),
650
+ });
560
651
  }
561
652
 
562
653
  /**
@@ -2304,6 +2395,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
2304
2395
  const built = await build([
2305
2396
  entryToBuild,
2306
2397
  "--id", id,
2398
+ ...(repo ? ["--repo", repo] : []),
2307
2399
  "--version", version,
2308
2400
  // NOT when the tree is dirty, even though `--version` let the publish
2309
2401
  // through. `commit` claims these bytes came from that commit, and on a
@@ -2328,7 +2420,9 @@ async function publish(rest: string[]): Promise<PackCliResult> {
2328
2420
  ...gitLines,
2329
2421
  ...(gitLines.length ? [""] : []),
2330
2422
  ...bundleNote,
2331
- ...built.lines.slice(0, 4),
2423
+ // Everything above build's blank line: a fixed count cut the rollback
2424
+ // reminder mid-sentence and dropped the asset paths.
2425
+ ...built.lines.slice(0, built.lines.indexOf("")),
2332
2426
  "",
2333
2427
  ...(repo
2334
2428
  ? ["Dry run — nothing was published.", `Drop --dry-run to release it on ${repo}.`]
@@ -2444,6 +2538,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
2444
2538
  `Published ${id}@${version} to ${repo} at tag ${tag}.` +
2445
2539
  (versionFromSha ? " That names the commit it was built from." : ""),
2446
2540
  ` ${assets.length} assets attached`,
2541
+ ...(built.meta?.semanticOnly ? JEV_ONLY_ROLLBACK_REMINDER : []),
2447
2542
  "",
2448
2543
  // The install lines are the whole point of the success message, and on a
2449
2544
  // private repository they are a lie — `policies add` 404s there. Print the
@@ -2555,6 +2650,36 @@ function semanticPhrase(count: number): string {
2555
2650
  return `${count} Jev ${count === 1 ? "check" : "checks"}`;
2556
2651
  }
2557
2652
 
2653
+ /**
2654
+ * How a pack's Jev checks sit beside this build's, for add, show and the
2655
+ * picker alike: a FailproofAI pack's replace them, anyone else's are added
2656
+ * (`replacesBuiltinChecks`), and only where `jevPacks` lets the pack take part —
2657
+ * never for an observe pack, only for its agents when scoped. One phrase, so
2658
+ * the three cannot drift apart again.
2659
+ */
2660
+ function besideBuiltinChecks(pack: { source?: string; effect?: PolicyEffect; clis?: string[] | null }): string {
2661
+ if (jevPacks([{ effect: pack.effect ?? "enforce", clis: null }]).length === 0) {
2662
+ return "not asked: this pack only observes, and Jev asks only the checks of packs that enforce";
2663
+ }
2664
+ const how = isFirstPartyPack(pack) ? "replacing this build's own set" : "added to this build's own checks";
2665
+ return pack.clis && pack.clis.length > 0 ? `${how}, for ${pack.clis.join(", ")} only` : how;
2666
+ }
2667
+
2668
+ /**
2669
+ * Which of a pack's checks the resolver would leave out beside what is
2670
+ * installed (the shared budget, a reserved or contested name). `preview` is a
2671
+ * pack not installed yet, judged as if it were. A diagnostic: never throws.
2672
+ */
2673
+ function jevCheckWarnings(id: string, preview?: PackPreview): string[] {
2674
+ try {
2675
+ const installed = readInstalledPacks().packs.filter((p) => p.id !== preview?.id);
2676
+ const packs = preview ? [...installed, { ...preview, clis: null }] : installed;
2677
+ return semanticPoliciesFromPacks(jevPacks(packs)).errors.filter((e) => e.includes(id));
2678
+ } catch {
2679
+ return [];
2680
+ }
2681
+ }
2682
+
2558
2683
  async function pickFromSource(
2559
2684
  source: string,
2560
2685
  io: { stdin: TTYIn; stdout: TTYOut },
@@ -2586,12 +2711,12 @@ async function pickFromSource(
2586
2711
  }
2587
2712
  const on = preview.policies.filter((p) => p.defaultEnabled).length;
2588
2713
  // Said BEFORE the list, because the list cannot say it: a pack's Jev checks are
2589
- // not rows here, they arrive whole, and they replace the ones this build ships.
2590
- // Somebody ticking boxes should know that is part of what they are agreeing to.
2714
+ // not rows here, and they arrive whole. Somebody ticking boxes should know
2715
+ // that is part of what they are agreeing to.
2591
2716
  if (preview.semantic.length > 0) {
2592
2717
  io.stdout.write(
2593
- `\n This pack also brings ${semanticPhrase(preview.semantic.length)}, which are not selectable —\n` +
2594
- ` they replace the ones this build ships with. See: failproofai policies show ${source}\n\n`,
2718
+ `\n This pack also brings ${semanticPhrase(preview.semantic.length)} (not selectable), ` +
2719
+ `${besideBuiltinChecks(preview)}.\n See: failproofai policies show ${source}\n\n`,
2595
2720
  );
2596
2721
  }
2597
2722
  const picked = await multiSelect<string>({
@@ -2719,16 +2844,25 @@ async function add(rest: string[]): Promise<PackCliResult> {
2719
2844
  : ` enabled (${result.enabled.length}/${result.available.length}, ${why}): ${summarise(result.enabled)}`,
2720
2845
  );
2721
2846
  // Counted separately, and without an on/off ratio, because there is none:
2722
- // a pack's Jev checks arrive whole and REPLACE the ones this build ships.
2847
+ // a pack's Jev checks arrive whole, and a FailproofAI pack's REPLACE the ones
2848
+ // this build ships (anyone else's are added to them).
2723
2849
  // Silence here was the shape of the original bug — the manifest's semantic
2724
2850
  // half was fetched, verified and then never written to `installed.json`, so
2725
2851
  // the half somebody installed did nothing and nothing said so.
2726
2852
  if (result.semantic > 0) {
2727
2853
  lines.push(
2728
- ` ${semanticPhrase(result.semantic)} for Jev, replacing this build's own set. ` +
2854
+ ` ${semanticPhrase(result.semantic)}, ${besideBuiltinChecks(result)}. ` +
2729
2855
  "They apply only where you configured Jev (`failproofai jev status`).",
2730
2856
  );
2731
2857
  }
2858
+ // Nothing after this screen says it: the record keeps no unreadable minimum.
2859
+ if (result.minCliVersionNote) lines.push(` ${result.minCliVersionNote}`);
2860
+ // Which of its checks the resolver will leave out beside what is already
2861
+ // installed (the shared question budget, a reserved or contested name).
2862
+ // Otherwise said only in the hook log, on the first call that asks Jev.
2863
+ if (result.semantic > 0) {
2864
+ for (const e of jevCheckWarnings(result.id)) lines.push(` ▲ ${e}`);
2865
+ }
2732
2866
 
2733
2867
  if (skipped.length > 0) {
2734
2868
  lines.push(` not enabled (${skipped.length}): ${summarise(skipped)}`);
@@ -3077,6 +3211,8 @@ export function jevChecksSection(
3077
3211
  pack: {
3078
3212
  policies: ReadonlyArray<PolicyCatalogEntry>;
3079
3213
  semantic: ReadonlyArray<SemanticManifestEntry>;
3214
+ /** Where it is (or would be) installed from, which decides replace or add. */
3215
+ source?: string;
3080
3216
  },
3081
3217
  opts?: RenderOpts,
3082
3218
  ): string[] | null {
@@ -3110,8 +3246,8 @@ export function jevChecksSection(
3110
3246
  ...table({ head: ["", "", ""], rows, protect: [0, 1] }, opts),
3111
3247
  "",
3112
3248
  ...note(
3113
- "Nothing toggles them: `--policy` cannot name one, `failproofai policies` never lists them, " +
3114
- "and a pack's checks replace the ones this build ships with.",
3249
+ "Nothing toggles them: `--policy` cannot name one and `failproofai policies` never lists them. " +
3250
+ `They arrive whole, ${besideBuiltinChecks(pack)}.`,
3115
3251
  opts,
3116
3252
  ),
3117
3253
  ...note(
@@ -3171,12 +3307,14 @@ async function listRemote(source: string): Promise<PackCliResult> {
3171
3307
  opts,
3172
3308
  ),
3173
3309
  preview.minCliVersion ? note(`Requires failproofai ${preview.minCliVersion} or newer.`, opts) : null,
3310
+ preview.minCliVersionNote ? note(preview.minCliVersionNote, opts) : null,
3174
3311
  preview.resolvedFromLatest ? note(`Newest release: ${preview.source}`, opts) : null,
3175
3312
  table({ head: ["", "", ""], rows }, opts),
3176
3313
  // The half the table above cannot hold: its own section, under the
3177
3314
  // policies rather than above them, because a check is read against the
3178
3315
  // policies it can clear and those are the rows just passed.
3179
3316
  jevChecksSection(preview, opts),
3317
+ preview.semantic.length > 0 ? jevCheckWarnings(preview.id, preview).flatMap((e) => warning([e], opts)) : null,
3180
3318
  nextStep(`failproofai policies add ${source}`, "Install the defaults with:", opts),
3181
3319
  note("Or take part of it: --policy <a,b>, --category <x,y>, --all", opts),
3182
3320
  ),
@@ -151,6 +151,9 @@ export function missingGuards(input: {
151
151
  // loaded, would have allowed.
152
152
  if (err.effect === "observe") continue;
153
153
  if (outOfScope(err.clis, input.cli)) continue;
154
+ // Jev checks only: nothing in the regex tier is missing. The refusal is
155
+ // still reported (hook log, `failproofai policies`); it just denies nothing.
156
+ if (err.semanticOnly) continue;
154
157
  const declared = err.declared ?? [];
155
158
  out.push({
156
159
  packId: err.id ?? "(unnamed pack)",
@@ -253,6 +253,13 @@ export interface PackError {
253
253
  * deny on their behalf locks an agent out over enforcement it never had.
254
254
  */
255
255
  clis?: string[] | null;
256
+ /**
257
+ * The record's regex `policies` list was readable and empty: a Jev-checks-only
258
+ * pack. It guards nothing in the regex tier, so its refusal has nothing to
259
+ * fail closed on. Read from the RAW list, never from `declared`: a list whose
260
+ * entries are all malformed also filters to nothing, and that must widen.
261
+ */
262
+ semanticOnly?: true;
256
263
  }
257
264
 
258
265
  export interface PackReadResult {
@@ -532,7 +539,9 @@ export function checkPackMinCliVersion(
532
539
  kind: "too-old",
533
540
  reason:
534
541
  `pack ${packId} needs failproofai ${String(declared)} or newer and this is ${current} — ` +
535
- "run `npm i -g failproofai && failproofai update`",
542
+ // As a range: the bare name installs `latest`, which can sit below a
543
+ // prerelease minimum. `declared` compared above, so it is plain semver.
544
+ `run \`npm i -g "failproofai@>=${String(declared)}" && failproofai update\``,
536
545
  };
537
546
  }
538
547
  return { kind: "satisfied", declared: declared as string };
@@ -883,6 +892,7 @@ export function readInstalledPacks(): PackReadResult {
883
892
  // Best effort: an entry too malformed to list policies yields nothing
884
893
  // here, and a deny built from it is unavoidably blanket.
885
894
  ...(Array.isArray(rec?.policies) ? { declared: safeDeclared(rec.policies) } : {}),
895
+ ...(Array.isArray(rec?.policies) && rec.policies.length === 0 ? { semanticOnly: true as const } : {}),
886
896
  // A list of names, or nothing. Anything else is unreadable scope, and
887
897
  // unreadable scope has to mean "every agent" — the same reasoning that
888
898
  // widens an unreadable `match`: a narrowing nobody can parse says
@@ -45,6 +45,7 @@ import {
45
45
  parsePackSemanticPolicy,
46
46
  readInstalledPacks,
47
47
  } from "./pack-manifest";
48
+ import { isFirstPartyPack } from "./effective-reviewers";
48
49
  import type { ResolvedPack, SemanticManifestEntry } from "./pack-manifest";
49
50
  import type { InstalledPackRecord } from "./pack-manifest";
50
51
  import type { PolicyCatalogEntry } from "./policy-types";
@@ -102,6 +103,11 @@ export interface AddPackResult {
102
103
  * reporting "the pack is installed and enforcing nothing".
103
104
  */
104
105
  semantic: number;
106
+ /** Why the pack's minCliVersion was ignored, when this build could not compare it. */
107
+ minCliVersionNote?: string;
108
+ /** As recorded, so the CLI can say where its Jev checks are asked (`jevPacks`). */
109
+ effect?: PolicyEffect;
110
+ clis?: string[];
105
111
  artifact: string;
106
112
  }
107
113
 
@@ -474,6 +480,8 @@ interface FetchedPack {
474
480
  semantic: SemanticManifestEntry[];
475
481
  /** The minimum CLI it claims, once this one has been checked against it. */
476
482
  minCliVersion?: string;
483
+ /** Why a minimum this build cannot compare was ignored, to be said out loud. */
484
+ minCliVersionNote?: string;
477
485
  effect?: PolicyEffect;
478
486
  /** The git commit the publisher built this from, when they had one. */
479
487
  commit?: string;
@@ -530,15 +538,22 @@ function parseManifestSemantic(packId: string, value: unknown): SemanticManifest
530
538
  * until somebody works out why. Catching it at install turns the same fact into
531
539
  * one line at the moment of choosing.
532
540
  *
533
- * A minimum this build cannot compare is IGNORED, exactly as the loader ignores
541
+ * A minimum this build cannot compare is ignored, exactly as the loader ignores
534
542
  * it: a publisher's typo in a version string must not be able to stop anyone
535
- * installing a pack. `failproofai publish` refuses to write one, so this is
536
- * only reachable from a pack built by some other tool.
543
+ * installing a pack. But the reason comes back as a note for the preview and
544
+ * the install to print, since the record keeps no unreadable value and the
545
+ * loader's own warning never reaches a CLI screen. `failproofai publish`
546
+ * refuses to write one, so this is only reachable from a pack built by some
547
+ * other tool.
537
548
  */
538
- function assertMinCliVersion(packId: string, declared: unknown): string | undefined {
549
+ function assertMinCliVersion(
550
+ packId: string,
551
+ declared: unknown,
552
+ ): { minCliVersion?: string; minCliVersionNote?: string } {
539
553
  const verdict = checkPackMinCliVersion(packId, declared);
540
554
  if (verdict.kind === "too-old") throw new Error(verdict.reason);
541
- return verdict.kind === "satisfied" ? verdict.declared : undefined;
555
+ if (verdict.kind === "unreadable") return { minCliVersionNote: verdict.reason };
556
+ return verdict.declared ? { minCliVersion: verdict.declared } : {};
542
557
  }
543
558
 
544
559
  /** Fetch and fully validate a pack, without writing anything. */
@@ -636,6 +651,8 @@ export interface PackPreview {
636
651
  semantic: SemanticManifestEntry[];
637
652
  /** The minimum CLI this pack declares, when it declares one this build can read. */
638
653
  minCliVersion?: string;
654
+ /** Why a declared minimum this build cannot read was ignored. */
655
+ minCliVersionNote?: string;
639
656
  /** The exact source the preview was read from, tag resolved and pinned. */
640
657
  source: string;
641
658
  /** True when the tag was resolved rather than typed. */
@@ -685,7 +702,7 @@ export async function fetchPackPreview(source: string): Promise<PackPreview> {
685
702
  semantic?: unknown; minCliVersion?: unknown;
686
703
  };
687
704
  const identity = parsePackIdentity(value);
688
- const minCliVersion = assertMinCliVersion(identity.id, value.minCliVersion);
705
+ const minCli = assertMinCliVersion(identity.id, value.minCliVersion);
689
706
  const semantic = parseManifestSemantic(identity.id, value.semantic);
690
707
  const policies = parseManifestPolicies(identity.id, value.policies);
691
708
  assertDeclaresSomething(policies, semantic);
@@ -696,7 +713,7 @@ export async function fetchPackPreview(source: string): Promise<PackPreview> {
696
713
  ...(identity.commit ? { commit: identity.commit } : {}),
697
714
  policies,
698
715
  semantic,
699
- ...(minCliVersion ? { minCliVersion } : {}),
716
+ ...minCli,
700
717
  source: formatPackSpec(spec),
701
718
  resolvedFromLatest,
702
719
  };
@@ -710,7 +727,14 @@ export async function fetchPackPreview(source: string): Promise<PackPreview> {
710
727
  function parseManifestPolicies(packId: string, value: unknown): PolicyCatalogEntry[] {
711
728
  if (value === undefined) return [];
712
729
  if (!Array.isArray(value)) throw new Error(`pack ${packId} policies is not an array`);
713
- return value.map((policy, i) => parsePackPolicy(packId, policy, i));
730
+ const policies = value.map((policy, i) => parsePackPolicy(packId, policy, i));
731
+ // The loader's rule: it refuses the whole record over a repeated name.
732
+ const names = new Set<string>();
733
+ for (const p of policies) {
734
+ if (names.has(p.name)) throw new Error(`pack ${packId} declares ${p.name} twice`);
735
+ names.add(p.name);
736
+ }
737
+ return policies;
714
738
  }
715
739
 
716
740
  /**
@@ -776,7 +800,7 @@ async function fetchPack(spec: PinnedPackSpec): Promise<FetchedPack> {
776
800
  // Before the shape checks below, because being too old for the pack is not a
777
801
  // complaint about the pack: the message to give is "update the CLI", and a
778
802
  // manifest field this build cannot read yet must not be reported as malformed.
779
- const minCliVersion = assertMinCliVersion(identity.id, raw.minCliVersion);
803
+ const minCli = assertMinCliVersion(identity.id, raw.minCliVersion);
780
804
  // Validated with the SAME rules the loader applies, so a pack that could never
781
805
  // load is refused here — while nothing has been written — rather than
782
806
  // installing cleanly and failing silently on the next tool call.
@@ -789,7 +813,7 @@ async function fetchPack(spec: PinnedPackSpec): Promise<FetchedPack> {
789
813
  version: identity.version,
790
814
  policies,
791
815
  semantic,
792
- ...(minCliVersion ? { minCliVersion } : {}),
816
+ ...minCli,
793
817
  ...(raw.effect !== undefined ? { effect: identity.effect } : {}),
794
818
  ...(identity.commit ? { commit: identity.commit } : {}),
795
819
  artifact,
@@ -995,6 +1019,16 @@ export async function addPack(
995
1019
  : `Name the tag whose manifest says ${fetched.version}, or fix the pack: ${fix}.`),
996
1020
  );
997
1021
  }
1022
+ // The id is self-declared, so without this any release could install as a
1023
+ // FailproofAI pack: labelled FailproofAI's own wherever verdicts are shown,
1024
+ // and squatting the id the real pack then cannot take. Owner only — an id may
1025
+ // otherwise differ from its repository (`publish --id`).
1026
+ if (/^failproofai\//i.test(fetched.id) && !isFirstPartyPack({ source: formatPackSpec(spec) })) {
1027
+ throw new Error(
1028
+ `${fetched.id} was not installed: the FailproofAI/ namespace is reserved for releases from ` +
1029
+ `github.com/FailproofAI, and this one came from ${spec.owner}/${spec.repo}.`,
1030
+ );
1031
+ }
998
1032
  const available = fetched.policies.map((p) => p.name);
999
1033
 
1000
1034
  const prior = priorRecordFor(fetched.id, fetched.artifactDigest);
@@ -1124,6 +1158,9 @@ export async function addPack(
1124
1158
  selection: reason,
1125
1159
  categories: [...new Set(fetched.policies.map((p) => slugifyCategory(p.category)))],
1126
1160
  semantic: fetched.semantic.length,
1161
+ ...(fetched.minCliVersionNote ? { minCliVersionNote: fetched.minCliVersionNote } : {}),
1162
+ ...(fetched.effect ? { effect: fetched.effect } : {}),
1163
+ ...(record.clis ? { clis: record.clis } : {}),
1127
1164
  artifact: artifactAbs,
1128
1165
  };
1129
1166
  }