failproofai 1.0.12-beta.0 → 1.0.12-beta.1

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 (195) 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 +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  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 +3 -3
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  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 +22 -22
  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 +1 -1
  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 +3 -3
  36. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  37. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  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 +4 -4
  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.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  54. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  57. package/.next/standalone/.next/server/app/settings/page.js +7 -7
  58. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/chunks/{[externals]__0-ha5ydqq0mws._.js → [externals]__0vt9i6fqinjne._.js} +1 -1
  61. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20xe05hm5u8_1._.js → [root-of-the-server]__0f_4g-vazrvxv._.js} +1 -1
  62. package/.next/standalone/.next/server/chunks/_0hf5q4xzyg40j._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/_19011-u44143j._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/{_0kc-piux3hady._.js → _1y590mjzpkbos._.js} +2 -2
  68. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +5 -0
  70. package/.next/standalone/.next/server/chunks/src_hooks_builtin-policies_ts_1yq5ybgx1v98e._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3h94wnn._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/src_hooks_daemon-client_ts_0il_yu1zc0291._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn_4twyh._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +3 -0
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vcyntk4snf0f._.js → [root-of-the-server]__0b78b0j8iyv6l._.js} +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1_xbg2kx30wp-._.js → [root-of-the-server]__0naevg4bcoh54._.js} +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifpkowwub._.js → _1a3tubds7s7ow._.js} +10 -9
  90. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_00jxj8foxqz5t.js → node_modules_html-to-image_es_index_1i8oid1gc7s1f.js} +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9ddabn3_._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +5 -0
  101. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +5 -0
  102. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4l1tzrm._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/src_hooks_daemon-client_ts_0k41_i7jg6ndv._.js +1 -1
  104. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  105. package/.next/standalone/.next/server/pages/404.html +1 -1
  106. package/.next/standalone/.next/server/pages/500.html +1 -1
  107. package/.next/standalone/.next/server/preview-props.json +3 -3
  108. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  109. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  110. package/.next/standalone/.next/static/chunks/{3tn1bzk094e8e.js → 0_zfad4co-t2z.js} +1 -1
  111. package/.next/standalone/.next/static/chunks/0jutamwqz8pg5.js +1 -0
  112. package/.next/standalone/.next/static/chunks/{2meqb9kygrncb.js → 114hbolvrv94e.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/{2znt1784j9gd9.js → 18433qz-8njv3.js} +1 -1
  114. package/.next/standalone/.next/static/chunks/1b32-afqacaen.js +1 -0
  115. package/.next/standalone/.next/static/chunks/{13jv-xvabeb62.js → 1p-hsi58i-6h0.js} +1 -1
  116. package/.next/standalone/.next/static/chunks/{0pjxnp927y7_j.js → 2-tf6_7u623kd.js} +1 -1
  117. package/.next/standalone/.next/static/chunks/{2qz5piiir-qqm.js → 24w3x2da9sk49.js} +1 -1
  118. package/.next/standalone/.next/static/chunks/{0a-ccsp3xmypz.js → 3-pku9qwv1yfu.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/42e8zab314654.js +1 -0
  120. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +41 -0
  121. package/.next/standalone/fp-cloud-cli/README.md +42 -1
  122. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +117 -12
  123. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +285 -19
  124. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +84 -18
  125. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +170 -6
  126. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +89 -6
  127. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +259 -21
  128. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +8 -5
  129. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +2 -2
  130. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +17 -9
  131. package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +692 -0
  132. package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +46 -0
  133. package/.next/standalone/fp-cloud-cli/tests/test_output.py +23 -0
  134. package/.next/standalone/hermes-plugin/README.md +6 -4
  135. package/.next/standalone/hermes-plugin/__init__.py +16 -0
  136. package/.next/standalone/hermes-plugin/client.py +5 -1
  137. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  138. package/.next/standalone/package.json +9 -9
  139. package/.next/standalone/server.js +1 -1
  140. package/bin/failproofai.mjs +17 -1
  141. package/dist/cli.mjs +2930 -1747
  142. package/dist/worker.mjs +1450 -145
  143. package/hermes-plugin/README.md +6 -4
  144. package/hermes-plugin/__init__.py +16 -0
  145. package/hermes-plugin/client.py +5 -1
  146. package/hermes-plugin/plugin.yaml +1 -1
  147. package/package.json +9 -9
  148. package/src/audit/open-browser.ts +1 -19
  149. package/src/hooks/agent-roster.ts +172 -0
  150. package/src/hooks/agent-targets.ts +60 -0
  151. package/src/hooks/cloud-connection.ts +97 -27
  152. package/src/hooks/cloud-enrollment-cli.ts +7 -4
  153. package/src/hooks/cloud-introspect.ts +2 -5
  154. package/src/hooks/cloud-managed-policies.ts +380 -20
  155. package/src/hooks/cloud-policy-errors.ts +481 -0
  156. package/src/hooks/config-status.ts +1 -1
  157. package/src/hooks/configure-wizard.ts +15 -311
  158. package/src/hooks/custom-hooks-loader.ts +26 -3
  159. package/src/hooks/daemon-client.ts +9 -4
  160. package/src/hooks/daemon-service.ts +2 -1
  161. package/src/hooks/effective-reviewers.ts +22 -1
  162. package/src/hooks/fp-home.ts +6 -0
  163. package/src/hooks/handler.ts +235 -18
  164. package/src/hooks/health-probe.ts +15 -0
  165. package/src/hooks/integrations.ts +18 -11
  166. package/src/hooks/jev-cli.ts +197 -0
  167. package/src/hooks/jev-cloud-connection.ts +92 -0
  168. package/src/hooks/manager.ts +122 -16
  169. package/src/hooks/pack-failclosed.ts +2 -0
  170. package/src/hooks/pack-manifest.ts +8 -1
  171. package/src/hooks/policy-evaluator.ts +5 -1
  172. package/src/hooks/policy-registry.ts +3 -2
  173. package/src/hooks/policy-reviewability.ts +21 -5
  174. package/src/hooks/semantic/cloud-jev-health.ts +227 -0
  175. package/src/hooks/semantic/cloud-jev.ts +677 -0
  176. package/src/hooks/semantic/jev-client.ts +18 -1
  177. package/src/hooks/semantic/jev-config.ts +173 -3
  178. package/src/hooks/semantic/jev-review.ts +129 -10
  179. package/src/hooks/semantic/pack-policies.ts +2 -1
  180. package/src/hooks/semantic/types.ts +8 -0
  181. package/src/hooks/session-pause-cli.ts +1 -1
  182. package/src/hooks/tui.ts +0 -271
  183. package/src/hooks/worker-server.ts +5 -1
  184. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0uhs2t1b._.js +0 -5
  185. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7cyqlb2._.js +0 -3
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ts0fg5yih1c1._.js +0 -3
  187. package/.next/standalone/.next/server/chunks/ssr/_1ve8y151ua7qx._.js +0 -3
  188. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo55j16x._.js +0 -5
  189. package/.next/standalone/.next/static/chunks/2ank2u3apy8m6.js +0 -1
  190. package/.next/standalone/.next/static/chunks/2ohktp_-lkv3x.js +0 -1
  191. package/.next/standalone/.next/static/chunks/2py0eldhwz8ay.js +0 -1
  192. package/src/hooks/cloud-login.ts +0 -245
  193. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → xq8UgF5yFxty1VWxY2hyn}/_buildManifest.js +0 -0
  194. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → xq8UgF5yFxty1VWxY2hyn}/_clientMiddlewareManifest.js +0 -0
  195. /package/.next/standalone/.next/static/{SNr7o_-G4kNBQTR-iLZMM → xq8UgF5yFxty1VWxY2hyn}/_ssgManifest.js +0 -0
