failproofai 1.0.0-beta.8 → 1.0.0-beta.9

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 (103) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  27. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/.next/server/app/index.html +1 -1
  30. package/.next/standalone/.next/server/app/index.rsc +14 -14
  31. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  32. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  33. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  34. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
  35. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  36. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  37. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
  40. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  53. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +1 -1
  56. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tkyag1._.js → [root-of-the-server]__0abann8._.js} +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ommekk._.js → [root-of-the-server]__0f9901b._.js} +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tyf0tc._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/_04mekdy._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/_0r-dsre._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/ssr/_0se484m._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/ssr/_0uz7o9y._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/_1fahg9e._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/{_0i4l3_6._.js → _1t3vvw3._.js} +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/_1w8sola._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/_1w_6p9g._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  77. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  78. package/.next/standalone/.next/server/pages/404.html +1 -1
  79. package/.next/standalone/.next/server/pages/500.html +1 -1
  80. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  81. package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
  82. package/.next/standalone/.next/static/chunks/{1zrdsxe91qq6i.js → 0jibhdbjojuq5.js} +1 -1
  83. package/.next/standalone/.next/static/chunks/{3yn1y6ckypqwp.js → 0qubxfmqjdk8-.js} +1 -1
  84. package/.next/standalone/.next/static/chunks/{2047vah_v4emu.js → 159mxiauw9a8_.js} +1 -1
  85. package/.next/standalone/.next/static/chunks/{2bw0stk-s-1dd.js → 1xxzquhtvg72x.js} +1 -1
  86. package/.next/standalone/.next/static/chunks/{3m8onpqrjxzu5.js → 270jpx60b2kuj.js} +1 -1
  87. package/.next/standalone/.next/static/chunks/{02vvywmww22zz.js → 2f295j8d9ta-h.js} +1 -1
  88. package/.next/standalone/.next/static/chunks/{1fwh-2un_l-eo.js → 2g7p545cy5458.js} +1 -1
  89. package/.next/standalone/.next/static/chunks/{2r_j5wrkbu5wm.js → 2nsm1u-n43hfa.js} +1 -1
  90. package/.next/standalone/.next/static/chunks/{23hl78dzffun4.js → 3940dugfmx1ai.js} +1 -1
  91. package/.next/standalone/.next/static/chunks/{0_-focvo4cuiv.js → 3dpc4c0l_hf32.js} +1 -1
  92. package/.next/standalone/package.json +5 -5
  93. package/.next/standalone/server.js +1 -1
  94. package/dist/cli.mjs +66 -30
  95. package/dist/worker.mjs +1 -1
  96. package/package.json +5 -5
  97. package/src/hooks/collector-config.ts +18 -2
  98. package/src/hooks/configure-wizard.ts +49 -28
  99. package/src/hooks/fp-home.ts +23 -3
  100. package/src/hooks/fp-reset.ts +112 -5
  101. /package/.next/standalone/.next/static/{gOR8i03AeQm1VnIayDHVV → biwmgAwO6vVswC0z-soDU}/_buildManifest.js +0 -0
  102. /package/.next/standalone/.next/static/{gOR8i03AeQm1VnIayDHVV → biwmgAwO6vVswC0z-soDU}/_clientMiddlewareManifest.js +0 -0
  103. /package/.next/standalone/.next/static/{gOR8i03AeQm1VnIayDHVV → biwmgAwO6vVswC0z-soDU}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -19,7 +19,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // package.json
22
- var version2 = "1.0.0-beta.8";
22
+ var version2 = "1.0.0-beta.9";
23
23
  var init_package = () => {};
24
24
 
25
25
  // src/posthog-key.ts
