failproofai 1.0.8-beta.0 → 1.0.9-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 (163) 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.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +15 -15
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js +3 -3
  42. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  55. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +20 -20
  59. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__14o3ek1._.js → [root-of-the-server]__0tmz862._.js} +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
  74. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/{_0o4xkpl._.js → _0n0lja6._.js} +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/{_1-7sqrb._.js → _1p3iz2d._.js} +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +3 -3
  82. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +1 -1
  90. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  91. package/.next/standalone/.next/server/pages/404.html +1 -1
  92. package/.next/standalone/.next/server/pages/500.html +1 -1
  93. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  94. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  95. package/.next/standalone/.next/static/chunks/{2z7i-yg59w4pn.js → 0247hc9ath5t-.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{0ahfwmbkpgfw4.js → 03001k9jy-kst.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{2vo7qbdbvnc0o.js → 06k7gsfnb7jkh.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/0qejem4-zt-hy.js +1 -0
  99. package/.next/standalone/.next/static/chunks/{1v_tp3hm8wyhe.js → 0snylg3y1i47a.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/1h0if8ky362as.js +1 -0
  101. package/.next/standalone/.next/static/chunks/{3bgot5v6f6s3c.js → 2qsrrrl2maqsb.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{0bhidk90e-07f.css → 3_e_wg5_oay87.css} +1 -1
  103. package/.next/standalone/.next/static/chunks/3ns-egfh8i8i5.js +6 -0
  104. package/.next/standalone/.next/static/chunks/3rq7fjj9we0v3.js +1 -0
  105. package/.next/standalone/.next/static/chunks/42m0f2hh8vfb1.js +1 -0
  106. package/.next/standalone/app/actions/get-jev-config.ts +2 -2
  107. package/.next/standalone/app/actions/update-jev-config.ts +12 -9
  108. package/.next/standalone/app/components/jev-notices.tsx +10 -10
  109. package/.next/standalone/app/settings/jev-panel.tsx +11 -11
  110. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +2 -0
  111. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +26 -0
  112. package/.next/standalone/fp-cloud-cli/tests/test_output.py +27 -0
  113. package/.next/standalone/hermes-plugin/README.md +18 -8
  114. package/.next/standalone/package.json +10 -10
  115. package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
  116. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
  117. package/.next/standalone/server.js +1 -1
  118. package/bin/failproofai.mjs +33 -7
  119. package/dist/cli.mjs +664 -553
  120. package/dist/worker.mjs +164 -407
  121. package/hermes-plugin/README.md +18 -8
  122. package/package.json +10 -10
  123. package/scripts/build-policy-pack.mjs +6 -9
  124. package/src/hooks/cloud-connection.ts +12 -6
  125. package/src/hooks/custom-hooks-loader.ts +1 -1
  126. package/src/hooks/effective-reviewers.ts +50 -43
  127. package/src/hooks/handler.ts +25 -16
  128. package/src/hooks/hermes-update.ts +74 -0
  129. package/src/hooks/hook-activity-store.ts +3 -3
  130. package/src/hooks/integrations.ts +349 -55
  131. package/src/hooks/jev-activity.ts +3 -3
  132. package/src/hooks/jev-cli.ts +17 -17
  133. package/src/hooks/jev-cloud-connection.ts +4 -4
  134. package/src/hooks/manager.ts +3 -2
  135. package/src/hooks/pack-cli.ts +24 -23
  136. package/src/hooks/pack-manifest.ts +25 -2
  137. package/src/hooks/pack-store.ts +2 -2
  138. package/src/hooks/policy-authority.ts +51 -32
  139. package/src/hooks/policy-evaluator.ts +5 -5
  140. package/src/hooks/policy-registry.ts +5 -7
  141. package/src/hooks/policy-reviewability.ts +26 -10
  142. package/src/hooks/policy-types.ts +4 -4
  143. package/src/hooks/semantic/combine.ts +13 -13
  144. package/src/hooks/semantic/decide.ts +134 -29
  145. package/src/hooks/semantic/evaluator.ts +5 -5
  146. package/src/hooks/semantic/facts.ts +91 -2
  147. package/src/hooks/semantic/jev-client.ts +1 -1
  148. package/src/hooks/semantic/jev-config.ts +21 -12
  149. package/src/hooks/semantic/jev-review.ts +3 -3
  150. package/src/hooks/semantic/jev-stats.ts +17 -17
  151. package/src/hooks/semantic/pack-policies.ts +48 -48
  152. package/src/hooks/semantic/policies.ts +13 -425
  153. package/src/hooks/semantic/precondition-names.ts +1 -1
  154. package/src/hooks/semantic/preconditions.ts +1 -1
  155. package/src/hooks/semantic/types.ts +7 -1
  156. package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +0 -1
  157. package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +0 -1
  158. package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +0 -1
  159. package/.next/standalone/.next/static/chunks/36uhh9el_oz8e.js +0 -1
  160. package/.next/standalone/.next/static/chunks/3o3f1ibfci0p7.js +0 -6
  161. /package/.next/standalone/.next/static/{O_b5R1axf5cb4NxIXq2kS → z1C0kJVF0NCtWn5R88hW3}/_buildManifest.js +0 -0
  162. /package/.next/standalone/.next/static/{O_b5R1axf5cb4NxIXq2kS → z1C0kJVF0NCtWn5R88hW3}/_clientMiddlewareManifest.js +0 -0
  163. /package/.next/standalone/.next/static/{O_b5R1axf5cb4NxIXq2kS → z1C0kJVF0NCtWn5R88hW3}/_ssgManifest.js +0 -0
@@ -21,11 +21,20 @@ Then run:
21
21
  failproofai policies --install --cli hermes --scope user
22
22
  ```
23
23
 
24
- FailproofAI copies this directory to every discovered profile at
25
- `<HERMES_HOME>/plugins/failproofai/` and adds `failproofai` to
26
- `plugins.enabled` in that profile's `config.yaml`. Reinstall replaces only a
27
- directory carrying `.failproofai-managed`; an unrelated plugin with the same
28
- directory name is never overwritten.
24
+ The installer symlinks `<HERMES_HOME>/plugins/failproofai` in every discovered
25
+ profile to this directory and adds `failproofai` to `plugins.enabled` in that
26
+ profile's `config.yaml`. Hermes' plugin scan follows the link, so an npm
27
+ upgrade updates the plugin with no reinstall. Where a symlink cannot be
28
+ created, the files are copied instead and marked `.failproofai-managed`.
29
+ Reinstall replaces only such a marked copy or a link into a FailproofAI
30
+ `hermes-plugin/`; an unrelated plugin with the same name is never overwritten.
31
+ Python may write `__pycache__/` here when the package directory is writable;
32
+ it goes with the package on upgrade and is skipped when it cannot be written.
33
+
34
+ `failproofai update` moves profiles that already use FailproofAI (legacy shell
35
+ hooks or a copied plugin) to the link. Legacy shell hooks never ran for Hermes
36
+ cron jobs, so `update` exits non-zero and keeps the shell hooks when the
37
+ running daemon cannot serve the plugin.
29
38
 
30
39
  Legacy FailproofAI shell hooks are removed during migration. Operator-owned
31
40
  hooks and unrelated plugin settings are preserved. No dashboard deployment or
@@ -118,7 +127,8 @@ untrusted evaluation failures.
118
127
  ## Diagnostics and rollback
119
128
 
120
129
  `failproofai config --status` reports each existing Hermes profile as healthy,
121
- disabled, incomplete, or duplicated with a legacy shell hook. Hermes-side load
130
+ disabled, incomplete, duplicated with a legacy shell hook, or still on legacy
131
+ shell hooks alone (Hermes cron jobs are not checked). Hermes-side load
122
132
  errors are available through:
123
133
 
124
134
  ```bash
@@ -134,5 +144,5 @@ the integration with:
134
144
  failproofai policies --uninstall --cli hermes --scope user
135
145
  ```
136
146
 
137
- Uninstall removes the config registration and only the plugin directory marked
138
- as FailproofAI-managed.
147
+ Uninstall removes the config registration and the profile's link (or marked
148
+ copy); it never removes this package directory.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.8-beta.0",
3
+ "version": "1.0.9-beta.0",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 40 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -116,21 +116,21 @@
116
116
  "postcss": "8.5.26",
117
117
  "eslint-plugin-react-hooks": "7.0.1",
118
118
  "vite": "8.0.16",
119
- "undici": "7.29.0",
119
+ "undici": "7.29.1",
120
120
  "brace-expansion": "5.0.9",
121
121
  "sharp": "0.35.4",
122
122
  "browserslist": "4.28.8"
123
123
  },
124
124
  "optionalDependencies": {
125
- "@failproofai/failproofaid-linux-x64": "1.0.8-beta.0",
126
- "@failproofai/failproofaid-linux-arm64": "1.0.8-beta.0",
127
- "@failproofai/failproofaid-darwin-x64": "1.0.8-beta.0",
128
- "@failproofai/failproofaid-darwin-arm64": "1.0.8-beta.0"
125
+ "@failproofai/failproofaid-linux-x64": "1.0.9-beta.0",
126
+ "@failproofai/failproofaid-linux-arm64": "1.0.9-beta.0",
127
+ "@failproofai/failproofaid-darwin-x64": "1.0.9-beta.0",
128
+ "@failproofai/failproofaid-darwin-arm64": "1.0.9-beta.0"
129
129
  },
130
130
  "failproofaidBinaries": {
131
- "linux-x64": "35b641f60e1724513fbb760db382123dad2b5fcdd67dda6373bda80651bd36bb",
132
- "linux-arm64": "a058288f5d08f99612f053793e5c7f59264d3da97f3dbe769e401b0f8536702d",
133
- "darwin-x64": "b4f3dbe4aea44e77cc10839647c0dc03a1b8b797dfb27e1d89a6a96554ef27b8",
134
- "darwin-arm64": "fd42f28bf92001d39117b488a215578b966f07d316214744aca64a1888f2aed2"
131
+ "linux-x64": "044b6619007227b8aa1fc1095b8c61a17bd90e11da53183ac1f5adecdcae6839",
132
+ "linux-arm64": "479450d7d15c52a59bd1a8cf4c7086be9210e8d31ad4f98d99b123f571e944bd",
133
+ "darwin-x64": "11562912ff726195bfb966ed24a774ffea38d01acc3edc1f9bfa3ea9c298977f",
134
+ "darwin-arm64": "5bbaee1ec89d2d8032b7f16e502fb96891fbc6aa2ee6c861f43a64a0a60e61ba"
135
135
  }
136
136
  }
@@ -40,15 +40,12 @@
40
40
  * `customPolicies.add`, so a manifest rebuilt from the entry (`failproofai
41
41
  * publish`) says the same thing.
42
42
  *
43
- * **It emits no `semantic` array, deliberately.** A pack that declares one
44
- * REPLACES this build's compiled-in semantic set wholesale
45
- * (`semantic/pack-policies.ts`), and this pack is the dress rehearsal for the
46
- * regex floor leaving the package — not for Jev's question set leaving it. So
47
- * the 39 builtins travel and the 16 semantic policies stay compiled in, which
48
- * also keeps `manifestAuthority` below judging `reviewedBy` against the right
49
- * list: the builtin reviewer names, which is what a machine installing this pack
50
- * will still be running. The separate `FailproofAI/jev-policies` pack is where
51
- * the semantic half ships, and it declares its own `minCliVersion`.
43
+ * **It emits no `semantic` array, deliberately.** Jev's sixteen checks ship in
44
+ * the separate `FailproofAI/jev-policies` pack (the npm package carries none),
45
+ * which declares its own `minCliVersion`. So `manifestAuthority` below judges
46
+ * `reviewedBy` against those sixteen NAMES (`SEMANTIC_REVIEWER_NAMES`, its
47
+ * default): the checks a machine asks once it installs that pack beside this
48
+ * one. Without it, every policy here registers hard.
52
49
  */
53
50
  import { execFileSync } from "node:child_process";
54
51
  import { createHash } from "node:crypto";
@@ -52,6 +52,7 @@ import {
52
52
  PERMISSION_POLICIES,
53
53
  } from "./cloud-introspect";
54
54
  import type { CloudJevConfigWrite } from "./jev-cloud-connection";
55
+ import { jevChecksDeclared } from "./effective-reviewers";
55
56
  import {
56
57
  ingestPath,
57
58
  validateIngestKey,
@@ -177,7 +178,7 @@ export interface JevConnectOutcome {
177
178
  * alone like any existing file (never overwritten), but a connection that
178
179
  * asked for decisions only is told that Jev still sends more than that.
179
180
  */
180
- stillOn?: "shadow" | "enforce";
181
+ stillOn?: "observe" | "enforce";
181
182
  /**
182
183
  * Introspect gave no answer about this key's permissions — the server could
183
184
  * not be asked (`unreachable`) or predates the endpoint (`unsupported`) — so
@@ -434,13 +435,18 @@ function jevLines(outcome: ConnectOutcome): string[] {
434
435
  }
435
436
  const config = jev.config;
436
437
  if (!config || config.status === "written") {
437
- // Plain http (loopback only) is accepted in shadow mode and nowhere else, so
438
+ // Plain http (loopback only) is accepted in observe mode and nowhere else, so
438
439
  // the enforce command would be refused there, by the CLI and the dashboard.
439
440
  const plainHttp = config?.status === "written" && new URL(config.baseUrl).protocol === "http:";
440
441
  return [
441
- ` Jev on through FailproofAI Cloud, in shadow mode: logged, not enforced (${config?.path ?? "jev.json"}).`,
442
+ ` Jev on through FailproofAI Cloud, in observe mode: logged, not enforced (${config?.path ?? "jev.json"}).`,
443
+ // The package ships no Jev checks, so "on" asks nothing until a pack
444
+ // gives it some — said here, where the user just turned it on.
445
+ ...(jevChecksDeclared()
446
+ ? []
447
+ : [" It has no checks installed yet, so it asks nothing. To enable it: `failproofai policies add FailproofAI/jev-policies`."]),
442
448
  plainHttp
443
- ? " Enforce needs an https FailproofAI Cloud URL: reconnect with `failproofai config --token <key> --url https://…`. Over plain http Jev stays in shadow mode."
449
+ ? " Enforce needs an https FailproofAI Cloud URL: reconnect with `failproofai config --token <key> --url https://…`. Over plain http Jev stays in observe mode."
444
450
  : " Enforce it with `failproofai jev setup --mode enforce`, or from the dashboard.",
445
451
  ];
446
452
  }
@@ -449,7 +455,7 @@ function jevLines(outcome: ConnectOutcome): string[] {
449
455
  if (config.otherOrigin) {
450
456
  lines.push(
451
457
  config.jevOff?.why === "off"
452
- ? ` It points at ${config.otherOrigin} and has Jev switched off (mode off), so Jev is off. To point it here and turn it on: \`failproofai jev setup --provider failproofai --mode shadow\`.`
458
+ ? ` It points at ${config.otherOrigin} and has Jev switched off (mode off), so Jev is off. To point it here and turn it on: \`failproofai jev setup --provider failproofai --mode observe\`.`
453
459
  : ` It points at ${config.otherOrigin}, so Jev stays off until you run \`failproofai jev setup --provider failproofai\`.`,
454
460
  );
455
461
  } else if (config.jevOff?.why === "refused") {
@@ -457,7 +463,7 @@ function jevLines(outcome: ConnectOutcome): string[] {
457
463
  ` It was refused (${config.jevOff.problem}), so Jev is off. ${config.jevOff.fix ? `Fix: \`${config.jevOff.fix}\`.` : "Write a valid one: `failproofai jev setup --provider failproofai`."}`,
458
464
  );
459
465
  } else if (config.jevOff?.why === "off") {
460
- lines.push(" It has Jev switched off (mode off), so Jev is off. To turn it on: `failproofai jev setup --mode shadow`.");
466
+ lines.push(" It has Jev switched off (mode off), so Jev is off. To turn it on: `failproofai jev setup --mode observe`.");
461
467
  }
462
468
  if (jev.stillOn) {
463
469
  lines.push(
@@ -535,7 +535,7 @@ export async function loadAllCustomHooks(
535
535
  * policy reviewable, which is the one thing a manifest may never do.
536
536
  *
537
537
  * `knownReviewers` is what makes that hold for a pack that ships BOTH tiers.
538
- * Judged against the compiled-in set, a `reviewedBy` naming one of the pack's
538
+ * Judged against any other set, a `reviewedBy` naming one of the pack's
539
539
  * own checks is a name nothing here has, so the reviewable entry and its hard
540
540
  * peer resolved alike and the merge could not tell them apart — while
541
541
  * registration, which reads the manifest's own checks, honoured it. The set
@@ -1,20 +1,16 @@
1
1
  /**
2
2
  * The semantic checks a `reviewedBy` may name ON THIS MACHINE.
3
3
  *
4
- * ## Why this is not simply `SEMANTIC_REVIEWER_NAMES`
5
- *
6
- * A pack that declares its own `semantic` entries replaces the compiled-in
7
- * `SEMANTIC_POLICIES` wholesale (`semantic/pack-policies.ts`). So on that
8
- * machine the sixteen builtin names are checks that will never be asked, and the
9
- * pack's own names are the only ones that will — while
10
- * `resolvePolicyAuthority`, judging against the builtin set, would read the
11
- * pack's `reviewedBy: ["its-own-check"]` as naming a check "this build does not
12
- * have" and downgrade the policy to `hard` with nothing but a warning in the
13
- * hook log. That is a rewritten policy set arriving with its reviewability
14
- * quietly removed: the deny half of the two-tier evaluator working exactly as
15
- * documented while the clearing half cannot fire once — the failure
16
- * `policy-reviewability.ts` was written to make audible, reintroduced one layer
17
- * down.
4
+ * ## Only what an installed pack declares
5
+ *
6
+ * The npm package ships no Jev checks, so no name is a reviewer by default. A
7
+ * `reviewedBy` is honoured only for checks some installed pack declares — in
8
+ * practice `FailproofAI/jev-policies`, which the core pack's fifteen
9
+ * `reviewable` policies name. With no such pack the set is EMPTY, every policy
10
+ * registers `hard`, and nothing Jev says can clear a regex verdict. That is the
11
+ * fail-safe direction, and it is also what keeps Jev inert on a machine that
12
+ * never installed its checks: `jevChecksInstalled` below is the hook path's
13
+ * gate for starting a review at all.
18
14
  *
19
15
  * The names come from the MANIFEST, which is already where `authority` and
20
16
  * `reviewedBy` themselves are read from, so this adds no new trust and imports
@@ -39,7 +35,7 @@
39
35
  * silently.
40
36
  */
41
37
  import { readInstalledPacks, type ResolvedPack } from "./pack-manifest";
42
- import { SEMANTIC_REVIEWER_NAMES } from "./policy-authority";
38
+ import { NO_REVIEWERS, SEMANTIC_REVIEWER_NAMES } from "./policy-authority";
43
39
 
44
40
  let cached: ReadonlySet<string> | null = null;
45
41
  let cachedContested: ReadonlyMap<string, string[]> = new Map();
@@ -148,8 +144,9 @@ export function isFirstPartyPack(pack: { source?: string }): boolean {
148
144
  }
149
145
 
150
146
  /**
151
- * A builtin check name declared by a pack that is not FailproofAI's. Core and
152
- * user policies name those checks in `reviewedBy`, so the third party's
147
+ * One of FailproofAI's sixteen check names (`SEMANTIC_REVIEWER_NAMES`) declared
148
+ * by a pack that is not FailproofAI's. Core and user policies name those checks
149
+ * in `reviewedBy`, so the third party's
153
150
  * question would become the reviewer that clears them — an `instruct`-mode
154
151
  * `destructive-deletion` beside the core pack turned `block-rm-rf` into a
155
152
  * warning. The claim is void: never asked, never a reviewer, and never a
@@ -162,33 +159,26 @@ export function isReservedClaim(pack: { source?: string }, name: string): boolea
162
159
  }
163
160
 
164
161
  /**
165
- * The installed packs' semantic policy names when any pack declares some, and
166
- * this build's compiled-in set otherwise.
162
+ * The semantic check names the installed packs declare — the only names a
163
+ * `reviewedBy` is honoured for on this machine. Empty when no pack declares any.
167
164
  *
168
165
  * A name two packs claim differently is left out — see
169
- * {@link contestedSemanticNames}. When that leaves nothing, the compiled-in set
170
- * stands, which is not a softening but the same rule
171
- * `semanticPoliciesFromPacks` applies to the QUESTIONS: a declaring pack whose
172
- * every entry was unusable leaves the compiled-in semantic set live, so the
173
- * names honoured here are the names of the questions that will actually be
174
- * asked, and each of those is one of ours.
175
- *
176
- * Never throws: an unreadable manifest declares nothing, and the builtin set
177
- * stands. That is the same fail-open posture every other reader of the pack
178
- * manifest takes, and here it also fails in the safe direction — a pack's
179
- * `reviewedBy` will not match a builtin name, so its policies stay `hard`
180
- * rather than being cleared by a question nobody could read.
166
+ * {@link contestedSemanticNames} — and so is one of FailproofAI's names claimed
167
+ * by anyone else ({@link isReservedClaim}).
168
+ *
169
+ * Never throws: an unreadable manifest declares nothing, so the set is empty and
170
+ * every policy stays `hard`. There is no compiled-in set to fall back to.
181
171
  */
182
172
  export function effectiveReviewerNames(): ReadonlySet<string> {
183
173
  if (cached) return cached;
184
- let names: ReadonlySet<string> = SEMANTIC_REVIEWER_NAMES;
174
+ let names: ReadonlySet<string> = NO_REVIEWERS;
185
175
  cachedContested = new Map();
186
176
  try {
187
177
  const packs = jevPacks(readInstalledPacks().packs, reviewerCli);
188
178
  names = reviewerNamesFor(packs);
189
179
  cachedContested = contestedSemanticNames(packs);
190
180
  } catch {
191
- // See above: silence here is the builtin set, not an empty one.
181
+ // See above: an unreadable manifest declares no checks.
192
182
  }
193
183
  cached = names;
194
184
  return names;
@@ -201,17 +191,34 @@ export function contestedReviewerNames(): ReadonlyMap<string, string[]> {
201
191
  }
202
192
 
203
193
  /**
204
- * A first-party pack declares Jev checks, so they REPLACE the compiled-in set.
205
- * Anyone else's checks are ADDED to it: a stranger's one check must not switch
206
- * off `credential-exfiltration` and turn every core reviewable policy hard.
207
- * `semanticPoliciesFromPacks` applies the same rule to the questions.
194
+ * Whether any installed pack gives Jev something to ask. False on a machine
195
+ * with no pack declaring a `semantic` entry — the vanilla install — and then
196
+ * the hook path does not start a Jev review at all: no request, no intent
197
+ * capture, exactly as if Jev were unconfigured. `failproofai policies add
198
+ * FailproofAI/jev-policies` is what turns it true.
199
+ *
200
+ * Read off the same cached set registration just used, so it costs nothing on
201
+ * the hook path and cannot disagree with which policies registered reviewable.
202
+ */
203
+ export function jevChecksInstalled(): boolean {
204
+ return effectiveReviewerNames().size > 0;
205
+ }
206
+
207
+ /**
208
+ * The same question for a CLI command, read fresh rather than from the
209
+ * registration cache (which only the hook path fills): any agent, packs as
210
+ * `jevPacks` narrows them. Never throws; an unreadable manifest has none.
208
211
  */
209
- export function replacesBuiltinChecks(packs: ReadonlyArray<Pick<ResolvedPack, "semantic"> & { source?: string }>): boolean {
210
- return packs.some((p) => isFirstPartyPack(p) && (p.semantic ?? []).length > 0);
212
+ export function jevChecksDeclared(): boolean {
213
+ try {
214
+ return reviewerNamesFor(jevPacks(readInstalledPacks().packs)).size > 0;
215
+ } catch {
216
+ return false;
217
+ }
211
218
  }
212
219
 
213
220
  /**
214
- * The reviewer set for the packs taking part: see {@link replacesBuiltinChecks}. Pure.
221
+ * The reviewer set for the packs taking part: every usable name they declare. Pure.
215
222
  *
216
223
  * Manifest-only: it cannot see a check `semanticPoliciesFromPacks` drops for the
217
224
  * question budget, because measuring questions means loading the semantic
@@ -226,9 +233,9 @@ export function reviewerNamesFor(
226
233
  const declared = packs
227
234
  .flatMap((p) => (p.semantic ?? []).filter((s) => !isReservedClaim(p, s.name)).map((s) => s.name))
228
235
  .filter((name) => !contested.has(name));
229
- // Nothing usable declared: the compiled-in set is the one being asked.
230
- if (declared.length === 0) return SEMANTIC_REVIEWER_NAMES;
231
- return new Set(replacesBuiltinChecks(packs) ? declared : [...SEMANTIC_REVIEWER_NAMES, ...declared]);
236
+ // Nothing usable declared: nothing is asked, so nothing may clear.
237
+ if (declared.length === 0) return NO_REVIEWERS;
238
+ return new Set(declared);
232
239
  }
233
240
 
234
241
  /**
@@ -38,7 +38,7 @@ import type { JevActivityFields } from "./semantic/combine";
38
38
  import type { JevConfig } from "./semantic/jev-config";
39
39
  import { clearPolicies, registerPolicy, getPoliciesForEvent } from "./policy-registry";
40
40
  import { loadAllCustomHooks } from "./custom-hooks-loader";
41
- import { contestedReviewerNames, effectiveReviewerNames } from "./effective-reviewers";
41
+ import { contestedReviewerNames, effectiveReviewerNames, jevChecksInstalled } from "./effective-reviewers";
42
42
  import {
43
43
  authorityDeclarationFor,
44
44
  refusedAuthorityWarning,
@@ -57,7 +57,7 @@ import { getInstanceId } from "../../lib/telemetry-id";
57
57
  import { hookLogInfo, hookLogWarn } from "./hook-logger";
58
58
  import { readStdinPayload } from "./read-stdin";
59
59
  import { readActiveCloudManagedPolicies, type CloudManagedPolicyArtifact } from "./cloud-managed-policies";
60
- import { hasInstalledPacks, readInstalledPacks, type PackError, type ResolvedPack } from "./pack-manifest";
60
+ import { hasInstalledRegexPacks, readInstalledPacks, type PackError, type ResolvedPack } from "./pack-manifest";
61
61
  import { missingGuards, packFailureReason, combinedGuardMatch, guardsCover } from "./pack-failclosed";
62
62
  import { readActivePause, type ActivePause } from "./session-pause";
63
63
  import { jevConfigFile } from "./fp-home";
@@ -217,6 +217,10 @@ async function runObserved(
217
217
  // alone — exactly as it did before two tiers existed — otherwise:
218
218
  //
219
219
  // - a valid BYOK config exists (`~/.failproofai/jev.json`, global only);
220
+ // - an installed pack gives Jev checks to ask (`jevChecksInstalled`). The npm
221
+ // package ships none: until `failproofai policies add FailproofAI/jev-policies`
222
+ // (or another pack declaring `semantic` checks) Jev is inert — no request, no
223
+ // intent capture — whether the config is BYOK or FailproofAI Cloud's;
220
224
  // - `FAILPROOFAI_EVALUATOR` is not `legacy` (see "Turning Jev off" below);
221
225
  // - this is not the fail-closed `forceDecision` path and no session pause is
222
226
  // active — a pause suspends local policy, and Jev must not become a way to
@@ -230,7 +234,7 @@ async function runObserved(
230
234
  // Turning Jev off. The switch that works everywhere is the config file itself:
231
235
  // it is read on every event, here, in whichever process evaluates — the
232
236
  // daemon's warm worker included — so `failproofai jev remove` (or a `mode:
233
- // "shadow"` config, which keeps enforcing the regex result) applies from the
237
+ // "observe"` config, which keeps enforcing the regex result) applies from the
234
238
  // next tool call, with no restart. `FAILPROOFAI_EVALUATOR=legacy` is the dev
235
239
  // escape hatch, read from the EVALUATING process's environment: it covers a
236
240
  // one-shot hook (no daemon) and a worker whose own environment sets it, but
@@ -311,6 +315,8 @@ async function startTwoTier(
311
315
  if (isHumanAuthoredGate(session.rawHookEventName, cli)) return null;
312
316
  if (typeof parsed.tool_name !== "string" || parsed.tool_name.length === 0) return null;
313
317
  if (jevForcedOff(opts) || activePause) return null;
318
+ // Read off the reviewer set registration just cached, so it is free.
319
+ if (!jevChecksInstalled()) return null;
314
320
  const loaded = await readJevConfig();
315
321
  if (!loaded) return null;
316
322
  const cfg = loaded.config;
@@ -334,7 +340,7 @@ async function startTwoTier(
334
340
  // carries the fallback.
335
341
  hookLogInfo(`Jev review could not start (${err instanceof Error ? err.message : String(err)})`);
336
342
  return {
337
- mode: cfg.mode === "shadow" || cfg.mode === "enforce" ? cfg.mode : loaded.defaultMode,
343
+ mode: cfg.mode === "observe" || cfg.mode === "enforce" ? cfg.mode : loaded.defaultMode,
338
344
  review: Promise.resolve({ kind: "fallback", reason: "error", latencyMs: null, model: null }),
339
345
  abort: () => {},
340
346
  authorityOf: () => ({ authority: "hard", reviewedBy: [] }),
@@ -398,6 +404,7 @@ async function captureJevIntent(
398
404
  ): Promise<void> {
399
405
  if (canonicalEventType !== "UserPromptSubmit" || jevForcedOff(opts)) return;
400
406
  try {
407
+ if (!jevChecksInstalled()) return;
401
408
  if (!(await readJevConfig())) return;
402
409
  if (decision === "deny") {
403
410
  const { ENFORCEMENT_CAPABILITY } = await import("./enforcement-capability");
@@ -561,7 +568,9 @@ export async function evaluateHookEvent(
561
568
  // yet, and it must not lose enforcement in the gap before `failproofai
562
569
  // update` runs. It disappears for that machine the moment a pack is
563
570
  // installed, and never fires for a machine set up by this version.
564
- const packsInstalledHere = hasInstalledPacks();
571
+ // Only a pack with regex policies retires the shim: a Jev-only pack
572
+ // (FailproofAI/jev-policies) replaces none of them.
573
+ const packsInstalledHere = hasInstalledRegexPacks();
565
574
  const legacyNames =
566
575
  activePause || packsInstalledHere ? [] : config.enabledPolicies;
567
576
  // `alwaysOn` policies bypass the enabled set inside `registerBuiltinPolicies`,
@@ -738,8 +747,8 @@ export async function evaluateHookEvent(
738
747
  const observeOnly = cloudManaged?.effect === "observe" || pack?.effect === "observe";
739
748
  const fn: PolicyFunction = async (ctx): Promise<PolicyResult> => {
740
749
  if (observeOnly) {
741
- const shadow = await runObserved(hook, ctx, hookName, eventType, cli);
742
- if (shadow.decision !== "allow") {
750
+ const observed = await runObserved(hook, ctx, hookName, eventType, cli);
751
+ if (observed.decision !== "allow") {
743
752
  // Sourced from whichever layer asked to observe. This read
744
753
  // `cloudManaged!.id` — a non-null assertion that is simply false
745
754
  // for a pack, so an observe-mode PACK threw on its first non-allow
@@ -751,8 +760,8 @@ export async function evaluateHookEvent(
751
760
  observedResults.push({
752
761
  policyId: observer!.id,
753
762
  version: observer!.version,
754
- decision: shadow.decision,
755
- reason: shadow.reason ?? null,
763
+ decision: observed.decision,
764
+ reason: observed.reason ?? null,
756
765
  });
757
766
  }
758
767
  return { decision: "allow" };
@@ -928,19 +937,19 @@ export async function evaluateHookEvent(
928
937
  releaseRegistry: opts?.releaseRegistry,
929
938
  })
930
939
  : await evaluatePolicies(canonicalEventType, parsed, session, config);
931
- // Shadow mode: what Jev WOULD have done, filed where observe-mode policies
940
+ // Jev's observe mode: what Jev WOULD have done, filed where observe-mode policies
932
941
  // file theirs, so the "would have" view counts it with no second channel.
933
942
  // `semantic/<check>` cannot be mistaken for a cloud policy — cloud ids
934
943
  // carry no `/` (`POLICY_ID_RE` in cloud-managed-policies.ts) — and the
935
944
  // version is a Jev model id or `jev`, never a deployment number. Only on
936
945
  // the two-tier path, so an unconfigured row is untouched.
937
- const shadowVerdict = result.twoTier?.shadowVerdict;
938
- if (shadowVerdict) {
946
+ const observeVerdict = result.twoTier?.observeVerdict;
947
+ if (observeVerdict) {
939
948
  observedResults.push({
940
- policyId: shadowVerdict.policyName,
941
- version: shadowVerdict.version,
942
- decision: shadowVerdict.decision,
943
- reason: shadowVerdict.reason,
949
+ policyId: observeVerdict.policyName,
950
+ version: observeVerdict.version,
951
+ decision: observeVerdict.decision,
952
+ reason: observeVerdict.reason,
944
953
  });
945
954
  }
946
955
  const durationMs = Math.round(performance.now() - startTime);
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The Hermes half of `failproofai update`.
3
+ *
4
+ * npm replaces the CLI and nothing else, and until this ran `update` never
5
+ * looked at Hermes — so a machine that installed Hermes enforcement with ≤1.0.5
6
+ * kept its config.yaml shell hooks through every upgrade. Those hooks never
7
+ * reach Hermes cron jobs (each cron fire builds its own hook scope, which only
8
+ * discovered plugins join), so every scheduled job on such a machine ran
9
+ * unchecked while everything looked configured.
10
+ *
11
+ * This migrates each profile that already uses FailproofAI to the linked native
12
+ * plugin, and reports per profile. It fails (ok: false → non-zero exit) whenever
13
+ * a profile that uses FailproofAI is left on shell hooks or otherwise could not
14
+ * be brought current, because a green `update` is exactly how the gap stayed
15
+ * invisible.
16
+ */
17
+ import { migrateHermesProfiles, type HermesPluginInstallDeps, type HermesProfileMigration } from "./integrations";
18
+
19
+ export interface HermesUpdateResult {
20
+ ok: boolean;
21
+ lines: string[];
22
+ profiles: HermesProfileMigration[];
23
+ }
24
+
25
+ const STATUS_LABEL: Record<HermesProfileMigration["status"], string> = {
26
+ migrated: "migrated",
27
+ current: "already current",
28
+ untouched: "skipped",
29
+ blocked: "NOT migrated",
30
+ failed: "FAILED",
31
+ };
32
+
33
+ export async function runHermesUpdateMigration(deps: {
34
+ daemonSupportsPolicyEvaluation: () => Promise<boolean>;
35
+ installDeps?: HermesPluginInstallDeps;
36
+ }): Promise<HermesUpdateResult> {
37
+ const profiles = await migrateHermesProfiles(deps);
38
+ const relevant = profiles.filter((p) => p.status !== "untouched");
39
+ // A machine without FailproofAI on Hermes hears nothing about Hermes.
40
+ if (relevant.length === 0) return { ok: true, lines: [], profiles };
41
+
42
+ const width = Math.max(...profiles.map((p) => ("hermes/" + p.name).length));
43
+ const lines = ["Hermes:"];
44
+ for (const p of profiles) {
45
+ const label = ("hermes/" + p.name).padEnd(width);
46
+ const detail = p.status === "current" ? "" : ` — ${p.detail}`;
47
+ lines.push(` ${label} ${STATUS_LABEL[p.status]}${detail}`);
48
+ }
49
+
50
+ const blocked = profiles.filter((p) => p.status === "blocked");
51
+ const failed = profiles.filter((p) => p.status === "failed");
52
+ if (profiles.some((p) => p.status === "migrated")) {
53
+ lines.push(
54
+ " Cron jobs load the plugin on their next run; restart running Hermes gateways and sessions to load it there.",
55
+ );
56
+ }
57
+ if (blocked.length > 0) {
58
+ lines.push(
59
+ "",
60
+ `Hermes is NOT migrated (${blocked.map((p) => p.name).join(", ")}): the running failproofaid cannot`,
61
+ "answer the native plugin (no policyEvaluation), so nothing was changed. Profiles still on",
62
+ "shell hooks do NOT check Hermes cron jobs. Run `failproofai config` (it asks for sudo to",
63
+ "replace the daemon), then `failproofai update` again.",
64
+ );
65
+ }
66
+ if (failed.length > 0) {
67
+ lines.push(
68
+ "",
69
+ `Hermes could not be migrated for: ${failed.map((p) => p.name).join(", ")}. Fix the reason above,`,
70
+ "then run `failproofai update` again.",
71
+ );
72
+ }
73
+ return { ok: blocked.length === 0 && failed.length === 0, lines, profiles };
74
+ }
@@ -105,8 +105,8 @@ export interface HookActivityEntry {
105
105
  jevFallbackReason?: string;
106
106
  jevLatencyMs?: number;
107
107
  jevModel?: string;
108
- /** `shadow` logs Jev but enforces the regex result; `enforce` applies the combine rules. */
109
- jevMode?: "shadow" | "enforce";
108
+ /** `observe` logs Jev but enforces the regex result; `enforce` applies the combine rules. */
109
+ jevMode?: "observe" | "enforce";
110
110
  durationMs: number;
111
111
  sessionId?: string;
112
112
  transcriptPath?: string;
@@ -159,7 +159,7 @@ export interface HookActivityEntry {
159
159
  * indistinguishable from one where the policy never matched, and the rollout
160
160
  * being trialled is unmeasurable.
161
161
  *
162
- * Jev in shadow mode files its own deny/instruct here too, as
162
+ * Jev in observe mode files its own deny/instruct here too, as
163
163
  * `{policyId: "semantic/<check>", version: <Jev model id or "jev">}`: the
164
164
  * same question — what would this have done — for the same reader.
165
165
  */