@@ -30,26 +30,20 @@ import { dirname, resolve, sep } from "node:path";
30
30
  import { CORE_SOURCE } from "./pack-store";
31
31
 
32
32
  import {
33
- BACK,
34
33
  collapseAfter,
35
34
  colorsEnabled,
36
35
  multiSelect,
37
36
  OPEN_SOURCE,
38
37
  optsFor,
39
38
  paint,
40
- promptBrowserLogin,
41
39
  promptCloudKey,
42
40
  screenKit,
43
- selectOne,
44
41
  type CloudKeyAnswer,
45
42
  type MultiChoice,
46
43
  type OpenSource,
47
- type SelectChoice,
48
44
  type TTYIn,
49
45
  type TTYOut,
50
46
  } from "./tui";
51
- import { claimLogin, machineOs, pollLogin, startLogin, type LoginOrg } from "./cloud-login";
52
- import { canOpenBrowser, openUrl } from "../audit/open-browser";
53
47
  import {
54
48
  DEFAULT_INGEST_URL,
55
49
  readIngestCredential,
@@ -1004,190 +998,6 @@ export async function runConfigureWizard(
1004
998
  stdout,
1005
999
  });
1006
1000
 
1007
- // A key typed after picking "Paste an API key" from the menu. Same field as
1008
- // above minus the saved key, which the menu already offered as "keep"; Tab is
1009
- // the way back to the menu, so it never means open source here.
1010
- const askKey = (): Promise<CloudKeyAnswer | OpenSource | null> =>
1011
- promptCloudKey({
1012
- message: "Connect to cloud",
1013
- meta: source ? `${host} · from ${source}` : host,
1014
- tabHint: "go back",
1015
- emptyError: "Paste a key, or press tab to go back.",
1016
- check: async (key) => {
1017
- if (!url) return { ok: false, line: badUrl ?? "No usable endpoint." };
1018
- const r = await checkCloudKey(url, key);
1019
- if (r.ok) {
1020
- typedOrg = r.org;
1021
- return { ok: true, line: r.note };
1022
- }
1023
- return { ok: false, line: r.refused ? `That key was refused: ${r.reason}.` : `Couldn't check the key: ${r.reason}.` };
1024
- },
1025
- collapsed: (answer) => (answer === OPEN_SOURCE ? [] : [...cloudLines(planFor(answer)), ""]),
1026
- stdin,
1027
- stdout,
1028
- });
1029
-
1030
- // The browser login. Resolves the approval, BACK for the menu of other
1031
- // options (Tab / Esc, or a cloud that cannot do this), or null when the person
1032
- // cancelled the whole run.
1033
- type Approval = Extract<CloudPlan, { kind: "login" }>;
1034
- // Set once the cloud says it has no browser login; the menu then drops it.
1035
- let loginUnsupported = false;
1036
- const askLogin = (endpoint: string): Promise<{ value: Approval } | typeof BACK | null> => {
1037
- let pollSecret = "";
1038
- return promptBrowserLogin<Approval>({
1039
- message: "Connect to cloud",
1040
- meta: source ? `${host} · from ${source}` : host,
1041
- machineLabel,
1042
- start: async () => {
1043
- const r = await startLogin(endpoint, { label: machineLabel, os: machineOs(), cliVersion });
1044
- if (r.kind === "ok") {
1045
- pollSecret = r.pollSecret;
1046
- void emit("configure_cloud_login", { step: "started" });
1047
- return { ok: true, url: r.verificationUrl, expiresIn: r.expiresIn, interval: r.interval };
1048
- }
1049
- if (r.kind === "unsupported") {
1050
- void emit("configure_cloud_login", { step: "unsupported" });
1051
- loginUnsupported = true;
1052
- return { ok: false, retryable: false, line: `${host} doesn't support logging in from here yet.` };
1053
- }
1054
- return {
1055
- ok: false,
1056
- retryable: true,
1057
- line: r.kind === "refused" ? `Couldn't start the login: ${r.reason}.` : `Couldn't reach ${host}: ${r.reason}.`,
1058
- };
1059
- },
1060
- poll: async () => {
1061
- const p = await pollLogin(endpoint, pollSecret);
1062
- switch (p.kind) {
1063
- case "approved":
1064
- void emit("configure_cloud_login", { step: "approved" });
1065
- return {
1066
- state: "approved",
1067
- value: {
1068
- kind: "login",
1069
- pollSecret,
1070
- org: p.org,
1071
- approvedBy: p.approvedBy,
1072
- keyName: p.keyName,
1073
- permissions: p.permissions,
1074
- },
1075
- };
1076
- case "pending":
1077
- return { state: "pending", interval: p.interval };
1078
- case "slow_down":
1079
- return { state: "slow_down" };
1080
- case "denied":
1081
- void emit("configure_cloud_login", { step: "denied" });
1082
- return { state: "denied" };
1083
- case "unreachable":
1084
- return { state: "unreachable", line: p.reason };
1085
- // Expired, already claimed, or swept: in every case this link is done.
1086
- default:
1087
- return { state: "expired" };
1088
- }
1089
- },
1090
- open: (link) => {
1091
- if (!canOpenBrowser()) return false;
1092
- openUrl(link);
1093
- return true;
1094
- },
1095
- collapsed: (plan) => [...cloudLines(plan), ""],
1096
- stdin,
1097
- stdout,
1098
- });
1099
- };
1100
-
1101
- // The interactive CONNECT step. A machine with no connection, or whose saved
1102
- // key is refused, starts the browser login straight away — Tab or Esc there
1103
- // opens the other options. A machine that is connected with a working key gets
1104
- // the menu, "keep" first, so re-running config never opens a browser by itself.
1105
- const chooseConnection = async (): Promise<CloudPlan | null> => {
1106
- const verdict =
1107
- saved && !saved.splitKeys
1108
- ? await transient("Checking your saved key…", () => checkCloudKey(saved.url, saved.token))
1109
- : null;
1110
- const savedBroken = verdict !== null && !verdict.ok && verdict.refused;
1111
- const keptPlan: CloudPlan =
1112
- verdict && verdict.ok
1113
- ? { kind: "kept", how: "saved", verified: true }
1114
- : verdict && !verdict.refused
1115
- ? { kind: "kept", how: "saved", verified: false }
1116
- : { kind: "kept", how: "skip", verified: false };
1117
- const keepHint = !saved
1118
- ? ""
1119
- : saved.splitKeys
1120
- ? "leave both keys as they are"
1121
- : verdict?.ok
1122
- ? `${verdict.org ?? saved.org ?? hostOf(saved.url)} · key ${maskToken(saved.token)}`
1123
- : savedBroken
1124
- ? "its key no longer works"
1125
- : "its key couldn't be checked just now";
1126
-
1127
- type Pick = "login" | "key" | "oss" | "keep";
1128
- const login: SelectChoice<Pick> = {
1129
- label: saved ? "Log in again" : "Log in to FailproofAI Cloud",
1130
- value: "login",
1131
- hint: savedBroken
1132
- ? "recommended · your saved key no longer works"
1133
- : saved
1134
- ? "opens your browser and makes a new key"
1135
- : "recommended · opens your browser",
1136
- };
1137
- const key: SelectChoice<Pick> = { label: "Paste an API key", value: "key", hint: "one you already have" };
1138
- const oss: SelectChoice<Pick> = { label: "Use open source", value: "oss", hint: "skip the cloud" };
1139
- const keep: SelectChoice<Pick> = { label: "Keep current connection", value: "keep", hint: keepHint };
1140
- // No initial-cursor option on selectOne: the default is whatever is first.
1141
- const choices = !saved ? [login, key, oss] : savedBroken ? [login, key, keep] : [keep, login, key];
1142
- const leftLogin = (result: Awaited<ReturnType<typeof askLogin>>): CloudPlan | null | typeof BACK => {
1143
- if (result === null || result === BACK) {
1144
- if (result === BACK && loginUnsupported) {
1145
- write([kit.caution(`${host} doesn't support logging in from here yet. Paste an API key instead.`), ""]);
1146
- }
1147
- return result;
1148
- }
1149
- return result.value;
1150
- };
1151
-
1152
- if (url && (!saved || savedBroken)) {
1153
- if (savedBroken && verdict && !verdict.ok) {
1154
- write([kit.caution(`Your saved key no longer works: ${verdict.reason}. Logging in again.`)]);
1155
- }
1156
- void emit("configure_cloud_choice", { choice: "login", connected: Boolean(saved), auto: true });
1157
- const first = leftLogin(await askLogin(url));
1158
- if (first !== BACK) return first;
1159
- }
1160
-
1161
- for (;;) {
1162
- const pick = await selectOne<Pick>({
1163
- message: "Connect to cloud",
1164
- meta: source ? `${host} · from ${source}` : host,
1165
- choices: loginUnsupported ? choices.filter((c) => c.value !== "login") : choices,
1166
- collapsed: (p) =>
1167
- p === "keep" ? [...cloudLines(keptPlan), ""] : p === "oss" ? [...cloudLines({ kind: "oss" }), ""] : [],
1168
- stdin,
1169
- stdout,
1170
- });
1171
- if (pick === null) return null;
1172
- void emit("configure_cloud_choice", { choice: pick, connected: Boolean(saved) });
1173
- if (pick === "keep") return keptPlan;
1174
- if (pick === "oss") return { kind: "oss" };
1175
- if (pick === "key") {
1176
- const answer = await askKey();
1177
- if (answer === null) return null;
1178
- if (answer === OPEN_SOURCE) continue;
1179
- return planFor(answer);
1180
- }
1181
- if (!url) {
1182
- write([kit.fail(badUrl ?? "No usable endpoint."), ""]);
1183
- continue;
1184
- }
1185
- const result = leftLogin(await askLogin(url));
1186
- if (result === BACK) continue;
1187
- return result;
1188
- }
1189
- };
1190
-
1191
1001
  let cloud: CloudPlan;
