failproofai 1.0.0-beta.12 → 1.0.0-beta.13

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 (173) 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 +2 -3
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js +2 -3
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
  29. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/audit/run/route.js +2 -2
  31. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +3 -2
  34. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
  36. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/logout/route.js +2 -2
  38. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
  40. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
  42. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/audit/page.js +2 -3
  46. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/index.html +1 -1
  49. package/.next/standalone/.next/server/app/index.rsc +15 -15
  50. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  51. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  52. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  53. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  54. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  55. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  56. package/.next/standalone/.next/server/app/page.js +2 -3
  57. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
  60. package/.next/standalone/.next/server/app/policies/page.js +2 -3
  61. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -4
  65. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  68. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  69. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +3 -4
  70. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  73. package/.next/standalone/.next/server/app/projects/page.js +3 -4
  74. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  77. package/.next/standalone/.next/server/app/settings/page.js +2 -3
  78. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  80. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1a7i2q4._.js → [root-of-the-server]__00ni9za._.js} +1 -1
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08y94xv._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ed01_d._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1_ps0jw._.js → [root-of-the-server]__0effm9m._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0lk_935._.js → [root-of-the-server]__0ix8gm7._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0ky_n0r._.js → [root-of-the-server]__0ji11a-._.js} +1 -1
  88. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0v2fpxe._.js → [root-of-the-server]__0ojwjxl._.js} +1 -1
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vb5fl2._.js +3 -0
  90. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ibjy5x._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1uyc-z4._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20d06rh._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -3
  95. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -3
  96. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1rdruw4._.js → [root-of-the-server]__0_1tiya._.js} +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ha3gul._.js → [root-of-the-server]__0eeipso._.js} +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tyf0tc._.js → [root-of-the-server]__17itmx-._.js} +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1io8n-9._.js → [root-of-the-server]__1dx-a-c._.js} +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +5 -0
  103. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1yw368c._.js → [root-of-the-server]__1n2vcv4._.js} +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1a2hcb_._.js → [root-of-the-server]__1tlvv9a._.js} +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/{_1fahg9e._.js → _0vo2qyt._.js} +2 -2
  108. package/.next/standalone/.next/server/chunks/ssr/{_0uz7o9y._.js → _0xej-0e._.js} +2 -2
  109. package/.next/standalone/.next/server/chunks/ssr/{_04mekdy._.js → _10399ge._.js} +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/{_0se484m._.js → _149k9oy._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/{_1vsgpqt._.js → _166jvu8._.js} +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/{_0r-dsre._.js → _1_hwp06._.js} +2 -2
  113. package/.next/standalone/.next/server/chunks/ssr/{_1w8sola._.js → _1mnhi_r._.js} +2 -2
  114. package/.next/standalone/.next/server/chunks/ssr/{_1w_6p9g._.js → _1ror7k9._.js} +2 -2
  115. package/.next/standalone/.next/server/chunks/ssr/{_1gmzkwj._.js → _1sdgprl._.js} +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  119. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  120. package/.next/standalone/.next/server/pages/404.html +1 -1
  121. package/.next/standalone/.next/server/pages/500.html +1 -1
  122. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  123. package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
  124. package/.next/standalone/.next/static/chunks/{2_h--2ame3r83.js → 01s3vpzky_qew.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/0km-jn9o5xyqs.css +2 -0
  126. package/.next/standalone/.next/static/chunks/{23hp7hekegpe5.js → 0zbcd_n8ku2ov.js} +1 -1
  127. package/.next/standalone/.next/static/chunks/{0cakk77ppmlo7.js → 1erji0ratofla.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{40-zk44qsg5y-.js → 23hnlzqx44vbe.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/{1wb8cnmzx69rz.js → 2cwsiyhc7d2cw.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/{0_rdbv_k5j51p.js → 2y42ebzynyxdv.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/{1spk1bub0vd-h.js → 315lncj7dsp9c.js} +1 -1
  132. package/.next/standalone/.next/static/chunks/{0-zh1umoq6ppc.js → 31_px0dfgdg9j.js} +2 -2
  133. package/.next/standalone/.next/static/chunks/{2bie7ajv70p9b.js → 3l4-fu3xt75j8.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/{298v78ypf8edl.js → 3poscoxn69x9g.js} +1 -1
  135. package/.next/standalone/app/policies/hooks-client.tsx +4 -4
  136. package/.next/standalone/ino.ujh3/.failproofai/VERSION +4 -0
  137. package/.next/standalone/ino.ujh3/.failproofai/state/last-version +1 -0
  138. package/.next/standalone/ino.ujh3/.failproofai/state/telemetry-id +1 -0
  139. package/.next/standalone/package.json +10 -10
  140. package/.next/standalone/server.js +1 -1
  141. package/bin/failproofai.mjs +186 -5
  142. package/dist/cli.mjs +1130 -1352
  143. package/dist/worker.mjs +107 -1000
  144. package/package.json +10 -10
  145. package/src/hooks/backfill-cli.ts +1 -1
  146. package/src/hooks/cloud-connection.ts +4 -4
  147. package/src/hooks/cloud-enrollment-cli.ts +1 -1
  148. package/src/hooks/cloud-enrollment.ts +3 -3
  149. package/src/hooks/cloud-managed-policies.ts +60 -22
  150. package/src/hooks/configure-wizard.ts +124 -53
  151. package/src/hooks/custom-hooks-loader.ts +57 -8
  152. package/src/hooks/daemon-service.ts +66 -11
  153. package/src/hooks/flush-cli.ts +180 -0
  154. package/src/hooks/fp-config.ts +242 -118
  155. package/src/hooks/fp-home.ts +138 -22
  156. package/src/hooks/fp-reset.ts +357 -43
  157. package/src/hooks/handler.ts +11 -11
  158. package/src/hooks/harness-cli.ts +309 -0
  159. package/src/hooks/hook-activity-store.ts +5 -5
  160. package/src/hooks/manager.ts +35 -0
  161. package/src/hooks/onboarding-attempt.ts +22 -6
  162. package/src/hooks/tui.ts +64 -7
  163. package/src/hooks/uninstall-cli.ts +1 -1
  164. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +0 -3
  165. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1iny24d._.js +0 -3
  166. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tyedjt._.js +0 -3
  167. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +0 -5
  168. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +0 -4
  169. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +0 -5
  170. package/.next/standalone/.next/static/chunks/13vrxdsdihqd_.css +0 -2
  171. /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_buildManifest.js +0 -0
  172. /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_clientMiddlewareManifest.js +0 -0
  173. /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.0-beta.12",
3
+ "version": "1.0.0-beta.13",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -101,12 +101,12 @@
101
101
  "html-to-image": "^1.11.13",
102
102
  "html2canvas": "^1.4.1",
103
103
  "posthog-node": "^5.47.7",
104
- "smol-toml": "^1.7.1",
105
104
  "sql.js": "^1.14.1",
106
105
  "yaml": "^2.9.0"
107
106
  },
108
107
  "overrides": {
109
108
  "postcss": "8.5.23",
109
+ "nanoid": "3.3.17",
110
110
  "eslint-plugin-react-hooks": "7.0.1",
111
111
  "vite": "8.0.16",
112
112
  "undici": "7.29.0",
@@ -114,15 +114,15 @@
114
114
  "sharp": "0.35.0"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@failproofai/failproofaid-linux-x64": "1.0.0-beta.12",
118
- "@failproofai/failproofaid-linux-arm64": "1.0.0-beta.12",
119
- "@failproofai/failproofaid-darwin-x64": "1.0.0-beta.12",
120
- "@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.12"
117
+ "@failproofai/failproofaid-linux-x64": "1.0.0-beta.13",
118
+ "@failproofai/failproofaid-linux-arm64": "1.0.0-beta.13",
119
+ "@failproofai/failproofaid-darwin-x64": "1.0.0-beta.13",
120
+ "@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.13"
121
121
  },
122
122
  "failproofaidBinaries": {
123
- "linux-x64": "3a17bf65b24cc16cd907578cbb90158b5ec53f12fb133c3d4b37a02e3c41ff98",
124
- "linux-arm64": "d341762677d7dbdcf03321d803d97b41ad12f4b903d766cf45b4d5258bf4ed93",
125
- "darwin-x64": "d17e95386c51bcefbb845d67134c64e1a269cbb444beed1801e4181ab976ab23",
126
- "darwin-arm64": "e8987ecfa755dedc3bc4470bde25a54838735a7a1cefa4b9c9225dd868503f44"
123
+ "linux-x64": "b96dd035433adbf1290ad8b4ed32fc8134d4dfc03057b8f6a210c041ac7d8d93",
124
+ "linux-arm64": "0e11c7349de7d10488a6ea8f80714260e3dbedabcd06b0ab530119970b16404d",
125
+ "darwin-x64": "7d2943d6126430d9282ca106918be8aa2e0133c4c7189493996b14cbbb361d06",
126
+ "darwin-arm64": "dd6c646611ee81d7db88254917232bd5b0cfac514657bd2ce1062c7bb24cf960"
127
127
  }
128
128
  }
@@ -168,7 +168,7 @@ export function runBackfillCommand(opts: BackfillOptions = {}): BackfillResult {
168
168
  exitCode: 1,
169
169
  lines: [
170
170
  "Collection is switched off, so a backfill would re-read history and send",
171
- "none of it. Enable it in ~/.failproofai/config.toml under [collector].",
171
+ 'none of it. Enable it in ~/.failproofai/config.json under "collector".',
172
172
  ],
173
173
  };
174
174
  }
@@ -137,7 +137,7 @@ export interface CapabilityOutcome {
137
137
  }
138
138
 
139
139
  export interface ConnectOutcome {
140
- policy: CapabilityOutcome & { policyCount?: number; generation?: number };
140
+ policy: CapabilityOutcome & { policyCount?: number; deployment?: number };
141
141
  ingest: CapabilityOutcome;
142
142
  /** Which organisation the key belongs to. Absent on a pre-introspect server. */
143
143
  org?: { id?: string; slug?: string; name?: string };
@@ -232,7 +232,7 @@ export async function connectToCloud(input: ConnectInput): Promise<ConnectOutcom
232
232
 
233
233
  const outcome: ConnectOutcome = {
234
234
  policy: policyResult.ok
235
- ? { ok: true, policyCount: policyResult.policyCount, generation: policyResult.generation }
235
+ ? { ok: true, policyCount: policyResult.policyCount, deployment: policyResult.deployment }
236
236
  : { ok: false, reason: policyResult.reason },
237
237
  ingest: ingestResult.ok ? { ok: true } : { ok: false, reason: ingestResult.reason },
238
238
  anyConfigured: false,
@@ -309,7 +309,7 @@ export function describeOutcome(outcome: ConnectOutcome, machineId: string, url:
309
309
  if (outcome.policy.ok && outcome.ingest.ok) {
310
310
  lines.push(`Connected to ${url} as ${machineId}${into}.`);
311
311
  const n = outcome.policy.policyCount ?? 0;
312
- lines.push(` Policy ${n} polic${n === 1 ? "y" : "ies"} assigned (generation ${outcome.policy.generation ?? 0}).`);
312
+ lines.push(` Policy ${n} polic${n === 1 ? "y" : "ies"} assigned (deployment ${outcome.policy.deployment ?? 0}).`);
313
313
  lines.push(` Dashboard hook activity will be sent to ${ingestUrlFor(url)}.`);
314
314
  return lines;
315
315
  }
@@ -317,7 +317,7 @@ export function describeOutcome(outcome: ConnectOutcome, machineId: string, url:
317
317
  if (outcome.policy.ok && !outcome.ingest.ok) {
318
318
  const n = outcome.policy.policyCount ?? 0;
319
319
  lines.push(`Connected to ${url} as ${machineId}${into}, for policy only.`);
320
- lines.push(` Policy ${n} polic${n === 1 ? "y" : "ies"} assigned (generation ${outcome.policy.generation ?? 0}).`);
320
+ lines.push(` Policy ${n} polic${n === 1 ? "y" : "ies"} assigned (deployment ${outcome.policy.deployment ?? 0}).`);
321
321
  lines.push(` Dashboard NOT configured: ${outcome.ingest.reason}`);
322
322
  lines.push("");
323
323
  lines.push(" Enforcement works. Nothing will appear in the dashboard until this");
@@ -214,7 +214,7 @@ export function runDisconnectCommand(): CommandResult {
214
214
  // credential ends the daemon's polling; every artifact already on disk stayed
215
215
  // referenced by `active.json` and kept being loaded on every tool call — so a
216
216
  // machine that had deliberately left its organisation went on being governed
217
- // by whatever generation was current when it left, indefinitely, while
217
+ // by whatever deployment was current when it left, indefinitely, while
218
218
  // `--status` reported it as unconnected.
219
219
  const stoppedManaged = clearActiveCloudManagedPolicies();
220
220
  // Back to OSS. Leaving mode = "cloud" with no credentials would describe a
@@ -182,7 +182,7 @@ export function clearCloudCredentials(): boolean {
182
182
  }
183
183
 
184
184
  export type VerifyResult =
185
- | { ok: true; policyCount: number; generation: number }
185
+ | { ok: true; policyCount: number; deployment: number }
186
186
  | { ok: false; reason: string };
187
187
 
188
188
  /**
@@ -228,11 +228,11 @@ export async function verifyCloudCredentials(creds: CloudCredentials): Promise<V
228
228
  }
229
229
 
230
230
  try {
231
- const body = (await response.json()) as { policies?: unknown[]; generation?: number };
231
+ const body = (await response.json()) as { policies?: unknown[]; deployment?: number };
232
232
  return {
233
233
  ok: true,
234
234
  policyCount: Array.isArray(body.policies) ? body.policies.length : 0,
235
- generation: typeof body.generation === "number" ? body.generation : 0,
235
+ deployment: typeof body.deployment === "number" ? body.deployment : 0,
236
236
  };
237
237
  } catch {
238
238
  return {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Reads the daemon-owned active cloud policy generation for the TypeScript
2
+ * Reads the daemon-owned active cloud policy deployment for the TypeScript
3
3
  * evaluator. The Rust reconciler is responsible for downloading and repairing
4
4
  * artifacts; this boundary independently verifies the digest immediately
5
5
  * before import so the worker never knowingly executes modified bytes.
@@ -10,7 +10,21 @@ import { homedir } from "node:os";
10
10
  import { isAbsolute, relative, resolve } from "node:path";
11
11
  import { cloudPoliciesDir } from "./fp-home";
12
12
 
13
- const ACTIVE_SCHEMA_VERSION = 1;
13
+ /**
14
+ * Active-manifest schema versions this reader accepts.
15
+ *
16
+ * MUST stay in step with `SUPPORTED_SCHEMA_VERSIONS` in `cloud_policies.rs`,
17
+ * which is what WRITES the file this parses. They are two hand-maintained
18
+ * copies of one contract in two languages, and the failure is silent in the
19
+ * worst direction: the daemon reconciles happily, `active.json` is correct on
20
+ * disk, and only the hook path refuses it — so cloud policy stops being
21
+ * enforced while every other signal says the machine is healthy. Reproduced
22
+ * exactly that way while syncing this with AgentEye#559.
23
+ *
24
+ * 1 is accepted for files a pre-rename beta daemon left behind; 2 is what is
25
+ * written now.
26
+ */
27
+ const ACCEPTED_ACTIVE_SCHEMA_VERSIONS: readonly number[] = [1, 2];
14
28
  const SHA256_RE = /^[a-f0-9]{64}$/;
15
29
  const POLICY_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
16
30
 
@@ -19,7 +33,7 @@ export type PolicyEffect = "enforce" | "observe";
19
33
 
20
34
  export interface CloudManagedPolicyArtifact {
21
35
  id: string;
22
- revision: number;
36
+ version: number;
23
37
  /**
24
38
  * `observe` policies are evaluated exactly like any other and then have their
25
39
  * verdict discarded, so a rollout can be measured against real traffic before
@@ -30,15 +44,15 @@ export interface CloudManagedPolicyArtifact {
30
44
  effect: PolicyEffect;
31
45
  sha256: string;
32
46
  path: string;
33
- generation: number;
47
+ deployment: number;
34
48
  }
35
49
 
36
50
  interface ActiveManifest {
37
51
  schemaVersion: number;
38
- generation: number;
52
+ deployment: number;
39
53
  policies: Array<{
40
54
  id: string;
41
- revision: number;
55
+ version: number;
42
56
  effect?: string;
43
57
  sha256: string;
44
58
  path: string;
@@ -52,17 +66,41 @@ export function cloudManagedPolicyRoot(): string {
52
66
  );
53
67
  }
54
68
 
69
+ /**
70
+ * A version-1 manifest, in the spelling a pre-rename daemon actually wrote.
71
+ *
72
+ * Accepting schema 1 while reading only the version-2 FIELD NAMES made the
73
+ * acceptance unreachable: every genuine v1 file threw "active manifest
74
+ * deployment is invalid". The Rust reader of this same file handles it with
75
+ * `#[serde(alias = "generation")]` / `#[serde(alias = "revision")]`; this is the
76
+ * TypeScript half of that pair, and it was missing.
77
+ */
78
+ interface LegacyActiveManifest {
79
+ generation?: number;
80
+ policies?: Array<{ revision?: number }>;
81
+ }
82
+
55
83
  function parseManifest(value: unknown): ActiveManifest {
56
84
  if (!value || typeof value !== "object") throw new Error("active manifest is not an object");
57
- const manifest = value as Partial<ActiveManifest>;
58
- if (manifest.schemaVersion !== ACTIVE_SCHEMA_VERSION) {
59
- throw new Error(`unsupported active manifest schema ${String(manifest.schemaVersion)}`);
85
+ const raw = value as Partial<ActiveManifest> & LegacyActiveManifest;
86
+ if (!ACCEPTED_ACTIVE_SCHEMA_VERSIONS.includes(raw.schemaVersion as number)) {
87
+ throw new Error(
88
+ `unsupported active manifest schema ${String(raw.schemaVersion)} ` +
89
+ `(supported: ${ACCEPTED_ACTIVE_SCHEMA_VERSIONS.join(", ")})`,
90
+ );
60
91
  }
61
- if (!Number.isSafeInteger(manifest.generation) || (manifest.generation ?? -1) < 0) {
62
- throw new Error("active manifest generation is invalid");
92
+ // New name wins; the old one is a fallback, not an equal. A file carrying both
93
+ // (written by a mixed-version machine) must resolve to the current field.
94
+ const deployment = raw.deployment ?? raw.generation;
95
+ if (!Number.isSafeInteger(deployment) || (deployment ?? -1) < 0) {
96
+ throw new Error("active manifest deployment is invalid");
63
97
  }
64
- if (!Array.isArray(manifest.policies)) throw new Error("active manifest policies is not an array");
65
- return manifest as ActiveManifest;
98
+ if (!Array.isArray(raw.policies)) throw new Error("active manifest policies is not an array");
99
+ const policies = raw.policies.map((p) => {
100
+ const entry = p as ActiveManifest["policies"][number] & { revision?: number };
101
+ return { ...entry, version: entry.version ?? entry.revision } as ActiveManifest["policies"][number];
102
+ });
103
+ return { schemaVersion: raw.schemaVersion as number, deployment: deployment as number, policies };
66
104
  }
67
105
 
68
106
  function resolveManagedPath(root: string, candidate: string): string {
@@ -73,7 +111,7 @@ function resolveManagedPath(root: string, candidate: string): string {
73
111
  throw new Error(`managed policy path escapes its root: ${JSON.stringify(candidate)}`);
74
112
  }
75
113
 
76
- // Reject a symlinked artifact or generation directory escaping the managed
114
+ // Reject a symlinked artifact or deployment directory escaping the managed
77
115
  // root. The current daemon is same-user, but following an arbitrary path is
78
116
  // still unnecessary ambient authority and becomes dangerous if its service
79
117
  // identity is hardened later.
@@ -89,8 +127,8 @@ function resolveManagedPath(root: string, candidate: string): string {
89
127
  /**
90
128
  * Stop enforcing cloud-managed policies on this machine.
91
129
  *
92
- * Removes `active.json` only — the manifest that says which generation is
93
- * live. The generation directories and content-addressed artifacts are left
130
+ * Removes `active.json` only — the manifest that says which deployment is
131
+ * live. The deployment directories and content-addressed artifacts are left
94
132
  * alone: they are large, hash-verified on use, and inert once nothing points
95
133
  * at them, so keeping them makes a reconnect cheap and offline-safe.
96
134
  *
@@ -98,7 +136,7 @@ function resolveManagedPath(root: string, candidate: string): string {
98
136
  * credential stops the daemon REFRESHING policy; every artifact already on disk
99
137
  * kept being loaded and enforced on every tool call, so a user who had
100
138
  * deliberately left their organisation's cloud went on being governed by
101
- * whatever generation happened to be current when they left — indefinitely,
139
+ * whatever deployment happened to be current when they left — indefinitely,
102
140
  * with `--status` reporting the machine as unconnected.
103
141
  *
104
142
  * Returns true when a manifest was actually removed.
@@ -134,8 +172,8 @@ export function readActiveCloudManagedPolicies(): CloudManagedPolicyArtifact[] {
134
172
  if (!POLICY_ID_RE.test(policy.id) || policy.id === "." || policy.id === "..") {
135
173
  throw new Error(`unsafe cloud-managed policy id ${JSON.stringify(policy.id)}`);
136
174
  }
137
- if (!Number.isSafeInteger(policy.revision) || policy.revision < 0) {
138
- throw new Error(`invalid revision for cloud-managed policy ${policy.id}`);
175
+ if (!Number.isSafeInteger(policy.version) || policy.version < 0) {
176
+ throw new Error(`invalid version for cloud-managed policy ${policy.id}`);
139
177
  }
140
178
  if (!SHA256_RE.test(policy.sha256)) {
141
179
  throw new Error(`invalid SHA-256 for cloud-managed policy ${policy.id}`);
@@ -154,16 +192,16 @@ export function readActiveCloudManagedPolicies(): CloudManagedPolicyArtifact[] {
154
192
  if (policy.effect !== undefined && policy.effect !== "enforce" && policy.effect !== "observe") {
155
193
  // Guessing would mean either enforcing something cloud did not ask to
156
194
  // enforce, or observing something it wanted enforced. Both are worse than
157
- // refusing the generation.
195
+ // refusing the deployment.
158
196
  throw new Error(`unknown effect ${JSON.stringify(policy.effect)} for cloud-managed policy ${policy.id}`);
159
197
  }
160
198
  return {
161
199
  id: policy.id,
162
- revision: policy.revision,
200
+ version: policy.version,
163
201
  effect: (policy.effect as PolicyEffect | undefined) ?? "enforce",
164
202
  sha256: policy.sha256,
165
203
  path,
166
- generation: manifest.generation,
204
+ deployment: manifest.deployment,
167
205
  };
168
206
  });
169
207
  }
@@ -33,6 +33,7 @@ import { dirname, resolve, sep } from "node:path";
33
33
  import {
34
34
  selectOne,
35
35
  multiSelect,
36
+ BACK,
36
37
  promptText,
37
38
  intro,
38
39
  outro,
@@ -126,6 +127,7 @@ export type WizardAbort =
126
127
  | "cancelled"
127
128
  | "needs_root"
128
129
  | "daemon_failed"
130
+ | "unsupported_platform"
129
131
  | "not_a_tty"
130
132
  | "running_as_sudo";
131
133
 
@@ -415,6 +417,37 @@ export function describeCustomPolicies(cwd: string): {
415
417
  return { active, warnings, fileCount, scopes };
416
418
  }
417
419
 
420
+ /**
421
+ * The wizard's one-line completion summary. Pure and exported so the widest
422
+ * real combination — every policy, every CLI, custom/daemon/reporting all
423
+ * present — can be pinned by a test without having to drive the whole wizard
424
+ * through a real chdir + on-disk custom-policy fixture.
425
+ *
426
+ * Kept inside a standard 80-column terminal: `writeLines` truncates with a
427
+ * hard cut and no ellipsis, so an over-long line doesn't just lose its tail
428
+ * — it reads as broken output. Naming all ten CLIs once took it to 182
429
+ * characters; the count alone carries the same information, and the user
430
+ * picked them two screens ago. A single grouped "· a, b, c" clause bounds the
431
+ * optional notes to one separator and short tags, rather than three
432
+ * independent " · " clauses stacking up.
433
+ */
434
+ export function buildCompletionSummary(
435
+ policiesCount: number,
436
+ harnessesCount: number,
437
+ customEnabled: boolean | undefined,
438
+ daemonInstalled: boolean,
439
+ connected: boolean,
440
+ ): string {
441
+ const extras: string[] = [];
442
+ if (customEnabled === true) extras.push("custom");
443
+ else if (customEnabled === false) extras.push("custom off");
444
+ if (daemonInstalled) extras.push("daemon");
445
+ if (connected) extras.push("reporting");
446
+ const extrasNote = extras.length > 0 ? ` · ${extras.join(", ")}` : "";
447
+ const harnesses = `${harnessesCount} harness${harnessesCount === 1 ? "" : "es"}`;
448
+ return `Setup complete — ${policiesCount} policies · ${harnesses}${extrasNote}`;
449
+ }
450
+
418
451
  export function reviewLines(state: {
419
452
  /** What the scope step resolved to. Expands to one or two real scopes. */
420
453
  target: SetupTarget;
@@ -441,9 +474,9 @@ export function reviewLines(state: {
441
474
  ? `This project (${homeify(cwd)})`
442
475
  : "Everywhere (global)";
443
476
  const lines: string[] = [];
444
- const assistantNames = clis.map((c) => getIntegration(c).displayName);
477
+ const harnessNames = clis.map((c) => getIntegration(c).displayName);
445
478
  lines.push(` Where : ${where}`);
446
- lines.push(` Assistants : ${assistantNames.length ? summarize(assistantNames, "assistants") : "(none)"}`);
479
+ lines.push(` Harnesses : ${harnessNames.length ? summarize(harnessNames, "harnesses") : "(none)"}`);
447
480
  // Zero is a deliberate answer, not a failed step — say so, and say where to
448
481
  // change it, so the review screen doesn't read like the wizard lost the
449
482
  // selection. Hooks still install; only the builtin set is empty.
@@ -712,7 +745,26 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
712
745
 
713
746
  // Fire-and-forget: never block the wizard's first paint on telemetry.
714
747
  void emit("configure_started", {});
715
- intro("let's set up your safety net", stdout);
748
+
749
+ // failproofaid — the only evaluator on a configured machine — only runs on
750
+ // Linux and macOS. Checked before intro() draws anything and before a
751
+ // single prompt is asked: completing setup anyway used to leave e.g. a
752
+ // Windows machine reading as configured while enforcing in-process with no
753
+ // fail-closed guarantee, which is worse than not being set up at all.
754
+ if (!isDaemonSupportedPlatform()) {
755
+ stdout.write(
756
+ `failproofai requires failproofaid, its background policy daemon, which runs on\n` +
757
+ `Linux and macOS only — not ${process.platform}. Setup cannot continue here: an\n` +
758
+ "installation with no daemon behind it would read as configured while enforcing\n" +
759
+ "nothing, which is worse than not being set up at all.\n\n" +
760
+ "Nothing was changed. This platform will be supported once failproofaid gains a\n" +
761
+ `${process.platform} service target.\n\n`,
762
+ );
763
+ void emit("configure_aborted", { reason: "unsupported_platform" });
764
+ return { applied: false, abort: "unsupported_platform" };
765
+ }
766
+
767
+ intro("let's set up failproofai", stdout);
716
768
 
717
769
  const cancel = (): WizardResult => {
718
770
  outro("Cancelled — nothing was changed.", { ok: false }, stdout);
@@ -733,9 +785,9 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
733
785
  // Machine-level, so it is deliberately NOT gated on the scope chosen in the
734
786
  // next step: one daemon serves every project on this machine.
735
787
  //
736
- // On a platform with no service manager there is nothing to install, so the
737
- // requirement does not apply — requiring an impossible step would lock those
738
- // users out of setup entirely rather than protecting anything.
788
+ // Always true here — the guard near the top of this function already
789
+ // refused setup on anything else. Kept as a real read (not a literal
790
+ // `true`) so this block still fails safe if that guard is ever moved.
739
791
  const daemonSupported = isDaemonSupportedPlatform();
740
792
  // An already-healthy daemon needs no install and no password. Re-running
741
793
  // setup on a configured machine must not demand sudo for work that is
@@ -909,24 +961,38 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
909
961
  // The assistants step below keeps its minimum deliberately: an empty CLI list
910
962
  // does NOT mean "no assistants" there — `installHooksImpl` falls back to
911
963
  // ["claude"], so letting it through would silently install for Claude.
912
- const presets = await multiSelect<string>({
913
- message: "What should we guard against?",
914
- choices: presetChoices,
915
- summaryNoun: "bundles",
916
- hint: "space toggles · combine presets · ↵ confirm · none is fine",
917
- stdin,
918
- stdout,
919
- });
920
- if (presets === null) return cancel();
921
- const policies = resolvePresetSelection(presets);
922
- // Only meaningful when there are files to switch off; with none, the row is
923
- // locked-unchecked and must not write a disabling flag.
924
- const customEnabled = hasCustomFiles ? presets.includes(CUSTOM) : undefined;
964
+ // Steps 2 and 3 are navigable: ← on the harness step returns to the policy
965
+ // step with the previous answer still selected. Before this, changing an
966
+ // earlier answer meant abandoning setup and starting over, because a prompt
967
+ // had exactly one way out and it was `null`.
968
+ //
969
+ // The policy step itself takes no `allowBack`: the only thing before it is
970
+ // the scope question, which is frequently not asked at all (a single choice
971
+ // is stated, not prompted), so ← there would sometimes go nowhere.
972
+ let presets: string[] | null = null;
973
+ let clisSel: string[] | null = null;
974
+ while (clisSel === null) {
975
+ // Re-entering after a ← must show what was picked, not a blank slate.
976
+ // Selection state lives on each choice, so carry it back in.
977
+ // Loop-carried: narrowed to `null` on the first pass, repopulated on a ←.
978
+ const priorPresets = presets as string[] | null;
979
+ presets = await multiSelect<string>({
980
+ message: "What should we guard against?",
981
+ choices: priorPresets
982
+ ? presetChoices.map((c) => ({ ...c, checked: priorPresets.includes(c.value) }))
983
+ : presetChoices,
984
+ summaryNoun: "bundles",
985
+ hint: "space toggles · combine presets · ↵ confirm · none is fine",
986
+ stdin,
987
+ stdout,
988
+ });
989
+ if (presets === null) return cancel();
925
990
 
926
- // 3 — Which assistants? An "Everything available" row protects every supported
927
- // CLI (detected + set-up-ahead); when ticked it wins over the individual boxes.
928
- const clisSel = await multiSelect<string>({
929
- message: "Which AI assistants should it protect?",
991
+ // 3 — Which harnesses? An "Everything available" row protects every supported
992
+ // CLI (detected + set-up-ahead); when ticked it wins over the individual boxes.
993
+ const priorClis = clisSel as string[] | null;
994
+ const picked: string[] | typeof BACK | null = await multiSelect<string>({
995
+ message: "Which harnesses should it protect?",
930
996
  choices: [
931
997
  {
932
998
  label: "Everything available",
@@ -934,19 +1000,30 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
934
1000
  // Counts only what this scope can actually take — expanding to all 12
935
1001
  // under project scope is what crashed the apply on Hermes.
936
1002
  hint: `protect all ${clisSupportingScope(primaryScope).length} CLIs configurable here`,
937
- // A selector, not an assistant. Counting it gave "13 assistants" for
1003
+ // A selector, not a harness. Counting it gave "13 harnesses" for
938
1004
  // the 12 supported CLIs, and listed "Everything available" among them.
939
1005
  summaryExclude: true,
940
1006
  },
941
1007
  ...buildAgentChoices(primaryScope, cwd),
942
- ],
943
- minSelected: 1,
944
- summaryNoun: "assistants",
945
- hint: "detected CLIs are pre-selected · space toggles · ctrl+a all · ↵ confirm",
946
- stdin,
947
- stdout,
948
- });
949
- if (clisSel === null) return cancel();
1008
+ ].map((c) => (priorClis ? { ...c, checked: priorClis.includes(c.value) } : c)),
1009
+ minSelected: 1,
1010
+ summaryNoun: "harnesses",
1011
+ hint: "detected CLIs are pre-selected · space toggles · ctrl+a all · ← back · ↵ confirm",
1012
+ allowBack: true as const,
1013
+ stdin,
1014
+ stdout,
1015
+ });
1016
+ if (picked === null) return cancel();
1017
+ // ← re-runs the loop, which re-asks the policy step with its answer intact.
1018
+ if (picked === BACK) continue;
1019
+ clisSel = picked;
1020
+ }
1021
+ // Non-null by construction: the loop only exits once both are assigned.
1022
+ const chosenPresets: string[] = presets ?? [];
1023
+ const policies = resolvePresetSelection(chosenPresets);
1024
+ // Only meaningful when there are files to switch off; with none, the row is
1025
+ // locked-unchecked and must not write a disabling flag.
1026
+ const customEnabled = hasCustomFiles ? chosenPresets.includes(CUSTOM) : undefined;
950
1027
  // Filter to what the chosen scopes support in BOTH branches: "Everything
951
1028
  // available" must not expand to CLIs that cannot take any selected scope,
952
1029
  // and a locked row can't be ticked but belt-and-braces keeps the invariant
@@ -987,17 +1064,21 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
987
1064
  " transcripts — prompts, file contents and command output — to your",
988
1065
  " dashboard. Staying local sends nothing, anywhere, ever.",
989
1066
  ],
1067
+ // Cloud first, and therefore preselected: connecting is what most people
1068
+ // running this wizard came to do, and the local path stays one keystroke
1069
+ // away. Reversing these two is the whole change — neither option's copy
1070
+ // moved, so "stay local" is still stated as plainly as it was.
990
1071
  choices: [
991
- {
992
- label: "Not now — stay local",
993
- value: "local",
994
- hint: "policies still enforce · connect later with failproofai config --connect",
995
- },
996
1072
  {
997
1073
  label: "Paste an API key",
998
1074
  value: "key",
999
1075
  hint: "reports decisions and transcripts to your dashboard",
1000
1076
  },
1077
+ {
1078
+ label: "Not now — stay local",
1079
+ value: "local",
1080
+ hint: "policies still enforce · connect later with failproofai config --connect",
1081
+ },
1001
1082
  ],
1002
1083
  stdin,
1003
1084
  stdout,
@@ -1319,7 +1400,7 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1319
1400
  cli: clis,
1320
1401
  cli_count: clis.length,
1321
1402
  policy_count: policies.length,
1322
- source: presets.join("+"),
1403
+ source: chosenPresets.join("+"),
1323
1404
  connected: connect !== null,
1324
1405
  });
1325
1406
 
@@ -1411,22 +1492,12 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1411
1492
  // And any record of an earlier failure is now false: this machine got set up.
1412
1493
  clearOnboardingAttempt();
1413
1494
 
1414
- // Keep this inside a standard 80-column terminal. `writeLines` truncates with
1415
- // a hard cut and no ellipsis, so an over-long line doesn't just lose its tail
1416
- // — it reads as broken output. Naming all ten CLIs took it to 182 characters;
1417
- // the count alone carries the same information, and the user picked them two
1418
- // screens ago.
1419
- const customNote =
1420
- customEnabled === true
1421
- ? " + your custom policies"
1422
- : customEnabled === false
1423
- ? " · custom policies DISABLED"
1424
- : "";
1425
- const daemonNote = daemonInstalled ? " · daemon on" : "";
1426
- const cloudNote = connected ? " · reporting on" : "";
1427
- const assistants = `${clis.length} assistant${clis.length === 1 ? "" : "s"}`;
1495
+ // Every real completed setup is on a supported platform now (an unsupported
1496
+ // one aborts before this point), so the optional notes in the summary below
1497
+ // are no longer occasional additions — see buildCompletionSummary's own doc
1498
+ // comment for why the widest combination still fits in 80 columns.
1428
1499
  outro(
1429
- `Setup complete — ${policies.length} policies${customNote} · ${assistants}${daemonNote}${cloudNote}`,
1500
+ buildCompletionSummary(policies.length, clis.length, customEnabled, daemonInstalled, connected),
1430
1501
  { ok: true },
1431
1502
  stdout,
1432
1503
  );