@@ -1007,7 +1007,8 @@ function resettablePaths() {
1007
1007
  legacy.launcherMarker(),
1008
1008
  legacy.lastVersion(),
1009
1009
  legacy.auditDashboard(),
1010
- legacy.cacheDir(),
1010
+ legacy.auditCacheDir(),
1011
+ legacy.codexSessionPaths(),
1011
1012
  legacy.spoolDir(),
1012
1013
  legacy.failedDir(),
1013
1014
  legacy.collectorHealth(),
@@ -1015,12 +1016,10 @@ function resettablePaths() {
1015
1016
  legacy.cloudManagedPolicies(),
1016
1017
  localPoliciesDir(),
1017
1018
  cloudPoliciesDir(),
1018
- at("cursors"),
1019
1019
  at("state"),
1020
1020
  configFile(),
1021
1021
  credentialsFile(),
1022
1022
  auditDir(),
1023
- hookActivityDir(),
1024
1023
  customAgentsDir(),
1025
1024
  logsDir()
1026
1025
  ];
@@ -11955,11 +11954,12 @@ var init_handler = __esm(() => {
11955
11954
  var exports_fp_reset = {};
11956
11955
  __export(exports_fp_reset, {
11957
11956
  resetHome: () => resetHome,
11957
+ migrateHookActivity: () => migrateHookActivity,
11958
11958
  migrateConventionPolicies: () => migrateConventionPolicies,
11959
11959
  layoutWarningForHook: () => layoutWarningForHook2,
11960
11960
  checkLayoutForCli: () => checkLayoutForCli
11961
11961
  });
11962
- import { existsSync as existsSync18, mkdirSync as mkdirSync10, readdirSync as readdirSync13, renameSync as renameSync5, rmSync as rmSync6 } from "node:fs";
11962
+ import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync10, readdirSync as readdirSync13, renameSync as renameSync5, rmSync as rmSync6 } from "node:fs";
11963
11963
  import { resolve as resolve15 } from "node:path";
11964
11964
  function migrateConventionPolicies() {
11965
11965
  const from = policiesDir();
@@ -11984,8 +11984,45 @@ function migrateConventionPolicies() {
11984
11984
  } catch {}
11985
11985
  return moved.sort((a, b) => a.localeCompare(b));
11986
11986
  }
11987
+ function migrateHookActivity() {
11988
+ const from = legacy.hookActivityDir();
11989
+ if (!existsSync18(from))
11990
+ return [];
11991
+ const moved = [];
11992
+ try {
11993
+ const entries = readdirSync13(from, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
11994
+ if (entries.length === 0)
11995
+ return [];
11996
+ const to = hookActivityDir();
11997
+ mkdirSync10(to, { recursive: true });
11998
+ const stamp2 = Date.now();
11999
+ let seq = 0;
12000
+ for (const entry of entries) {
12001
+ const source = resolve15(from, entry.name);
12002
+ let name = entry.name === "current.jsonl" ? `page-${stamp2}-${seq++}.jsonl` : entry.name;
12003
+ let target = resolve15(to, name);
12004
+ while (existsSync18(target)) {
12005
+ name = `page-${stamp2}-${seq++}.jsonl`;
12006
+ target = resolve15(to, name);
12007
+ }
12008
+ try {
12009
+ renameSync5(source, target);
12010
+ moved.push(name);
12011
+ } catch (err) {
12012
+ if (err?.code === "EXDEV") {
12013
+ try {
12014
+ copyFileSync(source, target);
12015
+ moved.push(name);
12016
+ } catch {}
12017
+ }
12018
+ }
12019
+ }
12020
+ } catch {}
12021
+ return moved.sort((a, b) => a.localeCompare(b));
12022
+ }
11987
12023
  function resetHome(from) {
11988
12024
  const migrated = migrateConventionPolicies();
12025
+ const activity = migrateHookActivity();
11989
12026
  const removed = [];
11990
12027
  for (const path3 of resettablePaths()) {
11991
12028
  if (!existsSync18(path3))
@@ -11996,7 +12033,7 @@ function resetHome(from) {
11996
12033
  } catch {}
11997
12034
  }
11998
12035
  writeVersionFile();
11999
- return { removed, migrated, from };
12036
+ return { removed, migrated, activity, from };
12000
12037
  }
12001
12038
  async function checkLayoutForCli() {
12002
12039
  const state = detectLayout();
@@ -12015,21 +12052,25 @@ async function checkLayoutForCli() {
12015
12052
  };
12016
12053
  }
12017
12054
  if (state.kind === "stale") {
12018
- const { removed, migrated } = resetHome(state.found);
12055
+ const { removed, migrated, activity } = resetHome(state.found);
12019
12056
  return {
12020
12057
  state,
12021
12058
  fatal: false,
12022
12059
  didReset: true,
12023
12060
  lines: [
12024
12061
  `failproofai reorganised ${failproofaiHome()} in this version.`,
12025
- `Removed ${removed.length} item(s) from the old layout — policy config, activity`,
12026
- `history and audit cache. Your downloaded daemon binary was kept.`,
12062
+ `Removed ${removed.length} item(s) from the old layout — policy config and`,
12063
+ `audit cache. Your daemon binary and decision history were kept.`,
12027
12064
  ...migrated.length > 0 ? [
12028
12065
  ``,
12029
12066
  `Kept your own policy file(s) and moved them to where this version`,
12030
12067
  `loads them (${customPoliciesDir()}):`,
12031
12068
  ...migrated.map((name) => ` ${name}`)
12032
12069
  ] : [],
12070
+ ...activity.length > 0 ? [
12071
+ ``,
12072
+ `Carried ${activity.length} page(s) of decision history into ${hookActivityDir()}.`
12073
+ ] : [],
12033
12074
  ``,
12034
12075
  `Run \`failproofai config\` to set up again.`
12035
12076
  ]
@@ -12957,7 +12998,7 @@ async function validateIngestKey(cred, fetchImpl = fetch, timeoutMs = 1e4) {
12957
12998
  clearTimeout(timer);
12958
12999
  }
12959
13000
  }
12960
- var DEFAULT_INGEST_URL = "https://server.befailproof.ai/v1/events";
13001
+ var DEFAULT_INGEST_URL = "https://app.befailproof.ai/v1/events";
12961
13002
  var init_collector_config = __esm(() => {
12962
13003
  init_fp_home();
12963
13004
  init_fp_config();
@@ -26036,31 +26077,26 @@ refreshed. Re-run \`failproofai config\` once you can use sudo.
26036
26077
  }
26037
26078
  }
26038
26079
  if (url === null) {
26039
- const entered = await promptText({
26040
- message: "Failproof Cloud URL",
26041
- defaultValue: cloudBaseFor(DEFAULT_INGEST_URL),
26042
- hint: "Enter for the hosted endpoint",
26043
- validate: (v) => {
26044
- const check = validateCloudUrl(cloudBaseFor(v));
26045
- return check.ok ? null : check.reason;
26046
- },
26047
- stdin,
26048
- stdout
26049
- });
26050
- if (entered === null)
26051
- return cancel();
26052
- const validated = validateCloudUrl(cloudBaseFor(entered));
26053
- if (!validated.ok) {
26080
+ const override = process.env.FAILPROOFAI_CLOUD_URL?.trim();
26081
+ if (override) {
26082
+ const validated = validateCloudUrl(cloudBaseFor(override));
26083
+ if (!validated.ok) {
26084
+ stdout.write(`
26085
+ FAILPROOFAI_CLOUD_URL is set to "${override}", which cannot be used: ${validated.reason}
26086
+ `);
26087
+ return cancel();
26088
+ }
26089
+ url = validated.url;
26054
26090
  stdout.write(`
26055
- ${validated.reason}
26091
+ Using ${url} (from FAILPROOFAI_CLOUD_URL).
26056
26092
  `);
26057
- return cancel();
26093
+ } else {
26094
+ url = cloudBaseFor(DEFAULT_INGEST_URL);
26058
26095
  }
26059
- url = validated.url;
26060
26096
  }
26061
26097
  if (token === null) {
26062
26098
  token = await promptText({
26063
- message: "API key",
26099
+ message: `API key for ${new URL(url).host}`,
26064
26100
  hint: "needs events:add · policies:pull enables managed policy too",
26065
26101
  mask: true,
26066
26102
  validate: (v) => v.length >= 8 ? null : "that looks too short to be a key",
@@ -28751,7 +28787,7 @@ import { realpathSync as realpathSync6 } from "fs";
28751
28787
  import { dirname as dirname17, resolve as resolve26 } from "path";
28752
28788
  import { fileURLToPath as fileURLToPath4 } from "url";
28753
28789
  // package.json
28754
- var version = "1.0.0-beta.8";
28790
+ var version = "1.0.0-beta.9";
28755
28791
 
28756
28792
  // bin/failproofai.mjs
28757
28793
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
package/dist/worker.mjs CHANGED
@@ -2505,7 +2505,7 @@ var init_builtin_policies = __esm(() => {
2505
2505
  });
2506
2506
 
2507
2507
  // package.json
2508
- var version = "1.0.0-beta.8";
2508
+ var version = "1.0.0-beta.9";
2509
2509
  var init_package = () => {};
2510
2510
 
2511
2511
  // src/posthog-key.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.0-beta.9",
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",
@@ -114,9 +114,9 @@
114
114
  "sharp": "0.35.0"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@failproofai/failproofaid-linux-x64": "1.0.0-beta.8",
118
- "@failproofai/failproofaid-linux-arm64": "1.0.0-beta.8",
119
- "@failproofai/failproofaid-darwin-x64": "1.0.0-beta.8",
120
- "@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.8"
117
+ "@failproofai/failproofaid-linux-x64": "1.0.0-beta.9",
118
+ "@failproofai/failproofaid-linux-arm64": "1.0.0-beta.9",
119
+ "@failproofai/failproofaid-darwin-x64": "1.0.0-beta.9",
120
+ "@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.9"
121
121
  }
122
122
  }
@@ -31,8 +31,24 @@ import { readHooksConfig, writeHooksConfig } from "./hooks-config";
31
31
  import { credentialsFile, failproofaiHome as layoutHome } from "./fp-home";
32
32
  import { readCredentials, writeCredentials, updateConfig } from "./fp-config";
33
33
 
34
- /** The hosted ingest endpoint — a COMPLETE endpoint, not a base to join onto. */
35
- export const DEFAULT_INGEST_URL = "https://server.befailproof.ai/v1/events";
34
+ /**
35
+ * The hosted ingest endpoint — a COMPLETE endpoint, not a base to join onto.
36
+ *
37
+ * The DASHBOARD hostname, not the API server's. On the hosted deployment a
38
+ * reverse proxy routes `/v1/*` and `/enforcement/v1/*` to the server and
39
+ * everything else to the Next.js app, so this path already reaches ingest —
40
+ * the server hostname is not needed to get there and does not need to be a
41
+ * public name for the product to work.
42
+ *
43
+ * One hostname is also what makes `--connect <origin>` mean one thing. Ingest,
44
+ * key introspection (`/v1/auth/introspect`) and cloud-managed policy
45
+ * (`/enforcement/v1/*`) all resolve against the same origin someone pastes,
46
+ * which is the origin they already have in their browser.
47
+ *
48
+ * MUST stay byte-identical to `DEFAULT_INGEST_URL` in
49
+ * `crates/fpai-collect/src/config.rs` — see that copy for why.
50
+ */
51
+ export const DEFAULT_INGEST_URL = "https://app.befailproof.ai/v1/events";
36
52
 
37
53
  export interface IngestCredential {
38
54
  url: string;
@@ -1021,39 +1021,60 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1021
1021
  }
1022
1022
 
1023
1023
  if (url === null) {
1024
- const entered = await promptText({
1025
- message: "Failproof Cloud URL",
1026
- defaultValue: cloudBaseFor(DEFAULT_INGEST_URL),
1027
- hint: "Enter for the hosted endpoint",
1028
- // The SAME guard `--connect` uses, not a looser lookalike. This was
1029
- // `/^https?:\/\//`, which accepts `http://cloud.example.com` — and
1030
- // the wizard is the documented primary path, so the interactive flow
1031
- // put the machine's bearer token on the wire in clear while the flag
1032
- // nobody uses refused to. `validateCloudUrl` permits http for
1033
- // loopback only, which is what the local walkthrough needs.
1034
- validate: (v) => {
1035
- const check = validateCloudUrl(cloudBaseFor(v));
1036
- return check.ok ? null : check.reason;
1037
- },
1038
- stdin,
1039
- stdout,
1040
- });
1041
- if (entered === null) return cancel();
1042
- // Asked for as a base and normalised, so this and `--connect` take the
1043
- // same thing. Someone pasting the older `/events` endpoint still works.
1044
- const validated = validateCloudUrl(cloudBaseFor(entered));
1045
- if (!validated.ok) {
1046
- // Unreachable while `validate` above rejects the same input; kept so
1047
- // the two can never drift into disagreeing silently.
1048
- stdout.write(`\n${validated.reason}\n`);
1049
- return cancel();
1024
+ // NOT asked for. There is exactly one right answer for everybody using
1025
+ // the hosted product, and asking made it look like a decision — which
1026
+ // is how a key ends up pasted into the URL field, and how someone
1027
+ // reasonably types the dashboard's own address and gets a 404 from a
1028
+ // web app that is not the ingest endpoint. Both are real, both happened
1029
+ // within ten minutes of each other, and neither is a mistake the person
1030
+ // making it can be expected to avoid: "Failproof Cloud URL" has no
1031
+ // knowable answer other than the default it was already showing.
1032
+ //
1033
+ // The two audiences that genuinely need a different endpoint keep an
1034
+ // explicit way to say so, and neither is an interactive prompt:
1035
+ //
1036
+ // • local development / self-hosting → FAILPROOFAI_CLOUD_URL, the
1037
+ // same variable the DAEMON already reads for cloud-managed policy
1038
+ // (crates/failproofaid/src/cloud_client.rs), so one export points
1039
+ // the whole machine at one place instead of the wizard and the
1040
+ // daemon disagreeing.
1041
+ // • scripted installs → `failproofai config --connect <url> --token`,
1042
+ // unchanged.
1043
+ //
1044
+ // The env value goes through the SAME `validateCloudUrl` a typed one
1045
+ // did — it is not a trusted back door. http stays loopback-only, so a
1046
+ // bearer token still cannot be exported onto the wire in clear by
1047
+ // setting a variable.
1048
+ const override = process.env.FAILPROOFAI_CLOUD_URL?.trim();
1049
+ if (override) {
1050
+ const validated = validateCloudUrl(cloudBaseFor(override));
1051
+ if (!validated.ok) {
1052
+ // Loud, not silent-fallback-to-hosted: someone who exported this
1053
+ // wants THAT endpoint, and quietly reporting a machine to the
1054
+ // hosted service instead is the one outcome they did not ask for.
1055
+ stdout.write(
1056
+ `\nFAILPROOFAI_CLOUD_URL is set to "${override}", which cannot be used: ` +
1057
+ `${validated.reason}\n`,
1058
+ );
1059
+ return cancel();
1060
+ }
1061
+ url = validated.url;
1062
+ // Named on screen, because an env var is invisible at the moment it
1063
+ // matters and a machine reporting somewhere unexpected is exactly the
1064
+ // thing nobody notices until they go looking for data that is not
1065
+ // there.
1066
+ stdout.write(`\nUsing ${url} (from FAILPROOFAI_CLOUD_URL).\n`);
1067
+ } else {
1068
+ url = cloudBaseFor(DEFAULT_INGEST_URL);
1050
1069
  }
1051
- url = validated.url;
1052
1070
  }
1053
1071
 
1054
1072
  if (token === null) {
1055
1073
  token = await promptText({
1056
- message: "API key",
1074
+ // The destination is in the question now that it is no longer a
1075
+ // question of its own. It is the only remaining place a person can
1076
+ // notice they are about to send a key somewhere they did not mean.
1077
+ message: `API key for ${new URL(url).host}`,
1057
1078
  hint: "needs events:add · policies:pull enables managed policy too",
1058
1079
  // Masked: setup is routinely run while screen-sharing, and a pasted
1059
1080
  // key would otherwise sit in the scrollback of every recording.
@@ -312,7 +312,15 @@ export function resettablePaths(): string[] {
312
312
  legacy.launcherMarker(),
313
313
  legacy.lastVersion(),
314
314
  legacy.auditDashboard(),
315
- legacy.cacheDir(),
315
+ // `legacy.cacheDir()` — the whole of `cache/` — stood here, and it CONTAINS
316
+ // `cache/hook-activity`: layout 1's decision log. Deleting the parent threw
317
+ // away every decision the machine had ever recorded, which is the data the
318
+ // dashboard's activity tab exists to show. Its children are now named
319
+ // individually so the log can be carried across by
320
+ // `migrateHookActivity()`, and everything else in `cache/` still goes —
321
+ // both remaining entries are re-derived on demand.
322
+ legacy.auditCacheDir(),
323
+ legacy.codexSessionPaths(),
316
324
  legacy.spoolDir(),
317
325
  legacy.failedDir(),
318
326
  legacy.collectorHealth(),
@@ -335,7 +343,14 @@ export function resettablePaths(): string[] {
335
343
  // setup, `cloud-policies/` by the next daemon poll.
336
344
  localPoliciesDir(),
337
345
  cloudPoliciesDir(),
338
- at("cursors"),
346
+ // `at("cursors")` is deliberately NOT here, and it is load-bearing for the
347
+ // activity migration above rather than a separate opinion. Cursors are keyed
348
+ // on `(device, inode)`; `migrateHookActivity()` MOVES pages, which preserves
349
+ // the inode, so a surviving cursor still points at the file it belongs to
350
+ // and resumes at the right offset. Delete the cursors and every carried page
351
+ // reads as new and re-ships in full — which is the outcome the move exists
352
+ // to avoid. A cursor whose file is gone is inert (`retain_existing` drops
353
+ // it), so keeping them costs nothing when there is nothing to resume.
339
354
  at("state"),
340
355
  // Layout 2.
341
356
  // NOT versionFile() — it is the layout marker, not user data, and the
@@ -344,7 +359,12 @@ export function resettablePaths(): string[] {
344
359
  configFile(),
345
360
  credentialsFile(),
346
361
  auditDir(),
347
- hookActivityDir(),
362
+ // NOT hookActivityDir(). It is the DESTINATION `migrateHookActivity()` has
363
+ // just moved layout 1's log into, so listing it here deleted the migration
364
+ // moments after it happened — the reset runs every path in this list after
365
+ // the migrations. It is also the one directory here that is neither derived
366
+ // nor re-fetchable: a decision log is the record of what this machine did,
367
+ // and nothing regenerates it.
348
368
  customAgentsDir(),
349
369
  logsDir(),
350
370
  // NOT bin/ — a downloaded daemon binary is large, version-pinned and
@@ -29,12 +29,14 @@
29
29
  * user out of their agent entirely, with no way back except hand-editing JSON.
30
30
  * A loud warning that survives until setup runs is the proportionate answer.
31
31
  */
32
- import { existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
32
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
33
33
  import { resolve } from "node:path";
34
34
  import {
35
35
  LAYOUT_VERSION,
36
36
  customPoliciesDir,
37
37
  failproofaiHome,
38
+ hookActivityDir,
39
+ legacy,
38
40
  policiesDir,
39
41
  resettablePaths,
40
42
  } from "./fp-home";
@@ -52,6 +54,8 @@ export interface ResetOutcome {
52
54
  removed: string[];
53
55
  /** Basenames of user-authored policy files moved into `custom-policies/`. */
54
56
  migrated: string[];
57
+ /** Basenames of decision-log pages carried into layout 2's `hook-activity/`. */
58
+ activity: string[];
55
59
  /** The layout that was found before the reset. */
56
60
  from: number;
57
61
  }
@@ -110,6 +114,94 @@ export function migrateConventionPolicies(): string[] {
110
114
  return moved.sort((a, b) => a.localeCompare(b));
111
115
  }
112
116
 
117
+ /**
118
+ * Carry layout 1's decision log into layout 2, WITHOUT re-shipping it.
119
+ *
120
+ * `cache/hook-activity` was deleted along with the rest of `cache/`, so an
121
+ * upgrade silently discarded every decision the machine had ever recorded —
122
+ * the data the dashboard's activity tab exists to show, and on a connected
123
+ * machine the data an operator had already been billed the collection of.
124
+ *
125
+ * # Why this MOVES rather than copies
126
+ *
127
+ * The collector keys its cursors on `(device, inode)` — deliberately, because
128
+ * the store rotates by RENAMING `current.jsonl`, and a path-keyed cursor would
129
+ * both re-ship the rotated file and carry its offset onto the fresh one.
130
+ * `rename()` preserves the inode, so a moved page is still the file its cursor
131
+ * belongs to and resumes at the right offset. A copy gives every page a NEW
132
+ * inode, which reads as "never seen before" and re-ships all of it.
133
+ *
134
+ * `head_fingerprint` is what makes that safe rather than lucky: the cursor
135
+ * verifies the file's first bytes whenever a resumed cursor's path has changed,
136
+ * which is exactly this situation. It was added for inode REUSE; a migration is
137
+ * the same question asked the other way round.
138
+ *
139
+ * The fallback for `EXDEV` (a `cache/` on a different filesystem — rare, since
140
+ * both live under one home, but possible with bind mounts) is a copy, accepting
141
+ * that those pages re-ship. Ingest dedups on a content hash and collapses them
142
+ * at merge, so the cost is bandwidth, not duplicate rows.
143
+ *
144
+ * # What is deliberately NOT carried
145
+ *
146
+ * `current.count` and `stats.json` are derived state, and two of each cannot be
147
+ * merged without inventing a number. They are dropped and the store rebuilds
148
+ * them. The legacy `current.jsonl` is moved under a PAGE name rather than onto
149
+ * the destination's own `current.jsonl`, which may already exist and may be
150
+ * mid-write — a rotated page is exactly what the store would have made of it.
151
+ */
152
+ export function migrateHookActivity(): string[] {
153
+ const from = legacy.hookActivityDir();
154
+ if (!existsSync(from)) return [];
155
+ const moved: string[] = [];
156
+ try {
157
+ const entries = readdirSync(from, { withFileTypes: true }).filter(
158
+ (e) => e.isFile() && e.name.endsWith(".jsonl"),
159
+ );
160
+ if (entries.length === 0) return [];
161
+
162
+ const to = hookActivityDir();
163
+ mkdirSync(to, { recursive: true });
164
+
165
+ // One timestamp for the whole migration, with a per-file counter, so the
166
+ // names are stable, ordered, and cannot collide with each other.
167
+ const stamp = Date.now();
168
+ let seq = 0;
169
+
170
+ for (const entry of entries) {
171
+ const source = resolve(from, entry.name);
172
+ // `current.jsonl` becomes a page: the destination has its own, and the
173
+ // store's reader treats pages and current identically.
174
+ let name = entry.name === "current.jsonl" ? `page-${stamp}-${seq++}.jsonl` : entry.name;
175
+ let target = resolve(to, name);
176
+ // Never overwrite. A same-named page in the destination is a different
177
+ // file with different records, and losing either is worse than a rename.
178
+ while (existsSync(target)) {
179
+ name = `page-${stamp}-${seq++}.jsonl`;
180
+ target = resolve(to, name);
181
+ }
182
+ try {
183
+ renameSync(source, target);
184
+ moved.push(name);
185
+ } catch (err) {
186
+ // EXDEV: a rename across filesystems. Copy instead and accept the
187
+ // re-ship; a page left behind would be data lost.
188
+ if ((err as NodeJS.ErrnoException)?.code === "EXDEV") {
189
+ try {
190
+ copyFileSync(source, target);
191
+ moved.push(name);
192
+ } catch {
193
+ // Unreadable or undeletable — leave it where it is. `cache/` is no
194
+ // longer deleted wholesale, so "left behind" means "still there".
195
+ }
196
+ }
197
+ }
198
+ }
199
+ } catch {
200
+ // An activity directory we cannot read is not worth aborting a reset over.
201
+ }
202
+ return moved.sort((a, b) => a.localeCompare(b));
203
+ }
204
+
113
205
  /**
114
206
  * Delete every path layout 1 or layout 2 could have written, then stamp
115
207
  * VERSION so the next run reads as current.
@@ -124,6 +216,7 @@ export function resetHome(from: number): ResetOutcome {
124
216
  // BEFORE the deletions, so a file that is mid-move is never one the reset
125
217
  // then walks over.
126
218
  const migrated = migrateConventionPolicies();
219
+ const activity = migrateHookActivity();
127
220
  const removed: string[] = [];
128
221
  for (const path of resettablePaths()) {
129
222
  if (!existsSync(path)) continue;
@@ -137,7 +230,7 @@ export function resetHome(from: number): ResetOutcome {
137
230
  }
138
231
  }
139
232
  writeVersionFile();
140
- return { removed, migrated, from };
233
+ return { removed, migrated, activity, from };
141
234
  }
142
235
 
143
236
  export interface LayoutCheck {
@@ -187,15 +280,19 @@ export async function checkLayoutForCli(): Promise<LayoutCheck> {
187
280
  }
188
281
 
189
282
  if (state.kind === "stale") {
190
- const { removed, migrated } = resetHome(state.found);
283
+ const { removed, migrated, activity } = resetHome(state.found);
191
284
  return {
192
285
  state,
193
286
  fatal: false,
194
287
  didReset: true,
195
288
  lines: [
196
289
  `failproofai reorganised ${failproofaiHome()} in this version.`,
197
- `Removed ${removed.length} item(s) from the old layout — policy config, activity`,
198
- `history and audit cache. Your downloaded daemon binary was kept.`,
290
+ // "activity history" was in this sentence while the reset was deleting
291
+ // it. It is carried over now, so the message says what was KEPT as well
292
+ // as what went — a user who reads "removed" and nothing else has no way
293
+ // to know their decision log survived.
294
+ `Removed ${removed.length} item(s) from the old layout — policy config and`,
295
+ `audit cache. Your daemon binary and decision history were kept.`,
199
296
  // Named individually rather than counted. These are files a person
200
297
  // wrote; "moved 3 items" is not something you can check at a glance,
201
298
  // and the whole point of saying it is that they can.
@@ -207,6 +304,16 @@ export async function checkLayoutForCli(): Promise<LayoutCheck> {
207
304
  ...migrated.map((name) => ` ${name}`),
208
305
  ]
209
306
  : []),
307
+ // Counted, not named. Unlike policy files these are machine-written
308
+ // pages with generated names — a list of them tells the reader nothing
309
+ // they could act on, where the COUNT answers the only question they
310
+ // have: did my history survive.
311
+ ...(activity.length > 0
312
+ ? [
313
+ ``,
314
+ `Carried ${activity.length} page(s) of decision history into ${hookActivityDir()}.`,
315
+ ]
316
+ : []),
210
317
  ``,
211
318
  `Run \`failproofai config\` to set up again.`,
212
319
  ],