1192
1002
  if (answers.oss) {
1193
1003
  cloud = saved ? { kind: "kept", how: "skip", verified: false } : { kind: "oss" };
@@ -1222,12 +1032,11 @@ export async function runConfigureWizard(
1222
1032
  cloud = saved ? { kind: "kept", how: "skip", verified: false } : { kind: "oss" };
1223
1033
  write([...cloudLines(cloud), ""]);
1224
1034
  } else {
1225
- // Said above the menu, which stays usable for keeping a saved key and for
1226
- // open source, neither of which sends anything anywhere.
1035
+ // Said above the field, which stays usable for Tab and a saved key.
1227
1036
  if (badUrl) write([kit.fail(badUrl)]);
1228
- const chosen = await chooseConnection();
1229
- if (chosen === null) return cancel();
1230
- cloud = chosen;
1037
+ const answer = await askCloud();
1038
+ if (answer === null) return cancel();
1039
+ cloud = planFor(answer);
1231
1040
  }
1232
1041
 
1233
1042
  // ── AGENTS ──────────────────────────────────────────────────────────────────
@@ -1240,10 +1049,8 @@ export async function runConfigureWizard(
1240
1049
  // Whether the daemon will be collecting once this run ends: a key is already
1241
1050
  // on disk, or this run writes one. Removal copy and the backfill request both
1242
1051
  // depend on it.
1243
- // A browser login connects exactly like a pasted key, one step later.
1244
- const connecting = cloud.kind === "connect" || cloud.kind === "login";
1245
- const collectingAfter = connecting || readIngestCredential() !== null;
1246
- const sessionsOn = connecting ? answers.noTranscripts !== true : fp.collector.sessions === true;
1052
+ const collectingAfter = cloud.kind === "connect" || readIngestCredential() !== null;
1053
+ const sessionsOn = cloud.kind === "connect" ? answers.noTranscripts !== true : fp.collector.sessions === true;
1247
1054
 
1248
1055
  let selected: IntegrationType[];
1249
1056
  if (answers.agents && answers.agents.length > 0) {
@@ -1298,37 +1105,6 @@ export async function runConfigureWizard(
1298
1105
  // 5. The connection last.
1299
1106
  const done: string[] = [];
1300
1107
  let daemonInstalled = daemonAlreadyRunning;
1301
- // The `sudo -v` at the top cached root for sudo's timeout — five minutes by
1302
- // default on macOS, fifteen on most Linux. A browser login, or simply a slow
1303
- // agents screen, can outlast it, and the install below runs `sudo -n`, which
1304
- // would then fail AFTER the person did everything else. Ask once more instead.
1305
- // The stale-unit refresh runs `sudo -n` too, so it is asked for the same way —
1306
- // but, as at the top, losing root there costs the scheduled audit, not setup.
1307
- // Unattended runs never primed anything, so there is nothing to have expired.
1308
- if ((daemonWanted || daemonUnitStale) && !unattended) {
1309
- let stillRoot = false;
1310
- try {
1311
- stillRoot = canElevate();
1312
- } catch {
1313
- stillRoot = false;
1314
- }
1315
- if (!stillRoot && daemonWanted) {
1316
- if (!elevate("Your sudo session expired while you were away. Installing failproofaid needs root again.")) {
1317
- write([
1318
- kit.fail("Couldn't get root, so setup stopped before changing anything."),
1319
- ` Re-run once you can use sudo: ${kit.cmd("failproofai config")}`,
1320
- ]);
1321
- void emit("configure_aborted", { reason: "needs_root" });
1322
- return { applied: false, abort: "needs_root" };
1323
- }
1324
- } else if (
1325
- !stillRoot &&
1326
- !elevate("Your sudo session expired while you were away. Refreshing failproofaid's service definition needs root again.")
1327
- ) {
1328
- done.push(kit.caution("No root, so its service definition stays as it is. Scheduled audits stay off until it is refreshed."));
1329
- daemonUnitStale = false;
1330
- }
1331
- }
1332
1108
  if (daemonWanted) {
1333
1109
  // A unit that is running but cannot answer is torn down before it is
1334
1110
  // rebuilt: it holds the singleton flock the replacement needs, so
@@ -1438,7 +1214,7 @@ export async function runConfigureWizard(
1438
1214
  pruneOldDaemonBinaries();
1439
1215
  }
1440
1216
 
1441
- const connectedAfterPlan = connecting || cloud.kind === "kept";
1217
+ const connectedAfterPlan = cloud.kind === "connect" || cloud.kind === "kept";
1442
1218
  // Telemetry runs concurrently with the rest of apply (never rejects,
1443
1219
  // 5s-bounded) and is awaited before returning. `cli` carries the selected
1444
1220
  // agent ids, as it always has (and as hooks_installed does): a fixed list of
@@ -1449,7 +1225,7 @@ export async function runConfigureWizard(
1449
1225
  cli: selected,
1450
1226
  cli_count: selected.length,
1451
1227
  policy_count: (readScopedHooksConfig("user", cwd).enabledPolicies ?? []).length,
1452
- connected: connecting,
1228
+ connected: cloud.kind === "connect",
1453
1229
  mode: connectedAfterPlan ? "cloud" : "oss",
1454
1230
  agents_selected: selected.length,
1455
1231
  agents_detected: plan.detected.size,
@@ -1497,36 +1273,8 @@ export async function runConfigureWizard(
1497
1273
  // a key on disk doing nothing. `connectToCloud` re-verifies each capability
1498
1274
  // and writes only what works.
1499
1275
  let connected = false;
1500
- // A browser login has an approval, not a key: claim it NOW, with the daemon
1501
- // in, so a run that stopped earlier left nothing minted anywhere. The claim
1502
- // is single-use — the server mints the key and hands it over exactly once.
1503
- let token: string | null = cloud.kind === "connect" ? cloud.token : null;
1504
- let orgLabel: string | undefined = cloud.kind === "connect" ? cloud.org : undefined;
1505
- let mintedName: string | undefined;
1506
- if (cloud.kind === "login" && url) {
1507
- const claimed = await transient("Creating this machine's key…", () => claimLogin(url, cloud.pollSecret));
1508
- void emit("configure_cloud_login", { step: "claimed", ok: claimed.kind === "ok", reason: claimed.kind });
1509
- if (claimed.kind === "ok") {
1510
- token = claimed.key;
1511
- orgLabel = loginOrgLabel(claimed.org);
1512
- mintedName = claimed.keyName;
1513
- } else {
1514
- hookLogWarn(`cloud login could not be completed: ${claimed.kind}`);
1515
- done.push(
1516
- kit.caution(
1517
- claimed.kind === "expired"
1518
- ? "The login took too long to finish, so no key was created. Everything else was applied."
1519
- : claimed.kind === "revoked"
1520
- ? "Whoever approved the login can no longer create keys in that org, so no key was created."
1521
- : claimed.kind === "unreachable"
1522
- ? `Couldn't reach ${host} to create the key (${claimed.reason}); everything else was applied.`
1523
- : `The key couldn't be created: ${claimed.reason}. Everything else was applied.`,
1524
- "failproofai config",
1525
- ),
1526
- );
1527
- }
1528
- }
1529
- if (token && url) {
1276
+ if (cloud.kind === "connect" && url) {
1277
+ const token = cloud.token;
1530
1278
  try {
1531
1279
  const outcome = await transient("Connecting…", () =>
1532
1280
  connectToCloud({
@@ -1540,12 +1288,7 @@ export async function runConfigureWizard(
1540
1288
  );
1541
1289
  connected = outcome.anyConfigured;
1542
1290
  done.push(...(outcome.changes ?? []));
1543
- done.push(...connectDoneLines(kit, outcome, host, orgLabel));
1544
- // Logging in again makes a NEW key; the one this machine held keeps
1545
- // working until someone revokes it. Said here, where it is still useful.
1546
- if (cloud.kind === "login" && saved && connected) {
1547
- done.push(` Your previous key for this machine still works until you revoke it on ${host}'s keys page.`);
1548
- }
1291
+ done.push(...connectDoneLines(kit, outcome, host, cloud.org));
1549
1292
  if (outcome.ingest.ok && answers.noTranscripts === true) {
1550
1293
  done.push(" Decisions only: session transcripts are not sent (--no-transcripts).");
1551
1294
  }
@@ -1555,22 +1298,12 @@ export async function runConfigureWizard(
1555
1298
  hookLogWarn(`cloud connection was not written: ${err instanceof Error ? err.message : String(err)}`);
1556
1299
  done.push(kit.caution("Couldn't connect; everything else was applied.", "failproofai config"));
1557
1300
  }
1558
- // The claim spent the approval and the key exists, but nothing here uses
1559
- // it: say which one, so it can be revoked rather than left live.
1560
- if (cloud.kind === "login" && !connected) {
1561
- done.push(` The key ${mintedName ? `"${mintedName}" ` : ""}was created but isn't in use here; revoke it on ${host}'s keys page.`);
1562
- }
1563
1301
  } else if (cloud.kind === "kept" && cloud.how === "saved" && fp.mode !== "cloud") {
1564
1302
  // A home migrated from layout 1 carries keys but no mode, and would read
1565
1303
  // as open source everywhere that asks.
1566
1304
  updateConfig({ mode: "cloud" });
1567
1305
  }
1568
1306
 
1569
- // Logging in again on a connected machine, with no key at the end of it,
1570
- // changed nothing about the connection: the previous one is still there.
1571
- const keptOld = cloud.kind === "login" && !token && saved !== undefined;
1572
- if (keptOld) done.push(" This machine keeps its previous connection.");
1573
-
1574
1307
  await applied;
1575
1308
  // Only now — a completed apply — is the launcher considered "seen", so
1576
1309
  // first-run onboarding stops offering itself on every command.
@@ -1588,7 +1321,7 @@ export async function runConfigureWizard(
1588
1321
  done,
1589
1322
  cwd,
1590
1323
  agents: selected.length,
1591
- connected: connected || cloud.kind === "kept" || keptOld,
1324
+ connected: connected || cloud.kind === "kept",
1592
1325
  codexTrust,
1593
1326
  }),
1594
1327
  );
@@ -1601,7 +1334,7 @@ export async function runConfigureWizard(
1601
1334
  policies,
1602
1335
  daemonInstalled,
1603
1336
  connected,
1604
- mode: connected || cloud.kind === "kept" || keptOld ? "cloud" : "oss",
1337
+ mode: connected || cloud.kind === "kept" ? "cloud" : "oss",
1605
1338
  agents: { selected, added, removed },
1606
1339
  };
1607
1340
  }
@@ -1612,19 +1345,7 @@ export async function runConfigureWizard(
1612
1345
  type CloudPlan =
1613
1346
  | { kind: "oss" }
1614
1347
  | { kind: "kept"; how: "skip" | "saved"; verified: boolean }
1615
- | { kind: "connect"; token: string; org?: string; note?: string }
1616
- /**
1617
- * Approved in the browser, not yet claimed. The key does not exist until
1618
- * apply claims it — after the daemon is in — so a run that stops before then
1619
- * leaves no key behind anywhere.
1620
- */
1621
- | { kind: "login"; pollSecret: string; org: LoginOrg; approvedBy?: string; keyName?: string; permissions: string[] };
1622
-
1623
- /** "Acme (acme)", or whichever half the server sent. */
1624
- function loginOrgLabel(org: LoginOrg): string {
1625
- if (org.name && org.slug) return `${org.name} (${org.slug})`;
1626
- return org.name ?? org.slug ?? "your org";
1627
- }
1348
+ | { kind: "connect"; token: string; org?: string; note?: string };
1628
1349
 
1629
1350
  /**
1630
1351
  * The connection already on this machine, from either credential: the policy
@@ -1673,15 +1394,7 @@ export function resolveCloudUrl(
1673
1394
  const env = process.env.FAILPROOFAI_CLOUD_URL?.trim();
1674
1395
  const fromFlag = flag?.trim();
1675
1396
  const override = fromFlag || env;
1676
- if (!override) {
1677
- if (!existing) return { ok: true, url: cloudBaseFor(DEFAULT_INGEST_URL) };
1678
- // Checked like a typed one: login sends its secret and receives a key here
1679
- // before anything else would, and the file it came from can be edited.
1680
- const saved = validateCloudUrl(existing);
1681
- return saved.ok
1682
- ? { ok: true, url: existing }
1683
- : { ok: false, message: `the saved cloud address "${existing}" cannot be used: ${saved.reason}` };
1684
- }
1397
+ if (!override) return { ok: true, url: existing ?? cloudBaseFor(DEFAULT_INGEST_URL) };
1685
1398
  const source = fromFlag && fromFlag !== env ? "--url" : "FAILPROOFAI_CLOUD_URL";
1686
1399
  const validated = validateCloudUrl(cloudBaseFor(override));
1687
1400
  if (!validated.ok) {
@@ -1764,15 +1477,6 @@ export function cloudCollapsedLines(
1764
1477
  kit.ok(plan.org ? `Key accepted for ${plan.org} on ${at.host}` : `Key accepted by ${at.host}`),
1765
1478
  ...(plan.note ? [kit.caution(plan.note)] : []),
1766
1479
  ];
1767
- case "login":
1768
- // Who approved it is said out loud: a link someone else approved into
1769
- // THEIR org would otherwise connect this machine there unnoticed.
1770
- return [
1771
- head,
1772
- kit.ok(
1773
- `Logged in${plan.approvedBy ? ` as ${plan.approvedBy}` : ""} · ${loginOrgLabel(plan.org)} on ${at.host}`,
1774
- ),
1775
- ];
1776
1480
  }
1777
1481
  }
1778
1482
 
@@ -176,7 +176,7 @@ async function importWithDeadline(fileUrl: string): Promise<void> {
176
176
  * Does NOT clear the registry — caller is responsible for that.
177
177
  */
178
178
  export interface PolicyLoadFailure {
179
- type: "module_not_found" | "syntax_error" | "load_timeout" | "runtime_error" | "path_missing";
179
+ type: "module_not_found" | "file_not_found" | "syntax_error" | "load_timeout" | "runtime_error" | "path_missing";
180
180
  reason: string;
181
181
  }
182
182
 
@@ -250,8 +250,13 @@ async function loadSingleFile(
250
250
  return null;
251
251
  } catch (err) {
252
252
  const msg = err instanceof Error ? err.message : String(err);
253
- const errorType = /Cannot find module|MODULE_NOT_FOUND|ENOENT/i.test(msg)
253
+ // An import that names no module, apart from a file the policy itself
254
+ // reads at load (a `readFileSync` of its data): both are ENOENT underneath,
255
+ // but "module not found" sent people looking for a missing package (B13).
256
+ const errorType = /Cannot find module|MODULE_NOT_FOUND|Cannot find package/i.test(msg)
254
257
  ? "module_not_found"
258
+ : /ENOENT/i.test(msg)
259
+ ? "file_not_found"
255
260
  : /SyntaxError|Unexpected token/i.test(msg)
256
261
  ? "syntax_error"
257
262
  : /timed out loading module/i.test(msg)
@@ -334,6 +339,13 @@ export interface LoadAllResult {
334
339
  * Only present for a path more than one pack resolved to.
335
340
  */
336
341
  packAliases: Map<string, string[]>;
342
+ /**
343
+ * Import failures of FailproofAI Cloud JS policies, keyed by Cloud policy id
344
+ * (every id behind a shared artifact). Fail-open like the rest of the Cloud
345
+ * read — nothing denies over them — but reported to Cloud in `errors.json`,
346
+ * so a deployment that is not actually enforcing says so.
347
+ */
348
+ cloudFailures?: Map<string, PolicyLoadFailure>;
337
349
  }
338
350
 
339
351
  export function customPolicyId(file: string, name: string): string {
@@ -456,8 +468,12 @@ export async function loadAllCustomHooks(
456
468
  // codebase exists to remove. It is also announced, so an operator can give the
457
469
  // two policies distinguishable source instead of living with the merge.
458
470
  const cloudManagedByPath = new Map<string, CloudManagedPolicyArtifact>();
471
+ /** Every Cloud policy id behind each artifact path, for `cloudFailures`. */
472
+ const cloudIdsByPath = new Map<string, string[]>();
473
+ const cloudFailures = new Map<string, PolicyLoadFailure>();
459
474
  for (const policy of opts?.cloudManagedPolicies ?? []) {
460
475
  const key = resolve(policy.path);
476
+ cloudIdsByPath.set(key, [...(cloudIdsByPath.get(key) ?? []), policy.id]);
461
477
  const existing = cloudManagedByPath.get(key);
462
478
  if (!existing) {
463
479
  cloudManagedByPath.set(key, policy);
@@ -660,6 +676,9 @@ export async function loadAllCustomHooks(
660
676
  if (failure && pack) {
661
677
  for (const id of packIdsByPath.get(absPath) ?? [pack.id]) packFailures.set(id, failure);
662
678
  }
679
+ if (failure && cloudManaged) {
680
+ for (const id of cloudIdsByPath.get(absPath) ?? [cloudManaged.id]) cloudFailures.set(id, failure);
681
+ }
663
682
  for (const hook of getCustomHooks().slice(hooksBefore)) {
664
683
  const tagged = hook as CustomHook & {
665
684
  __policyId?: string;
@@ -680,6 +699,10 @@ export async function loadAllCustomHooks(
680
699
  }
681
700
  } else {
682
701
  hookLogWarn(`custom policy path not found: ${absPath}`);
702
+ if (cloudManaged) {
703
+ const missing = { type: "path_missing", reason: `path missing: ${absPath}` } as const;
704
+ for (const id of cloudIdsByPath.get(absPath) ?? [cloudManaged.id]) cloudFailures.set(id, missing);
705
+ }
683
706
  if (pack) {
684
707
  const missing = { type: "path_missing", reason: `path missing: ${absPath}` } as const;
685
708
  for (const id of packIdsByPath.get(absPath) ?? [pack.id]) packFailures.set(id, missing);
@@ -830,5 +853,5 @@ export async function loadAllCustomHooks(
830
853
  const winner = packByPath.get(path);
831
854
  if (winner && ids.length > 1) packAliases.set(winner.id, ids);
832
855
  }
833
- return { hooks: allHooks, conventionSources, packFailures, packAliases };
856
+ return { hooks: allHooks, conventionSources, packFailures, packAliases, cloudFailures };
834
857
  }
@@ -16,7 +16,7 @@ import { existsSync } from "node:fs";
16
16
  import { daemonSocket as daemonSocketPath } from "./fp-home";
17
17
  import { readConfig } from "./fp-config";
18
18
 
19
- const PROTOCOL_VERSION = 1;
19
+ const PROTOCOL_VERSION = 2;
20
20
 
21
21
  /**
22
22
  * Reaching the daemon and getting an answer out of it are two different
@@ -56,6 +56,8 @@ export interface DaemonHookRequest {
56
56
  * hazard.
57
57
  */
58
58
  cwd?: string;
59
+ /** Settings path of the originating agent profile, not the daemon's. */
60
+ agentSettingsPath?: string;
59
61
  }
60
62
 
61
63
  export interface DaemonHookResponse {
@@ -98,6 +100,7 @@ export interface DaemonPolicyEvaluationRequest {
98
100
  event: string;
99
101
  payload: Record<string, unknown>;
100
102
  cwd?: string;
103
+ agentSettingsPath?: string;
101
104
  }
102
105
 
103
106
  export type DaemonPolicyEvaluationAttempt =
@@ -304,6 +307,7 @@ export async function attemptDaemonHook(
304
307
  cli: req.cli,
305
308
  stdin: req.stdin,
306
309
  cwd: req.cwd,
310
+ agentSettingsPath: req.agentSettingsPath,
307
311
  },
308
312
  opts,
309
313
  );
@@ -338,9 +342,9 @@ function isStringArray(value: unknown): value is string[] {
338
342
 
339
343
  /**
340
344
  * Attempts the structured request used by native in-process integrations.
341
- * A matching protocol version is not sufficient: pre-native v1 daemons speak
342
- * `hook` but cannot deserialize `policyEvaluation`, so only a complete,
343
- * well-shaped `policyResult` proves this capability exists.
345
+ * A matching protocol version is not sufficient: a broken or partial daemon
346
+ * may speak `hook` but not `policyEvaluation`, so only a complete, well-shaped
347
+ * `policyResult` proves this capability exists.
344
348
  */
345
349
  export async function attemptDaemonPolicyEvaluation(
346
350
  req: DaemonPolicyEvaluationRequest,
@@ -354,6 +358,7 @@ export async function attemptDaemonPolicyEvaluation(
354
358
  event: req.event,
355
359
  payload: req.payload,
356
360
  cwd: req.cwd,
361
+ agentSettingsPath: req.agentSettingsPath,
357
362
  },
358
363
  opts,
359
364
  );
@@ -7,6 +7,7 @@
7
7
  * `configure-wizard.ts` calls the functions here directly, the same
8
8
  * relationship it already has with `manager.ts`'s `installHooks()`.
9
9
  */
10
+ import { HEALTH_PROBE_SOURCE } from "./health-probe";
10
11
  import {
11
12
  existsSync,
12
13
  mkdirSync,
@@ -1030,7 +1031,7 @@ export async function probeDaemon(): Promise<DaemonProbe> {
1030
1031
  cli: "claude",
1031
1032
  stdin: JSON.stringify({
1032
1033
  hook_event_name: "SessionStart",
1033
- source: "failproofai-health-probe",
1034
+ source: HEALTH_PROBE_SOURCE,
1034
1035
  }),
1035
1036
  },
1036
1037
  { responseTimeoutMs: DAEMON_PROBE_TIMEOUT_MS },
@@ -12,6 +12,15 @@
12
12
  * never installed its checks: `jevChecksInstalled` below is the hook path's
13
13
  * gate for starting a review at all.
14
14
  *
15
+ * ## …and a FailproofAI Cloud `both` policy's own Cloud checks
16
+ *
17
+ * A `both` policy's JS half names its own Jev checks, which run on FailproofAI
18
+ * Cloud and never reach this machine (CONTRACT C10). While Cloud's Jev mode
19
+ * asks (`observe`/`enforce`), those names are reviewers too — but only as
20
+ * `cloud:<policyId>/<name>` (`cloudReviewerName`), which the Cloud review files
21
+ * that policy's own Cloud outcomes under. An installed pack's check of the same
22
+ * name is a different reviewer and can never clear it.
23
+ *
15
24
  * The names come from the MANIFEST, which is already where `authority` and
16
25
  * `reviewedBy` themselves are read from, so this adds no new trust and imports
17
26
  * no semantic module.
@@ -36,11 +45,14 @@
36
45
  */
37
46
  import { readInstalledPacks, type ResolvedPack } from "./pack-manifest";
38
47
  import { NO_REVIEWERS, SEMANTIC_REVIEWER_NAMES } from "./policy-authority";
48
+ import { cloudReviewerNames, readCloudAuthorityInputs, readCloudJevMode } from "./cloud-managed-policies";
49
+ import type { AgentIdentity } from "./agent-targets";
39
50
 
40
51
  let cached: ReadonlySet<string> | null = null;
41
52
  let cachedContested: ReadonlyMap<string, string[]> = new Map();
42
53
  /** The agent the current registration pass is for; see {@link forgetEffectiveReviewerNames}. */
43
54
  let reviewerCli: string | undefined;
55
+ let reviewerAgent: AgentIdentity | null = null;
44
56
 
45
57
  /**
46
58
  * The packs whose Jev checks take part for `cli`, filtered the way the regex
@@ -180,6 +192,14 @@ export function effectiveReviewerNames(): ReadonlySet<string> {
180
192
  } catch {
181
193
  // See above: an unreadable manifest declares no checks.
182
194
  }
195
+ try {
196
+ // Both reads answer "nothing" for a file they cannot use; guarded anyway,
197
+ // because a throw here would cost the registration pass — every policy.
198
+ const cloud = cloudReviewerNames(readCloudAuthorityInputs(reviewerAgent), readCloudJevMode());
199
+ if (cloud.length > 0) names = new Set([...names, ...cloud]);
200
+ } catch {
201
+ // No Cloud reviewers: every `both` policy stays hard.
202
+ }
183
203
  cached = names;
184
204
  return names;
185
205
  }
@@ -244,7 +264,8 @@ export function reviewerNamesFor(
244
264
  * runs before it registers anything — so a pack installed under a long-lived
245
265
  * warm worker is picked up on the next event rather than at the next restart.
246
266
  */
247
- export function forgetEffectiveReviewerNames(cli?: string): void {
267
+ export function forgetEffectiveReviewerNames(cli?: string, agent: AgentIdentity | null = null): void {
248
268
  cached = null;
249
269
  reviewerCli = cli;
270
+ reviewerAgent = agent;
250
271
  }
@@ -165,6 +165,9 @@ export const credentialsFile = (home?: string) => atHome(home, "credentials.json
165
165
  */
166
166
  export const jevConfigFile = (home?: string) => atHome(home, "jev.json");
167
167
 
168
+ /** Owner-only inventory maintained by failproofaid. Paths stay on this machine. */
169
+ export const agentRosterFile = (home?: string) => atHome(home, "agents", "roster.json");
170
+
168
171
  // ── Daemon binaries ──────────────────────────────────────────────────────────
169
172
 
170
173
  export const binDir = (home?: string) => atHome(home, "bin");
@@ -656,6 +659,9 @@ export const HOME_CLASSES: readonly { path: (home?: string) => string; class: Da
656
659
  // history the user was already told about. Kept OUT of `auditSessionFile`
657
660
  // precisely so both survive a sign-out.
658
661
  { path: auditMachineFile, class: "identity" },
662
+ // Profile IDs are stable across restarts and renames. A reset that deleted
663
+ // this file would make Cloud's exact-profile assignments match nobody.
664
+ { path: agentRosterFile, class: "identity" },
659
665
 
660
666
  // ── May be dropped: rebuilt on demand ──
661
667
  // NOTE: `auditDir` itself is deliberately absent. Layout 4 made it MIXED — it