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
@@ -14,11 +14,14 @@ import {
14
14
  existsSync,
15
15
  lstatSync,
16
16
  mkdirSync,
17
+ readlinkSync,
18
+ realpathSync,
17
19
  renameSync,
18
20
  rmSync,
21
+ symlinkSync,
19
22
  unlinkSync,
20
23
  } from "node:fs";
21
- import { resolve, dirname } from "node:path";
24
+ import { resolve, dirname, basename } from "node:path";
22
25
  import { fileURLToPath } from "node:url";
23
26
  import { homedir } from "node:os";
24
27
  import { parseDocument, type Document } from "yaml";
@@ -1388,9 +1391,13 @@ function makePiProjectRelativeEntry(extPath: string): string {
1388
1391
  // Hermes loads trusted Python plugins from `<HERMES_HOME>/plugins/<name>/`.
1389
1392
  // The shipped plugin registers Hermes-native hooks and talks directly to the
1390
1393
  // local failproofaid socket, avoiding a fresh CLI process per event. Config is
1391
- // YAML and profile-scoped; install copies the plugin into every profile and
1394
+ // YAML and profile-scoped; install LINKS every profile's `plugins/failproofai`
1395
+ // to the package's `hermes-plugin/` (so npm upgrades apply with no reinstall,
1396
+ // like OpenClaw's `plugins.load.paths`), falling back to a marked copy, and
1392
1397
  // adds `failproofai` to `plugins.enabled`. Legacy shell-hook entries are removed
1393
- // during migration so one tool call is never evaluated twice.
1398
+ // during migration so one tool call is never evaluated twice — and because
1399
+ // config.yaml shell hooks never reach Hermes cron jobs: each cron fire builds
1400
+ // its own hook scope, which discovered plugins join and config hooks do not.
1394
1401
 
1395
1402
  /** One hook entry as stored under a `hooks:` event key in config.yaml. */
1396
1403
  interface HermesHookEntry {
@@ -1409,38 +1416,137 @@ interface HermesPluginsConfig {
1409
1416
  const HERMES_PLUGIN_ID = "failproofai";
1410
1417
  const HERMES_PLUGIN_MARKER = ".failproofai-managed";
1411
1418
  const HERMES_PLUGIN_FILES = ["plugin.yaml", "__init__.py", "client.py", "ledger.py"] as const;
1419
+ /** Basename of the plugin directory inside the npm package. */
1420
+ const HERMES_PLUGIN_SOURCE_DIR = "hermes-plugin";
1412
1421
 
1413
1422
  function getHermesPluginSourcePath(): string {
1414
1423
  const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
1415
- if (fromEnv) return resolve(fromEnv, "hermes-plugin");
1416
- return resolve(fileURLToPath(import.meta.url), "..", "..", "..", "hermes-plugin");
1424
+ if (fromEnv) return resolve(fromEnv, HERMES_PLUGIN_SOURCE_DIR);
1425
+ return resolve(fileURLToPath(import.meta.url), "..", "..", "..", HERMES_PLUGIN_SOURCE_DIR);
1417
1426
  }
1418
1427
 
1419
1428
  export function hermesPluginPathForSettings(settingsPath: string): string {
1420
1429
  return resolve(dirname(settingsPath), "plugins", HERMES_PLUGIN_ID);
1421
1430
  }
1422
1431
 
1423
- function hasManagedHermesPlugin(settingsPath: string): boolean {
1424
- const pluginPath = hermesPluginPathForSettings(settingsPath);
1432
+ /** `existsSync` follows symlinks, so a dangling link reads as absent. This does not. */
1433
+ function pathPresent(path: string): boolean {
1425
1434
  try {
1426
- if (lstatSync(pluginPath).isSymbolicLink()) return false;
1427
- if (!existsSync(resolve(pluginPath, HERMES_PLUGIN_MARKER))) return false;
1428
- return HERMES_PLUGIN_FILES.every((file) => existsSync(resolve(pluginPath, file)));
1435
+ lstatSync(path);
1436
+ return true;
1429
1437
  } catch {
1430
1438
  return false;
1431
1439
  }
1432
1440
  }
1433
1441
 
1434
- function hasHermesPluginMarker(settingsPath: string): boolean {
1435
- const pluginPath = hermesPluginPathForSettings(settingsPath);
1442
+ function hasAllHermesPluginFiles(dir: string): boolean {
1443
+ return HERMES_PLUGIN_FILES.every((file) => existsSync(resolve(dir, file)));
1444
+ }
1445
+
1446
+ /**
1447
+ * Whether `dir` is a FailproofAI `hermes-plugin` directory (or was one: a link
1448
+ * whose target vanished during an npm upgrade or uninstall is still ours).
1449
+ * The basename is the package layout; the manifest name confirms it when the
1450
+ * target is readable, so a link to someone else's plugin is never claimed.
1451
+ */
1452
+ function isFailproofaiHermesPluginSource(dir: string): boolean {
1453
+ if (basename(dir) !== HERMES_PLUGIN_SOURCE_DIR) return false;
1454
+ if (!existsSync(dir)) return true;
1436
1455
  try {
1437
- return !lstatSync(pluginPath).isSymbolicLink() && existsSync(resolve(pluginPath, HERMES_PLUGIN_MARKER));
1456
+ const manifest = readFileSync(resolve(dir, "plugin.yaml"), "utf8");
1457
+ return /^name:\s*["']?failproofai["']?\s*$/m.test(manifest);
1438
1458
  } catch {
1439
1459
  return false;
1440
1460
  }
1441
1461
  }
1442
1462
 
1443
- function installHermesPlugin(settingsPath: string): void {
1463
+ function samePath(a: string, b: string): boolean {
1464
+ if (resolve(a) === resolve(b)) return true;
1465
+ try {
1466
+ return realpathSync(a) === realpathSync(b);
1467
+ } catch {
1468
+ return false;
1469
+ }
1470
+ }
1471
+
1472
+ /**
1473
+ * What occupies `<profile>/plugins/failproofai`, and whether FailproofAI owns it.
1474
+ *
1475
+ * • `link` — a symlink into a FailproofAI package's `hermes-plugin/` (the
1476
+ * current install shape: npm upgrades apply with no reinstall).
1477
+ * • `copy` — a copied directory carrying the `.failproofai-managed`
1478
+ * marker (1.0.6–1.0.8 installs, and the fallback where a
1479
+ * symlink cannot be created).
1480
+ * • `foreign` — anything else: an unmarked directory, a file, or a symlink
1481
+ * somewhere that is not a FailproofAI plugin. Never touched.
1482
+ */
1483
+ type HermesPluginState =
1484
+ | { kind: "absent" }
1485
+ | { kind: "link"; target: string; complete: boolean; current: boolean }
1486
+ | { kind: "copy"; complete: boolean }
1487
+ | { kind: "foreign" };
1488
+
1489
+ function hermesPluginState(settingsPath: string): HermesPluginState {
1490
+ const pluginPath = hermesPluginPathForSettings(settingsPath);
1491
+ let isLink: boolean;
1492
+ let isDirectory: boolean;
1493
+ try {
1494
+ const stat = lstatSync(pluginPath);
1495
+ isLink = stat.isSymbolicLink();
1496
+ isDirectory = stat.isDirectory();
1497
+ } catch {
1498
+ return { kind: "absent" };
1499
+ }
1500
+ if (isLink) {
1501
+ let target: string;
1502
+ try {
1503
+ target = resolve(dirname(pluginPath), readlinkSync(pluginPath));
1504
+ } catch {
1505
+ return { kind: "foreign" };
1506
+ }
1507
+ if (!isFailproofaiHermesPluginSource(target)) return { kind: "foreign" };
1508
+ return {
1509
+ kind: "link",
1510
+ target,
1511
+ complete: hasAllHermesPluginFiles(target),
1512
+ current: samePath(target, getHermesPluginSourcePath()),
1513
+ };
1514
+ }
1515
+ if (isDirectory && existsSync(resolve(pluginPath, HERMES_PLUGIN_MARKER))) {
1516
+ return { kind: "copy", complete: hasAllHermesPluginFiles(pluginPath) };
1517
+ }
1518
+ return { kind: "foreign" };
1519
+ }
1520
+
1521
+ function hasManagedHermesPlugin(settingsPath: string): boolean {
1522
+ const state = hermesPluginState(settingsPath);
1523
+ return (state.kind === "link" || state.kind === "copy") && state.complete;
1524
+ }
1525
+
1526
+ /** Test seam: lets a test make symlink creation fail to exercise the copy fallback. */
1527
+ export interface HermesPluginInstallDeps {
1528
+ symlink?: (target: string, path: string) => void;
1529
+ }
1530
+
1531
+ function defaultHermesSymlink(target: string, path: string): void {
1532
+ // A junction needs no privilege on Windows; the type is ignored elsewhere.
1533
+ symlinkSync(target, path, process.platform === "win32" ? "junction" : "dir");
1534
+ }
1535
+
1536
+ /**
1537
+ * Put the plugin at `<profile>/plugins/failproofai`, preferring a symlink to
1538
+ * the package's own `hermes-plugin/` so every npm upgrade updates it in place —
1539
+ * the same reason OpenClaw's config points into the package. Hermes' plugin
1540
+ * scan uses `Path.is_dir()`, which follows symlinks, so the link is discovered
1541
+ * like a directory. Falls back to a marked copy when a link cannot be created.
1542
+ *
1543
+ * Only a path FailproofAI owns (a marked copy, or a link into a FailproofAI
1544
+ * `hermes-plugin/`) is ever replaced; anything else throws.
1545
+ */
1546
+ export function installHermesPlugin(
1547
+ settingsPath: string,
1548
+ deps: HermesPluginInstallDeps = {},
1549
+ ): "linked" | "copied" | "unchanged" {
1444
1550
  const source = getHermesPluginSourcePath();
1445
1551
  for (const file of HERMES_PLUGIN_FILES) {
1446
1552
  if (!existsSync(resolve(source, file))) {
@@ -1449,50 +1555,88 @@ function installHermesPlugin(settingsPath: string): void {
1449
1555
  }
1450
1556
 
1451
1557
  const destination = hermesPluginPathForSettings(settingsPath);
1452
- if (existsSync(destination)) {
1453
- if (lstatSync(destination).isSymbolicLink() || !existsSync(resolve(destination, HERMES_PLUGIN_MARKER))) {
1454
- throw new Error(
1455
- `Refusing to overwrite an unmanaged Hermes plugin at ${destination}. ` +
1456
- `Move it aside or install FailproofAI under a different Hermes profile.`,
1457
- );
1458
- }
1558
+ const existing = hermesPluginState(settingsPath);
1559
+ if (existing.kind === "foreign") {
1560
+ throw new Error(
1561
+ `Refusing to overwrite an unmanaged Hermes plugin at ${destination}. ` +
1562
+ `Move it aside or install FailproofAI under a different Hermes profile.`,
1563
+ );
1459
1564
  }
1565
+ if (existing.kind === "link" && existing.current) return "unchanged";
1460
1566
 
1461
1567
  mkdirSync(dirname(destination), { recursive: true });
1462
1568
  const suffix = `${process.pid}-${Date.now()}`;
1463
1569
  const temporary = `${destination}.install-${suffix}`;
1464
- const backup = `${destination}.backup-${suffix}`;
1465
- let movedExisting = false;
1570
+
1571
+ let linked = false;
1466
1572
  try {
1573
+ (deps.symlink ?? defaultHermesSymlink)(source, temporary);
1574
+ linked = true;
1575
+ } catch {
1576
+ rmSync(temporary, { recursive: true, force: true });
1577
+ }
1578
+
1579
+ if (!linked) {
1467
1580
  mkdirSync(temporary, { recursive: true });
1468
- for (const file of HERMES_PLUGIN_FILES) {
1469
- cpSync(resolve(source, file), resolve(temporary, file));
1581
+ try {
1582
+ for (const file of HERMES_PLUGIN_FILES) {
1583
+ cpSync(resolve(source, file), resolve(temporary, file));
1584
+ }
1585
+ writeFileSync(
1586
+ resolve(temporary, HERMES_PLUGIN_MARKER),
1587
+ "Managed by failproofai. Remove with `failproofai policies --uninstall --cli hermes`.\n",
1588
+ { encoding: "utf8", mode: 0o600 },
1589
+ );
1590
+ } catch (err) {
1591
+ rmSync(temporary, { recursive: true, force: true });
1592
+ throw err;
1470
1593
  }
1471
- writeFileSync(
1472
- resolve(temporary, HERMES_PLUGIN_MARKER),
1473
- "Managed by failproofai. Remove with `failproofai policies --uninstall --cli hermes`.\n",
1474
- { encoding: "utf8", mode: 0o600 },
1475
- );
1476
- if (existsSync(destination)) {
1594
+ }
1595
+
1596
+ // A link replacing a link is one atomic rename. Anything replacing a
1597
+ // directory (or a link replaced by a directory) cannot rename over it, so
1598
+ // the old entry is moved aside first and restored if the swap fails.
1599
+ if (linked && (existing.kind === "absent" || existing.kind === "link")) {
1600
+ try {
1601
+ renameSync(temporary, destination);
1602
+ } catch (err) {
1603
+ rmSync(temporary, { force: true });
1604
+ throw err;
1605
+ }
1606
+ return "linked";
1607
+ }
1608
+
1609
+ const backup = `${destination}.backup-${suffix}`;
1610
+ let movedExisting = false;
1611
+ try {
1612
+ if (pathPresent(destination)) {
1477
1613
  renameSync(destination, backup);
1478
1614
  movedExisting = true;
1479
1615
  }
1480
1616
  renameSync(temporary, destination);
1617
+ // fs.rm never follows a symlink, so a backed-up link removes only itself.
1481
1618
  if (movedExisting) rmSync(backup, { recursive: true, force: true });
1482
1619
  } catch (err) {
1483
1620
  rmSync(temporary, { recursive: true, force: true });
1484
- if (movedExisting && !existsSync(destination) && existsSync(backup)) {
1621
+ if (movedExisting && !pathPresent(destination) && pathPresent(backup)) {
1485
1622
  renameSync(backup, destination);
1486
1623
  }
1487
1624
  throw err;
1488
1625
  }
1626
+ return linked ? "linked" : "copied";
1489
1627
  }
1490
1628
 
1491
1629
  function removeManagedHermesPlugin(settingsPath: string): boolean {
1492
1630
  const pluginPath = hermesPluginPathForSettings(settingsPath);
1631
+ const state = hermesPluginState(settingsPath);
1632
+ if (state.kind === "link") {
1633
+ // Only the link goes; the package directory it points at is npm's.
1634
+ unlinkSync(pluginPath);
1635
+ return true;
1636
+ }
1493
1637
  // The marker is the ownership boundary. Remove even an incomplete managed
1494
- // install so an interrupted copy can always be repaired or uninstalled.
1495
- if (!hasHermesPluginMarker(settingsPath)) return false;
1638
+ // copy so an interrupted install can always be repaired or uninstalled.
1639
+ if (state.kind !== "copy") return false;
1496
1640
  rmSync(pluginPath, { recursive: true, force: true });
1497
1641
  return true;
1498
1642
  }
@@ -1549,13 +1693,163 @@ function hermesConfigState(settingsPath: string): {
1549
1693
  };
1550
1694
  }
1551
1695
 
1696
+ /**
1697
+ * Enable the native plugin in one profile's config and drop FailproofAI's
1698
+ * legacy shell hooks. A native plugin and the legacy shell bridge running
1699
+ * together would evaluate every event twice. Only failproofai-owned shell
1700
+ * entries are removed; unrelated operator hooks remain intact.
1701
+ */
1702
+ function enableHermesPluginInDoc(doc: Document): void {
1703
+ removeLegacyHermesHooks(doc);
1704
+
1705
+ const js = (doc.toJS() ?? {}) as { plugins?: HermesPluginsConfig };
1706
+ const plugins: HermesPluginsConfig =
1707
+ js.plugins && typeof js.plugins === "object" ? js.plugins : {};
1708
+ const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
1709
+ if (!enabled.includes(HERMES_PLUGIN_ID)) enabled.push(HERMES_PLUGIN_ID);
1710
+ plugins.enabled = enabled;
1711
+ if (Array.isArray(plugins.disabled)) {
1712
+ plugins.disabled = plugins.disabled.filter((name) => name !== HERMES_PLUGIN_ID);
1713
+ if (plugins.disabled.length === 0) delete plugins.disabled;
1714
+ }
1715
+ doc.set("plugins", plugins);
1716
+ }
1717
+
1718
+ export type HermesMigrationStatus =
1719
+ /** Brought to the linked-plugin state by this run. */
1720
+ | "migrated"
1721
+ /** Already linked to this package, enabled, and free of shell hooks. */
1722
+ | "current"
1723
+ /** No FailproofAI integration in this profile — left alone. */
1724
+ | "untouched"
1725
+ /** Needs migrating, but the daemon cannot serve the native plugin. Nothing changed. */
1726
+ | "blocked"
1727
+ /** Could not be migrated (unmanaged plugin in the way, unreadable config, I/O error). */
1728
+ | "failed";
1729
+
1730
+ export interface HermesProfileMigration {
1731
+ name: string;
1732
+ home: string;
1733
+ status: HermesMigrationStatus;
1734
+ detail: string;
1735
+ /** Legacy config.yaml shell hooks are still in place after this run. */
1736
+ legacyShellHooksRemain: boolean;
1737
+ }
1738
+
1739
+ /**
1740
+ * The upgrade half `failproofai update` owes Hermes: bring every profile that
1741
+ * already uses FailproofAI (legacy shell hooks, a copied plugin, or a link) to
1742
+ * the linked-plugin state. Profiles with no FailproofAI integration are never
1743
+ * touched — an update must not opt a profile in.
1744
+ *
1745
+ * `daemonSupportsPolicyEvaluation` is asked at most once, and only when some
1746
+ * profile needs changing. When it says no, NOTHING is changed: removing the
1747
+ * shell hooks in favour of a plugin whose daemon cannot answer would fail every
1748
+ * tool call closed (the same gate `policies --install` applies).
1749
+ */
1750
+ export async function migrateHermesProfiles(opts: {
1751
+ daemonSupportsPolicyEvaluation: () => Promise<boolean>;
1752
+ installDeps?: HermesPluginInstallDeps;
1753
+ }): Promise<HermesProfileMigration[]> {
1754
+ const results: HermesProfileMigration[] = [];
1755
+ let daemonOk: boolean | undefined;
1756
+ for (const profile of listHermesProfiles()) {
1757
+ if (!existsSync(profile.home)) continue;
1758
+ const settingsPath = resolve(profile.home, "config.yaml");
1759
+ const pluginPath = hermesPluginPathForSettings(settingsPath);
1760
+ const config = hermesConfigState(settingsPath);
1761
+ const plugin = hermesPluginState(settingsPath);
1762
+ const ours = plugin.kind === "link" || plugin.kind === "copy";
1763
+ const base = { name: profile.name, home: profile.home };
1764
+ const legacy = config.legacyShellHookPresent;
1765
+
1766
+ if (!legacy && !ours && !config.pluginEnabled) {
1767
+ results.push({ ...base, status: "untouched", detail: "no failproofai integration", legacyShellHooksRemain: false });
1768
+ continue;
1769
+ }
1770
+ if (plugin.kind === "link" && plugin.current && plugin.complete && config.pluginEnabled && !legacy) {
1771
+ results.push({ ...base, status: "current", detail: "already linked", legacyShellHooksRemain: false });
1772
+ continue;
1773
+ }
1774
+ if (plugin.kind === "foreign") {
1775
+ results.push({
1776
+ ...base,
1777
+ status: "failed",
1778
+ detail:
1779
+ `an unmanaged plugin occupies ${pluginPath}` +
1780
+ (legacy ? "; shell hooks left in place (cron jobs are not checked)" : ""),
1781
+ legacyShellHooksRemain: legacy,
1782
+ });
1783
+ continue;
1784
+ }
1785
+ if (existsSync(settingsPath)) {
1786
+ const errors = parseDocument(readFileSync(settingsPath, "utf8")).errors;
1787
+ if (errors.length > 0) {
1788
+ results.push({
1789
+ ...base,
1790
+ status: "failed",
1791
+ detail: `${settingsPath} does not parse as YAML; left untouched`,
1792
+ legacyShellHooksRemain: legacy,
1793
+ });
1794
+ continue;
1795
+ }
1796
+ }
1797
+
1798
+ if (daemonOk === undefined) daemonOk = await opts.daemonSupportsPolicyEvaluation();
1799
+ if (!daemonOk) {
1800
+ results.push({
1801
+ ...base,
1802
+ status: "blocked",
1803
+ detail: legacy
1804
+ ? "daemon lacks native policy evaluation; shell hooks left in place (cron jobs are not checked)"
1805
+ : "daemon lacks native policy evaluation; the plugin here cannot get verdicts",
1806
+ legacyShellHooksRemain: legacy,
1807
+ });
1808
+ continue;
1809
+ }
1810
+
1811
+ try {
1812
+ // Plugin first, config second: the shell hooks are only removed once the
1813
+ // plugin that replaces them is on disk.
1814
+ const mode = installHermesPlugin(settingsPath, opts.installDeps);
1815
+ const doc = readYamlDoc(settingsPath);
1816
+ enableHermesPluginInDoc(doc);
1817
+ writeYamlDoc(settingsPath, doc);
1818
+ const from = legacy
1819
+ ? "shell hooks"
1820
+ : plugin.kind === "copy"
1821
+ ? "copied plugin"
1822
+ : plugin.kind === "link"
1823
+ ? "old plugin link"
1824
+ : "enabled but missing plugin";
1825
+ const to = mode === "copied" ? "plugin copy (symlink not possible here)" : "linked plugin";
1826
+ results.push({ ...base, status: "migrated", detail: `${from} → ${to}`, legacyShellHooksRemain: false });
1827
+ } catch (err) {
1828
+ results.push({
1829
+ ...base,
1830
+ status: "failed",
1831
+ detail: err instanceof Error ? err.message : String(err),
1832
+ legacyShellHooksRemain: hermesConfigState(settingsPath).legacyShellHookPresent,
1833
+ });
1834
+ }
1835
+ }
1836
+ return results;
1837
+ }
1838
+
1552
1839
  export interface HermesProfileHealth {
1553
1840
  name: string;
1554
1841
  home: string;
1555
1842
  settingsPath: string;
1556
1843
  pluginInstalled: boolean;
1844
+ /** How the plugin is installed: a link into the package, a copy, or neither. */
1845
+ pluginMode: "link" | "copy" | null;
1557
1846
  pluginEnabled: boolean;
1558
1847
  legacyShellHookPresent: boolean;
1848
+ /**
1849
+ * Still enforced only by legacy config.yaml shell hooks. Hermes cron jobs run
1850
+ * with none of those, so their tool calls go unchecked.
1851
+ */
1852
+ cronUnchecked: boolean;
1559
1853
  healthy: boolean;
1560
1854
  }
1561
1855
 
@@ -1564,15 +1858,19 @@ export function hermesProfileHealth(): HermesProfileHealth[] {
1564
1858
  return listHermesProfiles().map((profile) => {
1565
1859
  const settingsPath = resolve(profile.home, "config.yaml");
1566
1860
  const config = hermesConfigState(settingsPath);
1567
- const pluginInstalled = hasManagedHermesPlugin(settingsPath);
1861
+ const plugin = hermesPluginState(settingsPath);
1862
+ const pluginInstalled = (plugin.kind === "link" || plugin.kind === "copy") && plugin.complete;
1863
+ const pluginWorks = pluginInstalled && config.pluginEnabled;
1568
1864
  return {
1569
1865
  name: profile.name,
1570
1866
  home: profile.home,
1571
1867
  settingsPath,
1572
1868
  pluginInstalled,
1869
+ pluginMode: plugin.kind === "link" || plugin.kind === "copy" ? plugin.kind : null,
1573
1870
  pluginEnabled: config.pluginEnabled,
1574
1871
  legacyShellHookPresent: config.legacyShellHookPresent,
1575
- healthy: pluginInstalled && config.pluginEnabled && !config.legacyShellHookPresent,
1872
+ cronUnchecked: config.legacyShellHookPresent && !pluginWorks,
1873
+ healthy: pluginWorks && !config.legacyShellHookPresent,
1576
1874
  };
1577
1875
  });
1578
1876
  }
@@ -1582,13 +1880,25 @@ export function hermesProfileStatusRows(): Array<[string, string]> {
1582
1880
  return hermesProfileHealth()
1583
1881
  .filter((profile) => existsSync(profile.home))
1584
1882
  .map((profile) => {
1883
+ const label = "hermes/" + profile.name;
1884
+ if (profile.cronUnchecked) {
1885
+ // Not a cosmetic leftover: the shell-hook integration does not reach
1886
+ // cron jobs at all, so this profile has an unenforced path.
1887
+ return [
1888
+ label,
1889
+ "UNHEALTHY — legacy shell hooks: Hermes cron jobs are not checked. Run `failproofai update`",
1890
+ ];
1891
+ }
1585
1892
  const problems: string[] = [];
1586
1893
  if (!profile.pluginInstalled) problems.push("plugin files missing or incomplete");
1587
1894
  if (!profile.pluginEnabled) problems.push("plugin not enabled");
1588
1895
  if (profile.legacyShellHookPresent) problems.push("legacy shell hook also present");
1896
+ if (problems.length > 0) return [label, "UNHEALTHY — " + problems.join("; ")];
1589
1897
  return [
1590
- "hermes/" + profile.name,
1591
- problems.length === 0 ? "native plugin enabled" : "UNHEALTHY — " + problems.join("; "),
1898
+ label,
1899
+ profile.pluginMode === "copy"
1900
+ ? "native plugin enabled (copied — `failproofai update` links it so upgrades apply)"
1901
+ : "native plugin enabled",
1592
1902
  ];
1593
1903
  });
1594
1904
  }
@@ -1641,23 +1951,7 @@ export const hermes: Integration = {
1641
1951
  isFailproofaiHook: isMarkedHook,
1642
1952
 
1643
1953
  writeHookEntries(settings) {
1644
- const doc = settings as unknown as Document;
1645
- // A native plugin and the legacy shell bridge running together would
1646
- // evaluate every event twice. Remove only failproofai-owned shell entries;
1647
- // unrelated operator hooks remain intact.
1648
- removeLegacyHermesHooks(doc);
1649
-
1650
- const js = (doc.toJS() ?? {}) as { plugins?: HermesPluginsConfig };
1651
- const plugins: HermesPluginsConfig =
1652
- js.plugins && typeof js.plugins === "object" ? js.plugins : {};
1653
- const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
1654
- if (!enabled.includes(HERMES_PLUGIN_ID)) enabled.push(HERMES_PLUGIN_ID);
1655
- plugins.enabled = enabled;
1656
- if (Array.isArray(plugins.disabled)) {
1657
- plugins.disabled = plugins.disabled.filter((name) => name !== HERMES_PLUGIN_ID);
1658
- if (plugins.disabled.length === 0) delete plugins.disabled;
1659
- }
1660
- doc.set("plugins", plugins);
1954
+ enableHermesPluginInDoc(settings as unknown as Document);
1661
1955
  },
1662
1956
 
1663
1957
  removeHooksFromFile(settingsPath) {
@@ -57,7 +57,7 @@ export const JEV_ACTIVITY_KEYS = [
57
57
 
58
58
  const EVALUATORS = new Set(["jev", "jev-fallback"]);
59
59
  const DECISIONS = new Set(["allow", "instruct", "deny"]);
60
- const MODES = new Set(["shadow", "enforce"]);
60
+ const MODES = new Set(["observe", "enforce"]);
61
61
 
62
62
  /**
63
63
  * A reason's leading code — lowercase kebab-case — either alone or followed by
@@ -374,9 +374,9 @@ export function describeJevActivity(raw: JevActivityFields): string[] | null {
374
374
  if (e.jevDecision) facts.push(`Jev verdict: ${e.jevDecision}`);
375
375
  const cleared = e.jevCleared ?? [];
376
376
  if (cleared.length > 0) {
377
- facts.push(`${e.jevMode === "shadow" ? "would have cleared" : "cleared"} ${cleared.join(", ")}`);
377
+ facts.push(`${e.jevMode === "observe" ? "would have cleared" : "cleared"} ${cleared.join(", ")}`);
378
378
  }
379
- if (e.jevMode === "shadow") facts.push("shadow mode: the regex result was enforced");
379
+ if (e.jevMode === "observe") facts.push("observe mode: the regex result was enforced");
380
380
  if (e.jevLatencyMs !== undefined) facts.push(`${e.jevLatencyMs} ms`);
381
381
  if (e.jevModel) facts.push(e.jevModel);
382
382
  // Answered, but nothing this build can show (a verdict in a shape it cannot read).
@@ -186,7 +186,7 @@ export const JEV_USAGE = [
186
186
  "Usage:",
187
187
  " failproofai jev --url <url> [--key-stdin | --token <token>] [options]",
188
188
  " failproofai jev setup --provider <kind> [--key-stdin | --key-from-env] [options]",
189
- " failproofai jev setup --provider failproofai [--mode off|shadow|enforce] (FailproofAI Cloud: no key, no URL)",
189
+ " failproofai jev setup --provider failproofai [--mode off|observe|enforce] (FailproofAI Cloud: no key, no URL)",
190
190
  " failproofai jev status [--json]",
191
191
  " failproofai jev test [--json]",
192
192
  " failproofai jev models [--provider <kind>] [--url <base>] [--json]",
@@ -269,7 +269,7 @@ function modeLine(mode: NonNullable<JevConfig["mode"]>): string {
269
269
  if (mode === "off") return "off — Jev is not asked at all; the regex policies decide alone";
270
270
  return mode === "enforce"
271
271
  ? "enforce — Jev's verdicts apply: it may clear a reviewable policy's deny and add its own"
272
- : "shadow — Jev is asked and logged; the regex result is what is enforced";
272
+ : "observe — Jev is asked and logged; the regex result is what is enforced";
273
273
  }
274
274
 
275
275
  /**
@@ -458,12 +458,11 @@ export function jevStatsLines(stats: JevStats | null, opts: RenderOpts = {}): st
458
458
  .join(", ");
459
459
  const clears = byPolicy(stats.clearsByPolicy);
460
460
  // `clearsByPolicy` counts clears that CHANGED an outcome, which only enforce
461
- // mode can do; in shadow mode every clear Jev would have made is in
462
- // `shadowClearsByPolicy` instead and this one is empty. Printing the first
463
- // alone would tell a shadow user "cleared nothing" — the one number they
464
- // turned shadow mode on to watch. The field is optional because the stats
465
- // module T1 builds against does not have it yet (T8 adds it).
466
- const shadowClears = byPolicy((stats as { shadowClearsByPolicy?: Record<string, number> }).shadowClearsByPolicy);
461
+ // mode can do; in observe mode every clear Jev would have made is in
462
+ // `observeClearsByPolicy` instead and this one is empty. Printing the first
463
+ // alone would tell an observe-mode user "cleared nothing" — the one number
464
+ // they turned observe mode on to watch.
465
+ const observeClears = byPolicy(stats.observeClearsByPolicy);
467
466
  const ms = (v: number | null) => (v === null ? "—" : `${Math.round(v)} ms`);
468
467
  return stack(
469
468
  heading,
@@ -473,7 +472,7 @@ export function jevStatsLines(stats: JevStats | null, opts: RenderOpts = {}): st
473
472
  ["fell back to regex", `${pct(stats.fallbackRate)}${reasons ? ` (${reasons})` : ""}`],
474
473
  ["latency", `p50 ${ms(stats.latencyP50Ms)} · p95 ${ms(stats.latencyP95Ms)}`],
475
474
  ["cleared", clears || "nothing"],
476
- ...(shadowClears ? ([["would have cleared (shadow)", shadowClears]] as Array<[string, string]>) : []),
475
+ ...(observeClears ? ([["would have cleared (observe)", observeClears]] as Array<[string, string]>) : []),
477
476
  ],
478
477
  opts,
479
478
  ),
@@ -1206,11 +1205,10 @@ async function cloudSetup(values: Map<string, string>, bools: Set<string>, opts:
1206
1205
  delete next.apiKey;
1207
1206
  delete next.accountId;
1208
1207
  if (!sameProvider && existing?.raw.timeoutMs !== undefined) next.timeoutMs = existing.raw.timeoutMs;
1209
- // Shadow unless told otherwise, as `config --token` starts it: Jev on a new
1208
+ // Observe unless told otherwise, as `config --token` starts it: Jev on a new
1210
1209
  // route is logged before it is allowed to clear anything. A mode already in a
1211
1210
  // Cloud file is kept.
1212
- const mode = values.get("--mode") ?? (sameProvider && typeof next.mode === "string" ? next.mode : "shadow");
1213
- next.mode = mode;
1211
+ next.mode = values.get("--mode") ?? (sameProvider && typeof next.mode === "string" ? next.mode : "observe");
1214
1212
  if (values.has("--timeout-ms")) {
1215
1213
  const rawTimeout = values.get("--timeout-ms") as string;
1216
1214
  const n = Number(rawTimeout);
@@ -1247,7 +1245,7 @@ async function cloudSetup(values: Map<string, string>, bools: Set<string>, opts:
1247
1245
  ]
1248
1246
  : [
1249
1247
  "Not saved: this machine is not connected to FailproofAI Cloud with a key that carries jev:evaluate.",
1250
- "Connect it with one that does (the \"machine\" preset) — that also turns Jev on, in shadow mode, when there is no jev.json yet:",
1248
+ "Connect it with one that does (the \"machine\" preset) — that also turns Jev on, in observe mode, when there is no jev.json yet:",
1251
1249
  ]),
1252
1250
  " failproofai config --token <key>",
1253
1251
  "",
@@ -1332,7 +1330,7 @@ async function cloudSetup(values: Map<string, string>, bools: Set<string>, opts:
1332
1330
  /**
1333
1331
  * Where a file would send requests, for display, whether or not it loads. Only
1334
1332
  * the routing fields are used — the key and model do not decide where requests
1335
- * go — and shadow mode lets a loopback http URL through validation. Null when
1333
+ * go — and observe mode lets a loopback http URL through validation. Null when
1336
1334
  * it names nothing recognisable.
1337
1335
  */
1338
1336
  /**
@@ -1366,7 +1364,7 @@ function displayOnlyCloudOrigin(routing: { provider?: unknown; baseUrl?: unknown
1366
1364
 
1367
1365
  function namedEndpoint(raw: Record<string, unknown> | null): string | null {
1368
1366
  if (!raw || typeof raw.provider !== "string" || !(JEV_PROVIDER_KINDS as readonly string[]).includes(raw.provider)) return null;
1369
- const routing: Record<string, unknown> = { provider: raw.provider, apiKey: ENV_KEY_STAND_IN, mode: "shadow", ...displayOnlyCloudOrigin(raw) };
1367
+ const routing: Record<string, unknown> = { provider: raw.provider, apiKey: ENV_KEY_STAND_IN, mode: "observe", ...displayOnlyCloudOrigin(raw) };
1370
1368
  if (raw.baseUrl !== undefined) routing.baseUrl = raw.baseUrl;
1371
1369
  if (raw.accountId !== undefined) routing.accountId = raw.accountId;
1372
1370
  try {
@@ -1499,6 +1497,8 @@ async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
1499
1497
  enabled: coverage.enabled,
1500
1498
  reviewable: coverage.reviewable,
1501
1499
  customPolicyFiles: coverage.customFiles,
1500
+ // Checks an installed pack gives Jev to ask; 0 means Jev is inert.
1501
+ jevChecks: coverage.jevChecks,
1502
1502
  problem: coverageProblem,
1503
1503
  },
1504
1504
  }
@@ -1599,7 +1599,7 @@ async function status(argv: string[], opts: RenderOpts): Promise<JevCliResult> {
1599
1599
  ],
1600
1600
  opts,
1601
1601
  ),
1602
- nextStep("failproofai jev setup --mode shadow", "Switch it back on (shadow logs Jev and keeps enforcing regex; enforce lets it clear), here or from the dashboard:", opts),
1602
+ nextStep("failproofai jev setup --mode observe", "Switch it back on (observe logs Jev and keeps enforcing regex; enforce lets it clear), here or from the dashboard:", opts),
1603
1603
  legacyNote,
1604
1604
  jevStatsLines(stats, opts),
1605
1605
  ),
@@ -1786,7 +1786,7 @@ async function test(argv: string[], deps: JevCliDeps, opts: RenderOpts): Promise
1786
1786
  : inspection.status === "key-missing"
1787
1787
  ? "failproofai jev setup --key-stdin < key-file"
1788
1788
  : inspection.status === "off"
1789
- ? "failproofai jev setup --mode shadow"
1789
+ ? "failproofai jev setup --mode observe"
1790
1790
  : inspection.status === "not-connected" || inspection.status === "key-lacks-jev"
1791
1791
  ? "failproofai config --token <key>"
1792
1792
  : "failproofai jev setup --provider <kind> --key-stdin";