failproofai 1.0.0-beta.2 → 1.0.0-beta.3

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 (196) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  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/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  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/build-manifest.json +3 -3
  17. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  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.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  37. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  38. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  39. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/index.html +1 -1
  42. package/.next/standalone/.next/server/app/index.rsc +15 -15
  43. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  44. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  46. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  47. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  48. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  49. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  53. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
  54. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  55. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  58. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  67. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  68. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
  75. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ath6v_._.js → [root-of-the-server]__0ls0gsr._.js} +3 -3
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__10s_z4l._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
  85. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0nb0syg._.js → [root-of-the-server]__1ky3im9._.js} +2 -2
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0oy_-36.js → node_modules_html-to-image_es_index_0jfts82.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  93. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  94. package/.next/standalone/.next/server/pages/404.html +1 -1
  95. package/.next/standalone/.next/server/pages/500.html +1 -1
  96. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  97. package/.next/standalone/.next/server/server-reference-manifest.json +32 -20
  98. package/.next/standalone/.next/static/chunks/{3s7myk_co0gbh.js → 01h6lfjz1_oie.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 0a7k_x4rp_97z.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 0cvd6bcdwchc_.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 13-obgukzg_ke.js} +1 -1
  103. package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
  104. package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 1h0_yaoq5f3yx.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 29g8ykjk6sx_8.js} +1 -1
  106. package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 2rqm-klet_gbl.js} +1 -1
  107. package/.next/standalone/.next/static/chunks/{31feojxhjyjpl.js → 2uvyh8y0naofj.js} +1 -1
  108. package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
  109. package/.next/standalone/.next/static/chunks/3p-n_j80zlzi3.js +6 -0
  110. package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
  111. package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
  112. package/.next/standalone/Cargo.lock +1992 -198
  113. package/.next/standalone/Cargo.toml +1 -1
  114. package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
  115. package/.next/standalone/app/components/pause-notices.tsx +111 -0
  116. package/.next/standalone/app/policies/hooks-client.tsx +81 -6
  117. package/.next/standalone/crates/CLOUD_POLICIES.md +136 -0
  118. package/.next/standalone/crates/failproofaid/Cargo.toml +7 -0
  119. package/.next/standalone/crates/failproofaid/src/cloud_client.rs +484 -0
  120. package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +921 -0
  121. package/.next/standalone/crates/failproofaid/src/main.rs +452 -3
  122. package/.next/standalone/crates/failproofaid/src/paths.rs +29 -0
  123. package/.next/standalone/crates/fpai-collect/Cargo.toml +43 -0
  124. package/.next/standalone/crates/fpai-collect/src/config.rs +412 -0
  125. package/.next/standalone/crates/fpai-collect/src/cursor.rs +520 -0
  126. package/.next/standalone/crates/fpai-collect/src/delivery.rs +472 -0
  127. package/.next/standalone/crates/fpai-collect/src/filetail.rs +484 -0
  128. package/.next/standalone/crates/fpai-collect/src/health.rs +369 -0
  129. package/.next/standalone/crates/fpai-collect/src/lib.rs +34 -0
  130. package/.next/standalone/crates/fpai-collect/src/redact.rs +516 -0
  131. package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +279 -0
  132. package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +619 -0
  133. package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +156 -0
  134. package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +715 -0
  135. package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +158 -0
  136. package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +512 -0
  137. package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +511 -0
  138. package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +586 -0
  139. package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +318 -0
  140. package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +579 -0
  141. package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +310 -0
  142. package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +448 -0
  143. package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +23 -0
  144. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +176 -0
  145. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +500 -0
  146. package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +315 -0
  147. package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +493 -0
  148. package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +201 -0
  149. package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +533 -0
  150. package/.next/standalone/crates/fpai-collect/src/spool.rs +458 -0
  151. package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +387 -0
  152. package/.next/standalone/crates/fpai-collect/src/supervisor.rs +423 -0
  153. package/.next/standalone/crates/fpai-collect/src/uploader.rs +600 -0
  154. package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +738 -0
  155. package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +393 -0
  156. package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +975 -0
  157. package/.next/standalone/crates/fpai-collect/tests/config.rs +286 -0
  158. package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1019 -0
  159. package/.next/standalone/crates/fpai-collect/tests/delivery.rs +281 -0
  160. package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1008 -0
  161. package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1129 -0
  162. package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +621 -0
  163. package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +854 -0
  164. package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +1478 -0
  165. package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1191 -0
  166. package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +290 -0
  167. package/.next/standalone/crates/fpai-collect/tests/uploader.rs +506 -0
  168. package/.next/standalone/package.json +2 -2
  169. package/.next/standalone/server.js +1 -1
  170. package/bin/failproofai.mjs +129 -0
  171. package/dist/cli.mjs +2195 -770
  172. package/dist/worker.mjs +365 -52
  173. package/package.json +2 -2
  174. package/src/hooks/builtin-policies.ts +139 -0
  175. package/src/hooks/cloud-connection.ts +205 -0
  176. package/src/hooks/cloud-enrollment-cli.ts +227 -0
  177. package/src/hooks/cloud-enrollment.ts +173 -0
  178. package/src/hooks/cloud-managed-policies.ts +138 -0
  179. package/src/hooks/collector-config.ts +226 -0
  180. package/src/hooks/configure-wizard.ts +221 -5
  181. package/src/hooks/custom-hooks-loader.ts +69 -25
  182. package/src/hooks/daemon-client.ts +18 -0
  183. package/src/hooks/handler.ts +144 -6
  184. package/src/hooks/hook-activity-store.ts +50 -0
  185. package/src/hooks/loader-utils.ts +6 -4
  186. package/src/hooks/policy-types.ts +35 -0
  187. package/src/hooks/session-pause-cli.ts +157 -0
  188. package/src/hooks/session-pause.ts +236 -0
  189. package/src/hooks/tui.ts +115 -0
  190. package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
  191. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
  192. package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
  193. package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
  194. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_buildManifest.js +0 -0
  195. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_clientMiddlewareManifest.js +0 -0
  196. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_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.2";
22
+ var version2 = "1.0.0-beta.3";
23
23
  var init_package = () => {};
24
24
 
25
25
  // src/posthog-key.ts
@@ -332,6 +332,15 @@ function readHooksConfig() {
332
332
  return { enabledPolicies: [] };
333
333
  }
334
334
  }
335
+ function writeHooksConfig(config) {
336
+ const configPath = getConfigPath();
337
+ const dir = dirname(configPath);
338
+ if (!existsSync2(dir)) {
339
+ mkdirSync2(dir, { recursive: true });
340
+ }
341
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + `
342
+ `, "utf8");
343
+ }
335
344
  function getConfigPathForScope(scope, cwd) {
336
345
  switch (scope) {
337
346
  case "user":
@@ -397,11 +406,20 @@ var init_hooks_config = __esm(() => {
397
406
  var exports_daemon_client = {};
398
407
  __export(exports_daemon_client, {
399
408
  tryDaemonHook: () => tryDaemonHook,
400
- isDaemonConfigured: () => isDaemonConfigured
409
+ isDaemonConfigured: () => isDaemonConfigured,
410
+ daemonSocketPresent: () => daemonSocketPresent
401
411
  });
402
412
  import { createConnection } from "node:net";
403
413
  import { resolve as resolve2 } from "node:path";
404
414
  import { homedir as homedir3 } from "node:os";
415
+ import { existsSync as existsSync3 } from "node:fs";
416
+ function daemonSocketPresent() {
417
+ try {
418
+ return existsSync3(socketPath());
419
+ } catch {
420
+ return false;
421
+ }
422
+ }
405
423
  function socketPath() {
406
424
  if (process.env.FAILPROOFAI_DAEMON_SOCKET)
407
425
  return process.env.FAILPROOFAI_DAEMON_SOCKET;
@@ -1132,6 +1150,41 @@ function getFilePath(ctx) {
1132
1150
  function parseArgvTokens(cmd) {
1133
1151
  return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
1134
1152
  }
1153
+ function decodeAnsiC(body) {
1154
+ return body.replace(/\\(x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|[0-7]{1,3}|.)/g, (_, seq) => {
1155
+ try {
1156
+ if (seq[0] === "x")
1157
+ return String.fromCharCode(parseInt(seq.slice(1), 16));
1158
+ if (seq[0] === "u" || seq[0] === "U")
1159
+ return String.fromCodePoint(parseInt(seq.slice(1), 16));
1160
+ if (/^[0-7]+$/.test(seq))
1161
+ return String.fromCharCode(parseInt(seq, 8));
1162
+ } catch {
1163
+ return seq;
1164
+ }
1165
+ const named = {
1166
+ a: "\x07",
1167
+ b: "\b",
1168
+ e: "\x1B",
1169
+ f: "\f",
1170
+ n: `
1171
+ `,
1172
+ r: "\r",
1173
+ t: "\t",
1174
+ v: "\v",
1175
+ "\\": "\\",
1176
+ "'": "'",
1177
+ '"': '"',
1178
+ "?": "?"
1179
+ };
1180
+ return named[seq] ?? seq;
1181
+ });
1182
+ }
1183
+ function stripShellQuoting(command) {
1184
+ const joined = command.replace(/\\\r?\n/g, "");
1185
+ const ansiDecoded = joined.replace(/\$'((?:[^'\\]|\\.)*)'/g, (_, body) => decodeAnsiC(body));
1186
+ return ansiDecoded.replace(/\\(.)/g, "$1").replace(/['"]/g, "");
1187
+ }
1135
1188
  function statGitHeadMtimeMs(cwd) {
1136
1189
  try {
1137
1190
  return statSync3(join2(cwd, ".git", "HEAD")).mtimeMs;
@@ -1394,6 +1447,15 @@ function blockSudo(ctx) {
1394
1447
  }
1395
1448
  return allow();
1396
1449
  }
1450
+ function blockSelfPause(ctx) {
1451
+ if (ctx.toolName !== "Bash")
1452
+ return allow();
1453
+ const cmd = getCommand(ctx);
1454
+ if (SELF_PAUSE_RE.test(cmd) || SELF_PAUSE_RE.test(stripShellQuoting(cmd))) {
1455
+ return deny("Pausing failproofai enforcement is a human action, not an agent one. " + "If a policy is blocking legitimate work, say so and let the operator decide.");
1456
+ }
1457
+ return allow();
1458
+ }
1397
1459
  function blockCurlPipeSh(ctx) {
1398
1460
  if (ctx.toolName !== "Bash")
1399
1461
  return allow();
@@ -2131,7 +2193,7 @@ function registerBuiltinPolicies(enabledNames) {
2131
2193
  }
2132
2194
  }
2133
2195
  }
2134
- var isClaudeInternalPath, isClaudeSettingsFile, SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, SUDO_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, PS_WEB_PIPE_RE, SHORT_FLAG_BUNDLE_RE, SAFE_FORCE_PREFIXES, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_CLI_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, KUBECTL_RE, TERRAFORM_RE, AWS_CLI_RE, GCLOUD_RE, AZ_CLI_RE, HELM_RE, GH_PIPELINE_RE, gitBranchCache, GIT_BRANCH_CACHE_MAX_ENTRIES = 500, HOME_PREFIX_RE, RM_CMD_RE, FIND_CMD_RE, FIND_EXEC_RE, FIND_GLOBAL_OPT_RE, FIND_EXPR_START_RE, SCRATCH_ROOTS, CATASTROPHIC_DEPTH = 2, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
2196
+ var isClaudeInternalPath, isClaudeSettingsFile, SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, SUDO_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, SELF_PAUSE_RE, PS_WEB_PIPE_RE, SHORT_FLAG_BUNDLE_RE, SAFE_FORCE_PREFIXES, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_CLI_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, KUBECTL_RE, TERRAFORM_RE, AWS_CLI_RE, GCLOUD_RE, AZ_CLI_RE, HELM_RE, GH_PIPELINE_RE, gitBranchCache, GIT_BRANCH_CACHE_MAX_ENTRIES = 500, HOME_PREFIX_RE, RM_CMD_RE, FIND_CMD_RE, FIND_EXEC_RE, FIND_GLOBAL_OPT_RE, FIND_EXPR_START_RE, SCRATCH_ROOTS, CATASTROPHIC_DEPTH = 2, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
2135
2197
  var init_builtin_policies = __esm(() => {
2136
2198
  init_hook_logger();
2137
2199
  isClaudeInternalPath = isAgentInternalPath;
@@ -2172,6 +2234,7 @@ var init_builtin_policies = __esm(() => {
2172
2234
  PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
2173
2235
  RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
2174
2236
  CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
2237
+ SELF_PAUSE_RE = /failproofai[^\s]*(?:\s+\S+)*?\s+(?:config|configure|setup)\b(?:\s+\S+)*?\s+--pause\b/;
2175
2238
  PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
2176
2239
  SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
2177
2240
  SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"];
@@ -2319,6 +2382,16 @@ var init_builtin_policies = __esm(() => {
2319
2382
  }
2320
2383
  }
2321
2384
  },
2385
+ {
2386
+ name: "block-self-pause",
2387
+ displayTitle: "Tried to pause failproofai enforcement",
2388
+ impact: "An agent that can pause enforcement can switch off every other policy.",
2389
+ description: "Block agents from pausing failproofai enforcement",
2390
+ fn: blockSelfPause,
2391
+ match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
2392
+ defaultEnabled: true,
2393
+ category: "Dangerous Commands"
2394
+ },
2322
2395
  {
2323
2396
  name: "block-sudo",
2324
2397
  displayTitle: "Tried to run a command with sudo",
@@ -3628,8 +3701,14 @@ function clearCustomHooks() {
3628
3701
  const g = globalThis;
3629
3702
  g[REGISTRY_KEY2] = [];
3630
3703
  }
3631
- var REGISTRY_KEY2 = "__failproofai_custom_hooks__";
3632
- var init_custom_hooks_registry = () => {};
3704
+ var REGISTRY_KEY2 = "__failproofai_custom_hooks__", customPolicies;
3705
+ var init_custom_hooks_registry = __esm(() => {
3706
+ customPolicies = {
3707
+ add(hook) {
3708
+ getRegistry2().push(hook);
3709
+ }
3710
+ };
3711
+ });
3633
3712
 
3634
3713
  // src/hooks/loader-utils.ts
3635
3714
  import { readFile as readFile2, writeFile as writeFile2, unlink, access } from "fs/promises";
@@ -3670,8 +3749,8 @@ async function resolveLocalImport(fromDir, specifier) {
3670
3749
  }
3671
3750
  return null;
3672
3751
  }
3673
- async function createEsmShim(distIndex, distUrl) {
3674
- const shimPath = distIndex + ".__failproofai_esm_shim__.mjs";
3752
+ async function createEsmShim(distIndex, distUrl, tmpSuffix = TMP_SUFFIX) {
3753
+ const shimPath = `${distIndex}${tmpSuffix}.shim.mjs`;
3675
3754
  const shimCode = [
3676
3755
  `import _cjs from '${distUrl}';`,
3677
3756
  `export const customPolicies = _cjs.customPolicies;`,
@@ -3686,13 +3765,13 @@ async function createEsmShim(distIndex, distUrl) {
3686
3765
  await writeFile2(shimPath, shimCode, "utf-8");
3687
3766
  return { shimPath, shimUrl: pathToFileURL(shimPath).href };
3688
3767
  }
3689
- async function rewriteFileTree(entryPath, distUrl, distIndex) {
3768
+ async function rewriteFileTree(entryPath, distUrl, distIndex, tmpSuffix = TMP_SUFFIX) {
3690
3769
  const queue = [entryPath];
3691
3770
  const visited = new Set;
3692
3771
  const tmpFiles = [];
3693
3772
  let esmShimUrl = null;
3694
3773
  if (distIndex && distUrl) {
3695
- const shim = await createEsmShim(distIndex, distUrl);
3774
+ const shim = await createEsmShim(distIndex, distUrl, tmpSuffix);
3696
3775
  tmpFiles.push(shim.shimPath);
3697
3776
  esmShimUrl = shim.shimUrl;
3698
3777
  }
@@ -3723,7 +3802,7 @@ async function rewriteFileTree(entryPath, distUrl, distIndex) {
3723
3802
  if (!visited.has(resolved2) && !queue.includes(resolved2)) {
3724
3803
  queue.push(resolved2);
3725
3804
  }
3726
- let relPath = relative(dir, resolved2 + TMP_SUFFIX).split("\\").join("/");
3805
+ let relPath = relative(dir, resolved2 + tmpSuffix).split("\\").join("/");
3727
3806
  if (!relPath.startsWith("."))
3728
3807
  relPath = "./" + relPath;
3729
3808
  rewrites.set(specifier, relPath);
@@ -3736,7 +3815,7 @@ async function rewriteFileTree(entryPath, distUrl, distIndex) {
3736
3815
  code = code.replace(new RegExp(`'${escaped}'`, "g"), `'${replacement}'`);
3737
3816
  code = code.replace(new RegExp(`"${escaped}"`, "g"), `"${replacement}"`);
3738
3817
  }
3739
- const tmpPath = filePath + TMP_SUFFIX;
3818
+ const tmpPath = filePath + tmpSuffix;
3740
3819
  await writeFile2(tmpPath, code, "utf-8");
3741
3820
  tmpFiles.push(tmpPath);
3742
3821
  }
@@ -3757,11 +3836,13 @@ var init_loader_utils = __esm(() => {
3757
3836
 
3758
3837
  // src/hooks/custom-hooks-loader.ts
3759
3838
  import { resolve as resolve5, isAbsolute, basename } from "node:path";
3760
- import { existsSync as existsSync3, readdirSync } from "node:fs";
3839
+ import { existsSync as existsSync4, readdirSync } from "node:fs";
3840
+ import { readFile as readFile3 } from "node:fs/promises";
3761
3841
  import { pathToFileURL as pathToFileURL2 } from "node:url";
3762
3842
  import { homedir as homedir5 } from "node:os";
3843
+ import { createHash } from "node:crypto";
3763
3844
  function discoverPolicyFiles(dir) {
3764
- if (!existsSync3(dir))
3845
+ if (!existsSync4(dir))
3765
3846
  return [];
3766
3847
  try {
3767
3848
  const entries = readdirSync(dir, { withFileTypes: true });
@@ -3771,7 +3852,7 @@ function discoverPolicyFiles(dir) {
3771
3852
  }
3772
3853
  }
3773
3854
  function findSkippedPolicyFiles(dir) {
3774
- if (!existsSync3(dir))
3855
+ if (!existsSync4(dir))
3775
3856
  return [];
3776
3857
  try {
3777
3858
  return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && LOADABLE_EXT_RE.test(e.name) && !CONVENTION_FILE_RE.test(e.name) && !e.name.endsWith(".d.ts")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
@@ -3786,10 +3867,24 @@ async function loadSingleFile(absPath, opts) {
3786
3867
  try {
3787
3868
  const distIndex = await findDistIndex();
3788
3869
  const distUrl = distIndex ? pathToFileURL2(distIndex).href : null;
3789
- tmpFiles = await rewriteFileTree(absPath, distUrl, distIndex);
3790
- const entryTmp = absPath + TMP_SUFFIX;
3791
- const fileUrl = `${pathToFileURL2(entryTmp).href}?v=${++loadSequence}`;
3870
+ const sequence = ++loadSequence;
3871
+ const tmpSuffix = `${TMP_SUFFIX}.${process.pid}.${sequence}.mjs`;
3872
+ tmpFiles = await rewriteFileTree(absPath, distUrl, distIndex, tmpSuffix);
3873
+ const fingerprint = await fingerprintTemporaryTree(tmpFiles, tmpSuffix);
3874
+ const cached = policyModuleCache.get(absPath);
3875
+ if (cached?.fingerprint === fingerprint) {
3876
+ for (const hook of cached.hooks)
3877
+ customPolicies.add({ ...hook });
3878
+ return;
3879
+ }
3880
+ const entryTmp = absPath + tmpSuffix;
3881
+ const fileUrl = pathToFileURL2(entryTmp).href;
3882
+ const hooksBefore = getCustomHooks().length;
3792
3883
  await import(fileUrl);
3884
+ policyModuleCache.set(absPath, {
3885
+ fingerprint,
3886
+ hooks: getCustomHooks().slice(hooksBefore).map((hook) => ({ ...hook }))
3887
+ });
3793
3888
  } catch (err) {
3794
3889
  const msg = err instanceof Error ? err.message : String(err);
3795
3890
  const errorType = /Cannot find module|MODULE_NOT_FOUND|ENOENT/i.test(msg) ? "module_not_found" : /SyntaxError|Unexpected token/i.test(msg) ? "syntax_error" : "runtime_error";
@@ -3807,11 +3902,20 @@ async function loadSingleFile(absPath, opts) {
3807
3902
  await cleanupTmpFiles(tmpFiles);
3808
3903
  }
3809
3904
  }
3905
+ async function fingerprintTemporaryTree(tmpFiles, tmpSuffix) {
3906
+ const hash = createHash("sha256");
3907
+ for (const tmpFile of [...tmpFiles].sort()) {
3908
+ hash.update(tmpFile.replaceAll(tmpSuffix, TMP_SUFFIX));
3909
+ const contents = await readFile3(tmpFile, "utf8");
3910
+ hash.update(contents.replaceAll(tmpSuffix, TMP_SUFFIX));
3911
+ }
3912
+ return hash.digest("hex");
3913
+ }
3810
3914
  async function loadCustomHooks(customPoliciesPath, opts) {
3811
3915
  if (!customPoliciesPath)
3812
3916
  return [];
3813
3917
  const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve5(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
3814
- if (!existsSync3(absPath)) {
3918
+ if (!existsSync4(absPath)) {
3815
3919
  if (opts?.strict)
3816
3920
  throw new Error(`Custom hooks file not found: ${absPath}`);
3817
3921
  hookLogWarn(`customPoliciesPath not found: ${absPath}`);
@@ -3840,15 +3944,23 @@ async function loadAllCustomHooks(customPoliciesPaths, opts) {
3840
3944
  const projectRoot = findProjectConfigDir(opts?.sessionCwd ?? process.cwd());
3841
3945
  const conventionEnabled = opts?.customPoliciesEnabled !== false;
3842
3946
  const loadedPaths = new Set;
3947
+ const cloudManagedByPath = new Map((opts?.cloudManagedPolicies ?? []).map((policy) => [resolve5(policy.path), policy]));
3843
3948
  for (const customPoliciesPath of typeof customPoliciesPaths === "string" ? [customPoliciesPaths] : customPoliciesPaths ?? []) {
3844
3949
  const absPath = resolve5(projectRoot, customPoliciesPath);
3845
- if (existsSync3(absPath)) {
3950
+ if (existsSync4(absPath)) {
3846
3951
  if (!loadedPaths.has(absPath)) {
3847
3952
  loadedPaths.add(absPath);
3848
3953
  const hooksBefore = getCustomHooks().length;
3849
3954
  await loadSingleFile(absPath);
3850
3955
  for (const hook of getCustomHooks().slice(hooksBefore)) {
3851
- hook.__policyId = customPolicyId(absPath, hook.name);
3956
+ const cloudManaged = cloudManagedByPath.get(absPath);
3957
+ const tagged = hook;
3958
+ if (cloudManaged) {
3959
+ tagged.__cloudManaged = cloudManaged;
3960
+ tagged.__policyId = `cloud:${cloudManaged.id}@${cloudManaged.revision}:${hook.name}`;
3961
+ } else {
3962
+ tagged.__policyId = customPolicyId(absPath, hook.name);
3963
+ }
3852
3964
  }
3853
3965
  }
3854
3966
  } else {
@@ -3920,7 +4032,7 @@ async function loadAllCustomHooks(customPoliciesPaths, opts) {
3920
4032
  }
3921
4033
  return { hooks: allHooks, conventionSources };
3922
4034
  }
3923
- var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__", CONVENTION_FILE_RE, loadSequence = 0, LOADABLE_EXT_RE;
4035
+ var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__", CONVENTION_FILE_RE, loadSequence = 0, policyModuleCache, LOADABLE_EXT_RE;
3924
4036
  var init_custom_hooks_loader = __esm(() => {
3925
4037
  init_hook_logger();
3926
4038
  init_custom_hooks_registry();
@@ -3929,6 +4041,7 @@ var init_custom_hooks_loader = __esm(() => {
3929
4041
  init_hook_telemetry2();
3930
4042
  init_telemetry_id2();
3931
4043
  CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
4044
+ policyModuleCache = new Map;
3932
4045
  LOADABLE_EXT_RE = /\.(js|mjs|ts)$/;
3933
4046
  });
3934
4047
 
@@ -3940,14 +4053,14 @@ import {
3940
4053
  renameSync as renameSync2,
3941
4054
  readdirSync as readdirSync2,
3942
4055
  mkdirSync as mkdirSync3,
3943
- existsSync as existsSync4,
4056
+ existsSync as existsSync5,
3944
4057
  statSync as statSync4,
3945
4058
  unlinkSync
3946
4059
  } from "node:fs";
3947
4060
  import { join as join3 } from "node:path";
3948
4061
  import { homedir as homedir6 } from "node:os";
3949
4062
  function ensureDir() {
3950
- if (!existsSync4(storeDir)) {
4063
+ if (!existsSync5(storeDir)) {
3951
4064
  mkdirSync3(storeDir, { recursive: true });
3952
4065
  }
3953
4066
  }
@@ -4019,6 +4132,13 @@ function writeCount(countPath, n) {
4019
4132
  writeFileSync2(countPath, String(n), "utf-8");
4020
4133
  } catch {}
4021
4134
  }
4135
+ function readFileSafe(filePath) {
4136
+ try {
4137
+ return readFileSync2(filePath, "utf-8");
4138
+ } catch {
4139
+ return "";
4140
+ }
4141
+ }
4022
4142
  function readStoredStats() {
4023
4143
  try {
4024
4144
  return JSON.parse(readFileSync2(join3(storeDir, STATS_FILE), "utf-8"));
@@ -4048,6 +4168,49 @@ function updateStats(entry) {
4048
4168
  } catch {}
4049
4169
  }
4050
4170
  }
4171
+ function getAllHookActivityEntries() {
4172
+ ensureDir();
4173
+ const currentPath = join3(storeDir, CURRENT_FILE);
4174
+ const currentEntries = readJsonlFile(currentPath).reverse();
4175
+ const archives = getArchiveFiles();
4176
+ const archiveEntries = [];
4177
+ for (const file of archives) {
4178
+ const entries = readJsonlFile(join3(storeDir, file));
4179
+ archiveEntries.push(...entries.reverse());
4180
+ }
4181
+ return [...currentEntries, ...archiveEntries];
4182
+ }
4183
+ function readJsonlFile(filePath) {
4184
+ const content = readFileSafe(filePath);
4185
+ if (!content.trim())
4186
+ return [];
4187
+ const entries = [];
4188
+ for (const line of content.trim().split(`
4189
+ `)) {
4190
+ try {
4191
+ entries.push(JSON.parse(line));
4192
+ } catch {}
4193
+ }
4194
+ return entries;
4195
+ }
4196
+ function getArchiveFiles() {
4197
+ try {
4198
+ const files = readdirSync2(storeDir);
4199
+ return files.filter((f) => f.startsWith("page-") && f.endsWith(".jsonl")).sort((a, b) => {
4200
+ const partsA = a.slice(5, -6).split("-");
4201
+ const partsB = b.slice(5, -6).split("-");
4202
+ const tsA = parseInt(partsA[0], 10);
4203
+ const tsB = parseInt(partsB[0], 10);
4204
+ if (tsA !== tsB)
4205
+ return tsB - tsA;
4206
+ const seqA = partsA.length > 1 ? parseInt(partsA[1], 10) : 0;
4207
+ const seqB = partsB.length > 1 ? parseInt(partsB[1], 10) : 0;
4208
+ return seqB - seqA;
4209
+ });
4210
+ } catch {
4211
+ return [];
4212
+ }
4213
+ }
4051
4214
  var PAGE_SIZE = 25, DEFAULT_STORE_DIR, CURRENT_FILE = "current.jsonl", COUNT_FILE = "current.count", STATS_FILE = "stats.json", LOCK_FILE = "current.lock", LOCK_STALE_MS = 2000, storeDir, rotateSeq = 0;
4052
4215
  var init_hook_activity_store = __esm(() => {
4053
4216
  DEFAULT_STORE_DIR = join3(homedir6(), ".failproofai", "cache", "hook-activity");
@@ -4395,7 +4558,7 @@ __export(exports_copilot_projects, {
4395
4558
  getCachedCopilotSessionsByEncodedName: () => getCachedCopilotSessionsByEncodedName,
4396
4559
  getCachedCopilotProjects: () => getCachedCopilotProjects
4397
4560
  });
4398
- import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "node:fs/promises";
4561
+ import { readdir as readdir2, readFile as readFile4, stat as stat2 } from "node:fs/promises";
4399
4562
  import { homedir as homedir9 } from "node:os";
4400
4563
  import { join as join6 } from "node:path";
4401
4564
  function getCopilotSessionStateRoot() {
@@ -4434,7 +4597,7 @@ async function scanCopilotSessions() {
4434
4597
  const settled = await batchAll(candidates.map((c) => async () => {
4435
4598
  let workspaceText;
4436
4599
  try {
4437
- workspaceText = await readFile3(c.workspacePath, "utf-8");
4600
+ workspaceText = await readFile4(c.workspacePath, "utf-8");
4438
4601
  } catch {
4439
4602
  return null;
4440
4603
  }
@@ -4543,7 +4706,7 @@ __export(exports_cursor_projects, {
4543
4706
  getCachedCursorSessionsByEncodedName: () => getCachedCursorSessionsByEncodedName,
4544
4707
  getCachedCursorProjects: () => getCachedCursorProjects
4545
4708
  });
4546
- import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "node:fs/promises";
4709
+ import { readdir as readdir3, readFile as readFile5, stat as stat3 } from "node:fs/promises";
4547
4710
  import { homedir as homedir10 } from "node:os";
4548
4711
  import { join as join7 } from "node:path";
4549
4712
  function getCursorHome() {
@@ -4584,7 +4747,7 @@ async function findFirstUsableMeta(dir, candidates) {
4584
4747
  continue;
4585
4748
  let text;
4586
4749
  try {
4587
- text = await readFile4(path3, "utf-8");
4750
+ text = await readFile5(path3, "utf-8");
4588
4751
  } catch {
4589
4752
  continue;
4590
4753
  }
@@ -4916,7 +5079,7 @@ __export(exports_pi_projects, {
4916
5079
  getCachedPiSessionsByEncodedName: () => getCachedPiSessionsByEncodedName,
4917
5080
  getCachedPiProjects: () => getCachedPiProjects
4918
5081
  });
4919
- import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "node:fs/promises";
5082
+ import { readdir as readdir4, readFile as readFile6, stat as stat4 } from "node:fs/promises";
4920
5083
  import { homedir as homedir11 } from "node:os";
4921
5084
  import { join as join8 } from "node:path";
4922
5085
  function getPiSessionsRoot() {
@@ -4939,7 +5102,7 @@ async function statMtime3(path3) {
4939
5102
  async function readSessionCwd(filePath) {
4940
5103
  let text;
4941
5104
  try {
4942
- text = await readFile5(filePath, "utf-8");
5105
+ text = await readFile6(filePath, "utf-8");
4943
5106
  } catch {
4944
5107
  return null;
4945
5108
  }
@@ -5080,7 +5243,7 @@ var init_pi_projects = __esm(() => {
5080
5243
  });
5081
5244
 
5082
5245
  // lib/sqlite-reader.ts
5083
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
5246
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
5084
5247
  import initSqlJs from "sql.js/dist/sql-asm.js";
5085
5248
  async function tryNodeSqlite(dbPath) {
5086
5249
  try {
@@ -5131,7 +5294,7 @@ async function trySqlJs(dbPath) {
5131
5294
  }
5132
5295
  }
5133
5296
  async function openSqliteReadonly(dbPath) {
5134
- if (!existsSync5(dbPath))
5297
+ if (!existsSync6(dbPath))
5135
5298
  return null;
5136
5299
  return await tryNodeSqlite(dbPath) ?? await trySqlJs(dbPath);
5137
5300
  }
@@ -5521,7 +5684,7 @@ var init_hermes_projects = __esm(() => {
5521
5684
  });
5522
5685
 
5523
5686
  // lib/openclaw-sessions.ts
5524
- import { readFile as readFile6 } from "node:fs/promises";
5687
+ import { readFile as readFile7 } from "node:fs/promises";
5525
5688
  import { readdirSync as readdirSync4, statSync as statSync5 } from "node:fs";
5526
5689
  import { homedir as homedir13 } from "node:os";
5527
5690
  import { join as join11 } from "node:path";
@@ -5693,7 +5856,7 @@ async function getOpenClawSessionLog(sessionId) {
5693
5856
  return null;
5694
5857
  let content;
5695
5858
  try {
5696
- content = await readFile6(filePath, "utf-8");
5859
+ content = await readFile7(filePath, "utf-8");
5697
5860
  } catch {
5698
5861
  return null;
5699
5862
  }
@@ -5890,7 +6053,7 @@ var init_openclaw_projects = __esm(() => {
5890
6053
  });
5891
6054
 
5892
6055
  // lib/factory-sessions.ts
5893
- import { readFile as readFile7 } from "node:fs/promises";
6056
+ import { readFile as readFile8 } from "node:fs/promises";
5894
6057
  import { readdirSync as readdirSync5, statSync as statSync6 } from "node:fs";
5895
6058
  import { homedir as homedir14 } from "node:os";
5896
6059
  import { join as join13 } from "node:path";
@@ -6081,7 +6244,7 @@ async function getFactorySessionLog(sessionId) {
6081
6244
  return null;
6082
6245
  let content;
6083
6246
  try {
6084
- content = await readFile7(filePath, "utf-8");
6247
+ content = await readFile8(filePath, "utf-8");
6085
6248
  } catch {
6086
6249
  return null;
6087
6250
  }
@@ -6485,7 +6648,7 @@ var init_devin_projects = __esm(() => {
6485
6648
  });
6486
6649
 
6487
6650
  // lib/antigravity-sessions.ts
6488
- import { readFile as readFile8 } from "node:fs/promises";
6651
+ import { readFile as readFile9 } from "node:fs/promises";
6489
6652
  import { readdirSync as readdirSync6, statSync as statSync7 } from "node:fs";
6490
6653
  import { homedir as homedir16 } from "node:os";
6491
6654
  import { join as join15 } from "node:path";
@@ -6645,7 +6808,7 @@ async function getAntigravitySessionLog(conversationId) {
6645
6808
  return null;
6646
6809
  let content;
6647
6810
  try {
6648
- content = await readFile8(filePath, "utf-8");
6811
+ content = await readFile9(filePath, "utf-8");
6649
6812
  } catch {
6650
6813
  return null;
6651
6814
  }
@@ -7321,7 +7484,7 @@ async function resolveSubagentPath(projectsPath, projectName, sessionId, agentId
7321
7484
  var init_resolve_subagent_path = () => {};
7322
7485
 
7323
7486
  // lib/log-entries.ts
7324
- import { readFile as readFile9 } from "fs/promises";
7487
+ import { readFile as readFile10 } from "fs/promises";
7325
7488
  import { join as join20 } from "path";
7326
7489
  function formatTimestamp(date) {
7327
7490
  const base = formatDate(date);
@@ -7521,7 +7684,7 @@ async function parseSessionLog(projectName, sessionId) {
7521
7684
  const projectDir = resolveProjectPath(projectName);
7522
7685
  const projectsPath = getClaudeProjectsPath();
7523
7686
  const filePath = join20(projectDir, `${sessionId}.jsonl`);
7524
- const fileContent = await readFile9(filePath, "utf-8");
7687
+ const fileContent = await readFile10(filePath, "utf-8");
7525
7688
  const { entries: sessionEntries, rawLines: sessionRawLines, subagentIds } = await parseFileContent(fileContent, "session");
7526
7689
  if (subagentIds.length === 0) {
7527
7690
  return { entries: sessionEntries, rawLines: sessionRawLines, subagentIds: [] };
@@ -7531,7 +7694,7 @@ async function parseSessionLog(projectName, sessionId) {
7531
7694
  const agentPath = await resolveSubagentPath(projectsPath, projectName, sessionId, agentId);
7532
7695
  if (!agentPath)
7533
7696
  return null;
7534
- const agentContent = await readFile9(agentPath, "utf-8");
7697
+ const agentContent = await readFile10(agentPath, "utf-8");
7535
7698
  const { entries, rawLines } = await parseFileContent(agentContent, agentSource);
7536
7699
  return { entries, rawLines };
7537
7700
  }), SUBAGENT_CONCURRENCY);
@@ -7557,13 +7720,13 @@ var init_log_entries = __esm(() => {
7557
7720
  });
7558
7721
 
7559
7722
  // lib/codex-sessions.ts
7560
- import { readFileSync as readFileSync5, readdirSync as readdirSync7, existsSync as existsSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, statSync as statSync8 } from "node:fs";
7561
- import { readFile as readFile10 } from "node:fs/promises";
7723
+ import { readFileSync as readFileSync5, readdirSync as readdirSync7, existsSync as existsSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, statSync as statSync8 } from "node:fs";
7724
+ import { readFile as readFile11 } from "node:fs/promises";
7562
7725
  import { dirname as dirname4, join as join21 } from "node:path";
7563
7726
  import { homedir as homedir18 } from "node:os";
7564
7727
  function readCache() {
7565
7728
  try {
7566
- if (!existsSync6(CACHE_PATH))
7729
+ if (!existsSync7(CACHE_PATH))
7567
7730
  return {};
7568
7731
  return JSON.parse(readFileSync5(CACHE_PATH, "utf-8"));
7569
7732
  } catch {
@@ -7591,7 +7754,7 @@ function dirSearch(dir, sessionId) {
7591
7754
  function findCodexTranscript(sessionId) {
7592
7755
  const cache = readCache();
7593
7756
  const cached = cache[sessionId];
7594
- if (cached && existsSync6(cached))
7757
+ if (cached && existsSync7(cached))
7595
7758
  return cached;
7596
7759
  const root = join21(homedir18(), ".codex", "sessions");
7597
7760
  const today = new Date;
@@ -7832,7 +7995,7 @@ async function getCodexSessionLog(sessionId) {
7832
7995
  const filePath = findCodexTranscript(sessionId);
7833
7996
  if (!filePath)
7834
7997
  return null;
7835
- const fileContent = await readFile10(filePath, "utf-8");
7998
+ const fileContent = await readFile11(filePath, "utf-8");
7836
7999
  const { entries, rawLines, cwd } = await parseCodexLog(fileContent, "session");
7837
8000
  return { entries, rawLines, cwd, filePath };
7838
8001
  }
@@ -7884,8 +8047,8 @@ var init_resolve_permission_mode = __esm(() => {
7884
8047
  });
7885
8048
 
7886
8049
  // lib/copilot-sessions.ts
7887
- import { readFileSync as readFileSync7, readdirSync as readdirSync8, existsSync as existsSync7, statSync as statSync9 } from "node:fs";
7888
- import { readFile as readFile11 } from "node:fs/promises";
8050
+ import { readFileSync as readFileSync7, readdirSync as readdirSync8, existsSync as existsSync8, statSync as statSync9 } from "node:fs";
8051
+ import { readFile as readFile12 } from "node:fs/promises";
7889
8052
  import { join as join22, resolve as resolve8, sep as sep2 } from "node:path";
7890
8053
  import { homedir as homedir19 } from "node:os";
7891
8054
  function getCopilotHome() {
@@ -7913,13 +8076,13 @@ function findCopilotTranscript(sessionId) {
7913
8076
  const candidate = resolveSessionFile(sessionId, "events.jsonl");
7914
8077
  if (!candidate)
7915
8078
  return null;
7916
- return existsSync7(candidate) ? candidate : null;
8079
+ return existsSync8(candidate) ? candidate : null;
7917
8080
  }
7918
8081
  function findCopilotWorkspace(sessionId) {
7919
8082
  const candidate = resolveSessionFile(sessionId, "workspace.yaml");
7920
8083
  if (!candidate)
7921
8084
  return null;
7922
- return existsSync7(candidate) ? candidate : null;
8085
+ return existsSync8(candidate) ? candidate : null;
7923
8086
  }
7924
8087
  function readYamlScalar(path3, key) {
7925
8088
  try {
@@ -8086,7 +8249,7 @@ async function getCopilotSessionLog(sessionId) {
8086
8249
  return null;
8087
8250
  let fileContent;
8088
8251
  try {
8089
- fileContent = await readFile11(filePath, "utf-8");
8252
+ fileContent = await readFile12(filePath, "utf-8");
8090
8253
  } catch {
8091
8254
  return null;
8092
8255
  }
@@ -8101,8 +8264,8 @@ var init_copilot_sessions = __esm(() => {
8101
8264
  });
8102
8265
 
8103
8266
  // lib/cursor-sessions.ts
8104
- import { readFileSync as readFileSync8, readdirSync as readdirSync9, existsSync as existsSync8, statSync as statSync10 } from "node:fs";
8105
- import { readFile as readFile12 } from "node:fs/promises";
8267
+ import { readFileSync as readFileSync8, readdirSync as readdirSync9, existsSync as existsSync9, statSync as statSync10 } from "node:fs";
8268
+ import { readFile as readFile13 } from "node:fs/promises";
8106
8269
  import { basename as basename3, join as join23, resolve as resolve9, sep as sep3 } from "node:path";
8107
8270
  import { homedir as homedir20 } from "node:os";
8108
8271
  function getCursorHome2() {
@@ -8124,7 +8287,7 @@ function getCursorSessionDir(sessionId) {
8124
8287
  const transcriptRoot = resolve9(projectsRoot, entry.name, NEW_AGENT_TRANSCRIPTS_DIR2);
8125
8288
  if (candidate === transcriptRoot || !candidate.startsWith(`${transcriptRoot}${sep3}`))
8126
8289
  continue;
8127
- if (existsSync8(candidate))
8290
+ if (existsSync9(candidate))
8128
8291
  return candidate;
8129
8292
  }
8130
8293
  for (const sub of LEGACY_SESSION_ROOT_CANDIDATES2) {
@@ -8132,7 +8295,7 @@ function getCursorSessionDir(sessionId) {
8132
8295
  const candidate = resolve9(root, sessionId);
8133
8296
  if (candidate === root || !candidate.startsWith(`${root}${sep3}`))
8134
8297
  continue;
8135
- if (existsSync8(candidate))
8298
+ if (existsSync9(candidate))
8136
8299
  return candidate;
8137
8300
  }
8138
8301
  return null;
@@ -8142,11 +8305,11 @@ function findCursorTranscript(sessionId) {
8142
8305
  if (!dir)
8143
8306
  return null;
8144
8307
  const newCandidate = join23(dir, `${basename3(dir)}.jsonl`);
8145
- if (existsSync8(newCandidate))
8308
+ if (existsSync9(newCandidate))
8146
8309
  return newCandidate;
8147
8310
  for (const name of LEGACY_TRANSCRIPT_FILE_CANDIDATES2) {
8148
8311
  const candidate = join23(dir, name);
8149
- if (existsSync8(candidate))
8312
+ if (existsSync9(candidate))
8150
8313
  return candidate;
8151
8314
  }
8152
8315
  return null;
@@ -8341,7 +8504,7 @@ async function getCursorSessionLog(sessionId) {
8341
8504
  return null;
8342
8505
  let fileContent;
8343
8506
  try {
8344
- fileContent = await readFile12(filePath, "utf-8");
8507
+ fileContent = await readFile13(filePath, "utf-8");
8345
8508
  } catch {
8346
8509
  return null;
8347
8510
  }
@@ -8357,8 +8520,8 @@ var init_cursor_sessions = __esm(() => {
8357
8520
  });
8358
8521
 
8359
8522
  // lib/pi-sessions.ts
8360
- import { readFileSync as readFileSync9, readdirSync as readdirSync10, existsSync as existsSync9, statSync as statSync11 } from "node:fs";
8361
- import { readFile as readFile13 } from "node:fs/promises";
8523
+ import { readFileSync as readFileSync9, readdirSync as readdirSync10, existsSync as existsSync10, statSync as statSync11 } from "node:fs";
8524
+ import { readFile as readFile14 } from "node:fs/promises";
8362
8525
  import { join as join24, resolve as resolve10, sep as sep4 } from "node:path";
8363
8526
  import { homedir as homedir21 } from "node:os";
8364
8527
  function getPiSessionStateRoot() {
@@ -8394,7 +8557,7 @@ function findPiTranscript(sessionId) {
8394
8557
  const candidate = resolve10(cwdPath, f);
8395
8558
  if (!candidate.startsWith(`${cwdPath}${sep4}`))
8396
8559
  continue;
8397
- if (existsSync9(candidate))
8560
+ if (existsSync10(candidate))
8398
8561
  return candidate;
8399
8562
  }
8400
8563
  }
@@ -8520,7 +8683,7 @@ async function getPiSessionLog(sessionId) {
8520
8683
  return null;
8521
8684
  let fileContent;
8522
8685
  try {
8523
- fileContent = await readFile13(filePath, "utf-8");
8686
+ fileContent = await readFile14(filePath, "utf-8");
8524
8687
  } catch {
8525
8688
  return null;
8526
8689
  }
@@ -8621,6 +8784,288 @@ async function readStdinPayload2(maxBytes = 1048576) {
8621
8784
  return { payload, oversized, readError };
8622
8785
  }
8623
8786
 
8787
+ // src/hooks/cloud-managed-policies.ts
8788
+ import { createHash as createHash2 } from "node:crypto";
8789
+ import { existsSync as existsSync11, readFileSync as readFileSync10, realpathSync } from "node:fs";
8790
+ import { homedir as homedir22 } from "node:os";
8791
+ import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve11 } from "node:path";
8792
+ function cloudManagedPolicyRoot() {
8793
+ return process.env.FAILPROOFAI_CLOUD_POLICY_DIR ?? resolve11(homedir22(), ".failproofai", "policies", "cloud-managed");
8794
+ }
8795
+ function parseManifest(value) {
8796
+ if (!value || typeof value !== "object")
8797
+ throw new Error("active manifest is not an object");
8798
+ const manifest = value;
8799
+ if (manifest.schemaVersion !== ACTIVE_SCHEMA_VERSION) {
8800
+ throw new Error(`unsupported active manifest schema ${String(manifest.schemaVersion)}`);
8801
+ }
8802
+ if (!Number.isSafeInteger(manifest.generation) || (manifest.generation ?? -1) < 0) {
8803
+ throw new Error("active manifest generation is invalid");
8804
+ }
8805
+ if (!Array.isArray(manifest.policies))
8806
+ throw new Error("active manifest policies is not an array");
8807
+ return manifest;
8808
+ }
8809
+ function resolveManagedPath(root, candidate) {
8810
+ if (!candidate || isAbsolute2(candidate))
8811
+ throw new Error(`unsafe managed policy path ${JSON.stringify(candidate)}`);
8812
+ const absolute = resolve11(root, candidate);
8813
+ const lexicalRelative = relative3(root, absolute);
8814
+ if (lexicalRelative.startsWith("..") || isAbsolute2(lexicalRelative)) {
8815
+ throw new Error(`managed policy path escapes its root: ${JSON.stringify(candidate)}`);
8816
+ }
8817
+ const realRoot = realpathSync(root);
8818
+ const realFile = realpathSync(absolute);
8819
+ const physicalRelative = relative3(realRoot, realFile);
8820
+ if (physicalRelative.startsWith("..") || isAbsolute2(physicalRelative)) {
8821
+ throw new Error(`managed policy symlink escapes its root: ${JSON.stringify(candidate)}`);
8822
+ }
8823
+ return realFile;
8824
+ }
8825
+ function readActiveCloudManagedPolicies() {
8826
+ const root = cloudManagedPolicyRoot();
8827
+ const activePath = resolve11(root, "active.json");
8828
+ if (!existsSync11(activePath))
8829
+ return [];
8830
+ let raw;
8831
+ try {
8832
+ raw = JSON.parse(readFileSync10(activePath, "utf8"));
8833
+ } catch (err) {
8834
+ throw new Error(`failed to read cloud-managed active manifest: ${err instanceof Error ? err.message : String(err)}`);
8835
+ }
8836
+ const manifest = parseManifest(raw);
8837
+ const seen = new Set;
8838
+ return manifest.policies.map((policy) => {
8839
+ if (!POLICY_ID_RE.test(policy.id) || policy.id === "." || policy.id === "..") {
8840
+ throw new Error(`unsafe cloud-managed policy id ${JSON.stringify(policy.id)}`);
8841
+ }
8842
+ if (!Number.isSafeInteger(policy.revision) || policy.revision < 0) {
8843
+ throw new Error(`invalid revision for cloud-managed policy ${policy.id}`);
8844
+ }
8845
+ if (!SHA256_RE.test(policy.sha256)) {
8846
+ throw new Error(`invalid SHA-256 for cloud-managed policy ${policy.id}`);
8847
+ }
8848
+ if (seen.has(policy.id))
8849
+ throw new Error(`duplicate cloud-managed policy id ${policy.id}`);
8850
+ seen.add(policy.id);
8851
+ const path3 = resolveManagedPath(root, policy.path);
8852
+ const bytes = readFileSync10(path3);
8853
+ const actual = createHash2("sha256").update(bytes).digest("hex");
8854
+ if (actual !== policy.sha256) {
8855
+ throw new Error(`cloud-managed policy ${policy.id} failed integrity verification: expected ${policy.sha256}, got ${actual}`);
8856
+ }
8857
+ if (policy.effect !== undefined && policy.effect !== "enforce" && policy.effect !== "observe") {
8858
+ throw new Error(`unknown effect ${JSON.stringify(policy.effect)} for cloud-managed policy ${policy.id}`);
8859
+ }
8860
+ return {
8861
+ id: policy.id,
8862
+ revision: policy.revision,
8863
+ effect: policy.effect ?? "enforce",
8864
+ sha256: policy.sha256,
8865
+ path: path3,
8866
+ generation: manifest.generation
8867
+ };
8868
+ });
8869
+ }
8870
+ var ACTIVE_SCHEMA_VERSION = 1, SHA256_RE, POLICY_ID_RE;
8871
+ var init_cloud_managed_policies = __esm(() => {
8872
+ SHA256_RE = /^[a-f0-9]{64}$/;
8873
+ POLICY_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
8874
+ });
8875
+
8876
+ // lib/atomic-write.ts
8877
+ import {
8878
+ chmodSync,
8879
+ existsSync as existsSync12,
8880
+ mkdirSync as mkdirSync5,
8881
+ renameSync as renameSync3,
8882
+ rmSync,
8883
+ statSync as statSync12,
8884
+ writeFileSync as writeFileSync4
8885
+ } from "node:fs";
8886
+ import { dirname as dirname5 } from "node:path";
8887
+ import { randomBytes } from "node:crypto";
8888
+ function writeJsonAtomically(filePath, value, opts = {}) {
8889
+ const mode = opts.mode ?? 384;
8890
+ const dirMode = opts.dirMode ?? 448;
8891
+ const dir = dirname5(filePath);
8892
+ if (!existsSync12(dir))
8893
+ mkdirSync5(dir, { recursive: true, mode: dirMode });
8894
+ const tmp = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
8895
+ try {
8896
+ writeFileSync4(tmp, JSON.stringify(value, null, 2), { mode });
8897
+ try {
8898
+ if ((statSync12(tmp).mode & 63) !== 0)
8899
+ chmodSync(tmp, mode);
8900
+ } catch {}
8901
+ renameSync3(tmp, filePath);
8902
+ try {
8903
+ if ((statSync12(filePath).mode & 63) !== 0)
8904
+ chmodSync(filePath, mode);
8905
+ } catch {}
8906
+ } catch (err) {
8907
+ try {
8908
+ rmSync(tmp, { force: true });
8909
+ } catch {}
8910
+ throw err;
8911
+ }
8912
+ }
8913
+ var init_atomic_write = () => {};
8914
+
8915
+ // src/hooks/session-pause.ts
8916
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync11, readFileSync as readFileSync11, rmSync as rmSync2 } from "node:fs";
8917
+ import { homedir as homedir23 } from "node:os";
8918
+ import { createHash as createHash3 } from "node:crypto";
8919
+ import { join as join25, resolve as resolve12 } from "node:path";
8920
+ function pauseStateDir() {
8921
+ const override = process.env.FAILPROOFAI_STATE_DIR;
8922
+ if (override)
8923
+ return resolve12(override, "sessions");
8924
+ return join25(homedir23(), ".failproofai", "state", "sessions");
8925
+ }
8926
+ function pauseFilePath(sessionId) {
8927
+ const digest = createHash3("sha256").update(sessionId).digest("hex").slice(0, 32);
8928
+ return join25(pauseStateDir(), `${digest}.json`);
8929
+ }
8930
+ function parsePauseDuration(input, ceilingMs = PAUSE_CEILING_MS) {
8931
+ if (input === undefined || input === "")
8932
+ return Math.min(PAUSE_DEFAULT_MS, ceilingMs);
8933
+ const match = /^(\d+)\s*(s|m|h)?$/i.exec(input.trim());
8934
+ if (!match) {
8935
+ throw new Error(`Invalid duration ${JSON.stringify(input)}. Use e.g. 30m, 2h, 90s (a bare number means minutes).`);
8936
+ }
8937
+ const value = Number(match[1]);
8938
+ const unit = (match[2] ?? "m").toLowerCase();
8939
+ const ms = unit === "s" ? value * 1000 : unit === "h" ? value * 3600000 : value * 60000;
8940
+ if (ms <= 0)
8941
+ throw new Error("Duration must be greater than zero.");
8942
+ if (ms > ceilingMs) {
8943
+ throw new Error(`Duration ${input} exceeds the maximum pause of ${formatDuration2(ceilingMs)}.`);
8944
+ }
8945
+ return ms;
8946
+ }
8947
+ function formatDuration2(ms) {
8948
+ if (ms < 60000)
8949
+ return `${Math.round(ms / 1000)}s`;
8950
+ const minutes = Math.round(ms / 60000);
8951
+ if (minutes < 60)
8952
+ return `${minutes}m`;
8953
+ const hours = Math.floor(minutes / 60);
8954
+ const rest = minutes % 60;
8955
+ return rest === 0 ? `${hours}h` : `${hours}h${rest}m`;
8956
+ }
8957
+ function parseStored(raw) {
8958
+ if (!raw || typeof raw !== "object")
8959
+ return null;
8960
+ const p = raw;
8961
+ if (p.schemaVersion !== SCHEMA_VERSION)
8962
+ return null;
8963
+ if (typeof p.sessionId !== "string" || p.sessionId.length === 0)
8964
+ return null;
8965
+ if (!Number.isFinite(p.pausedAt) || !Number.isFinite(p.expiresAt))
8966
+ return null;
8967
+ return {
8968
+ schemaVersion: SCHEMA_VERSION,
8969
+ sessionId: p.sessionId,
8970
+ pausedAt: p.pausedAt,
8971
+ expiresAt: p.expiresAt,
8972
+ setBy: typeof p.setBy === "string" ? p.setBy : "unknown",
8973
+ ...typeof p.cwd === "string" ? { cwd: p.cwd } : {}
8974
+ };
8975
+ }
8976
+ function readActivePause(sessionId, now = Date.now()) {
8977
+ if (!sessionId)
8978
+ return null;
8979
+ try {
8980
+ const path3 = pauseFilePath(sessionId);
8981
+ if (!existsSync13(path3))
8982
+ return null;
8983
+ const stored = parseStored(JSON.parse(readFileSync11(path3, "utf8")));
8984
+ if (!stored)
8985
+ return null;
8986
+ if (stored.expiresAt <= now)
8987
+ return null;
8988
+ const { schemaVersion: _ignored, ...pause } = stored;
8989
+ return pause;
8990
+ } catch {
8991
+ return null;
8992
+ }
8993
+ }
8994
+ function writePause(opts) {
8995
+ const now = opts.now ?? Date.now();
8996
+ const pause = {
8997
+ sessionId: opts.sessionId,
8998
+ pausedAt: now,
8999
+ expiresAt: now + opts.durationMs,
9000
+ setBy: opts.setBy ?? "cli",
9001
+ ...opts.cwd ? { cwd: opts.cwd } : {}
9002
+ };
9003
+ mkdirSync6(pauseStateDir(), { recursive: true, mode: 448 });
9004
+ writeJsonAtomically(pauseFilePath(opts.sessionId), { schemaVersion: SCHEMA_VERSION, ...pause });
9005
+ return pause;
9006
+ }
9007
+ function clearPause(sessionId) {
9008
+ const path3 = pauseFilePath(sessionId);
9009
+ if (!existsSync13(path3))
9010
+ return false;
9011
+ try {
9012
+ rmSync2(path3);
9013
+ return true;
9014
+ } catch {
9015
+ return false;
9016
+ }
9017
+ }
9018
+ function listActivePauses(now = Date.now()) {
9019
+ const dir = pauseStateDir();
9020
+ if (!existsSync13(dir))
9021
+ return [];
9022
+ const out = [];
9023
+ let names;
9024
+ try {
9025
+ names = readdirSync11(dir);
9026
+ } catch {
9027
+ return [];
9028
+ }
9029
+ for (const name of names) {
9030
+ if (!name.endsWith(".json"))
9031
+ continue;
9032
+ try {
9033
+ const stored = parseStored(JSON.parse(readFileSync11(join25(dir, name), "utf8")));
9034
+ if (!stored || stored.expiresAt <= now)
9035
+ continue;
9036
+ const { schemaVersion: _ignored, ...pause } = stored;
9037
+ out.push(pause);
9038
+ } catch {}
9039
+ }
9040
+ return out.sort((a, b) => b.pausedAt - a.pausedAt);
9041
+ }
9042
+ function resolveSessionForCwd(cwd, maxAgeMs = 12 * 3600000, now = Date.now()) {
9043
+ let best = null;
9044
+ let entries;
9045
+ try {
9046
+ entries = getAllHookActivityEntries();
9047
+ } catch {
9048
+ return null;
9049
+ }
9050
+ for (const entry of entries) {
9051
+ if (!entry.sessionId || entry.cwd !== cwd)
9052
+ continue;
9053
+ if (now - entry.timestamp > maxAgeMs)
9054
+ continue;
9055
+ if (!best || entry.timestamp > best.timestamp) {
9056
+ best = { sessionId: entry.sessionId, timestamp: entry.timestamp };
9057
+ }
9058
+ }
9059
+ return best?.sessionId ?? null;
9060
+ }
9061
+ var SCHEMA_VERSION = 1, PAUSE_DEFAULT_MS, PAUSE_CEILING_MS;
9062
+ var init_session_pause = __esm(() => {
9063
+ init_atomic_write();
9064
+ init_hook_activity_store();
9065
+ PAUSE_DEFAULT_MS = 30 * 60000;
9066
+ PAUSE_CEILING_MS = 8 * 60 * 60000;
9067
+ });
9068
+
8624
9069
  // src/hooks/handler.ts
8625
9070
  var exports_handler = {};
8626
9071
  __export(exports_handler, {
@@ -8661,6 +9106,24 @@ function canonicalizeEventType(raw, cli) {
8661
9106
  }
8662
9107
  return raw;
8663
9108
  }
9109
+ async function runObserved(hook, ctx, hookName, eventType, cli) {
9110
+ try {
9111
+ return await Promise.race([
9112
+ hook.fn(ctx),
9113
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 1e4))
9114
+ ]);
9115
+ } catch (err) {
9116
+ const msg = err instanceof Error ? err.message : String(err);
9117
+ trackHookEvent2(getInstanceId2(), "custom_hook_error", {
9118
+ hook_name: hookName,
9119
+ error_type: msg === "timeout" ? "timeout" : "exception",
9120
+ event_type: eventType,
9121
+ cli,
9122
+ is_observe_mode: true
9123
+ });
9124
+ return { decision: "allow" };
9125
+ }
9126
+ }
8664
9127
  async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts) {
8665
9128
  const startTime = performance.now();
8666
9129
  try {
@@ -8702,6 +9165,10 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
8702
9165
  let config;
8703
9166
  let customHooksList = [];
8704
9167
  let conventionHookNames = new Set;
9168
+ let activePause = null;
9169
+ const policyAttribution = new Map;
9170
+ let cloudGeneration;
9171
+ const observedResults = [];
8705
9172
  if (opts?.forceDecision) {
8706
9173
  config = { enabledPolicies: [] };
8707
9174
  clearPolicies();
@@ -8710,23 +9177,49 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
8710
9177
  } else {
8711
9178
  config = readMergedHooksConfig(session.cwd);
8712
9179
  clearPolicies();
8713
- registerBuiltinPolicies(config.enabledPolicies);
8714
- const loadResult = await loadAllCustomHooks(config.customPoliciesPaths ?? config.customPoliciesPath, {
9180
+ activePause = readActivePause(session.sessionId);
9181
+ registerBuiltinPolicies(activePause ? [] : config.enabledPolicies);
9182
+ const cloudManagedPolicies = readActiveCloudManagedPolicies();
9183
+ cloudGeneration = cloudManagedPolicies[0]?.generation;
9184
+ const configuredCustomPaths = config.customPoliciesPaths ?? config.customPoliciesPath;
9185
+ const allExplicitPaths = cloudManagedPolicies.length === 0 ? configuredCustomPaths : [
9186
+ ...typeof configuredCustomPaths === "string" ? [configuredCustomPaths] : configuredCustomPaths ?? [],
9187
+ ...cloudManagedPolicies.map((policy) => policy.path)
9188
+ ];
9189
+ const loadResult = await loadAllCustomHooks(allExplicitPaths, {
8715
9190
  sessionCwd: session.cwd,
8716
- customPoliciesEnabled: config.customPoliciesEnabled
9191
+ customPoliciesEnabled: config.customPoliciesEnabled,
9192
+ ...cloudManagedPolicies.length > 0 ? { cloudManagedPolicies } : {}
8717
9193
  });
8718
9194
  customHooksList = loadResult.hooks;
8719
9195
  const disabledCustomPolicies = new Set(config.disabledCustomPolicies ?? []);
8720
9196
  conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
8721
9197
  for (const hook of customHooksList) {
8722
- const policyId = hook.__policyId;
8723
- if (policyId && disabledCustomPolicies.has(policyId))
9198
+ const taggedHook = hook;
9199
+ const policyId = taggedHook.__policyId;
9200
+ const cloudManaged = taggedHook.__cloudManaged;
9201
+ if (!cloudManaged && policyId && disabledCustomPolicies.has(policyId))
9202
+ continue;
9203
+ if (!cloudManaged && activePause)
8724
9204
  continue;
8725
9205
  const hookName = hook.name;
8726
9206
  const conventionScope = hook.__conventionScope;
8727
9207
  const isConvention = !!conventionScope;
8728
- const prefix = isConvention ? `.failproofai-${conventionScope}` : "custom";
9208
+ const prefix = cloudManaged ? `cloud/${cloudManaged.id}@${cloudManaged.revision}` : isConvention ? `.failproofai-${conventionScope}` : "custom";
9209
+ const observeOnly = cloudManaged?.effect === "observe";
8729
9210
  const fn = async (ctx) => {
9211
+ if (observeOnly) {
9212
+ const shadow = await runObserved(hook, ctx, hookName, eventType, cli);
9213
+ if (shadow.decision !== "allow") {
9214
+ observedResults.push({
9215
+ policyId: cloudManaged.id,
9216
+ revision: cloudManaged.revision,
9217
+ decision: shadow.decision,
9218
+ reason: shadow.reason ?? null
9219
+ });
9220
+ }
9221
+ return { decision: "allow" };
9222
+ }
8730
9223
  try {
8731
9224
  const result2 = await Promise.race([
8732
9225
  hook.fn(ctx),
@@ -8748,7 +9241,12 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
8748
9241
  return { decision: "allow" };
8749
9242
  }
8750
9243
  };
8751
- registerPolicy(`${prefix}/${hookName}`, hook.description ?? "", fn, hook.match ?? {}, -1);
9244
+ const registeredName = `${prefix}/${hookName}`;
9245
+ policyAttribution.set(registeredName, {
9246
+ source: cloudManaged ? "cloud" : isConvention ? "convention" : "custom",
9247
+ ...cloudManaged ? { cloudPolicyId: cloudManaged.id, cloudRevision: cloudManaged.revision } : {}
9248
+ });
9249
+ registerPolicy(registeredName, hook.description ?? "", fn, hook.match ?? {}, -1);
8752
9250
  }
8753
9251
  if (customHooksList.length > 0) {
8754
9252
  trackHookEvent2(getInstanceId2(), "custom_hooks_loaded", {
@@ -8789,7 +9287,18 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
8789
9287
  transcriptPath: session.transcriptPath,
8790
9288
  cwd: session.cwd,
8791
9289
  permissionMode: session.permissionMode,
8792
- hookEventName: session.hookEventName
9290
+ hookEventName: session.hookEventName,
9291
+ ...result.policyName ? (() => {
9292
+ const attribution = policyAttribution.get(result.policyName);
9293
+ return {
9294
+ policySource: attribution?.source ?? "builtin",
9295
+ ...attribution?.cloudPolicyId ? { cloudPolicyId: attribution.cloudPolicyId } : {},
9296
+ ...attribution?.cloudRevision !== undefined ? { cloudRevision: attribution.cloudRevision } : {}
9297
+ };
9298
+ })() : {},
9299
+ ...cloudGeneration !== undefined ? { cloudGeneration } : {},
9300
+ ...observedResults.length > 0 ? { observed: observedResults } : {},
9301
+ ...activePause ? { pausedBy: activePause.setBy, pauseExpiresAt: activePause.expiresAt } : {}
8793
9302
  };
8794
9303
  try {
8795
9304
  persistHookActivity(activityEntry);
@@ -8869,22 +9378,24 @@ var init_handler = __esm(() => {
8869
9378
  init_resolve_transcript_path();
8870
9379
  init_telemetry_id2();
8871
9380
  init_hook_logger();
9381
+ init_cloud_managed_policies();
9382
+ init_session_pause();
8872
9383
  });
8873
9384
 
8874
9385
  // scripts/install-telemetry.mjs
8875
9386
  import { createHmac, randomUUID } from "node:crypto";
8876
9387
  import { execSync as execSync4 } from "node:child_process";
8877
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "node:fs";
8878
- import { homedir as homedir22, platform, arch, hostname, cpus } from "node:os";
8879
- import { join as join25 } from "node:path";
9388
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7 } from "node:fs";
9389
+ import { homedir as homedir24, platform, arch, hostname, cpus } from "node:os";
9390
+ import { join as join26 } from "node:path";
8880
9391
  function hashToId3(raw) {
8881
9392
  return createHmac("sha256", NAMESPACE3).update(raw).digest("hex");
8882
9393
  }
8883
9394
  function getInstanceId3() {
8884
- const idDir = join25(homedir22(), ".failproofai");
8885
- const idFile = join25(idDir, "instance-id");
9395
+ const idDir = join26(homedir24(), ".failproofai");
9396
+ const idFile = join26(idDir, "instance-id");
8886
9397
  try {
8887
- const existing = readFileSync10(idFile, "utf-8").trim();
9398
+ const existing = readFileSync12(idFile, "utf-8").trim();
8888
9399
  if (existing)
8889
9400
  return existing;
8890
9401
  } catch {}
@@ -8893,7 +9404,7 @@ function getInstanceId3() {
8893
9404
  if (p === "linux") {
8894
9405
  for (const machineIdPath of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
8895
9406
  try {
8896
- const id2 = readFileSync10(machineIdPath, "utf-8").trim();
9407
+ const id2 = readFileSync12(machineIdPath, "utf-8").trim();
8897
9408
  if (id2)
8898
9409
  return hashToId3(id2);
8899
9410
  } catch {}
@@ -8916,7 +9427,7 @@ function getInstanceId3() {
8916
9427
  try {
8917
9428
  const sysProps = [
8918
9429
  hostname(),
8919
- homedir22(),
9430
+ homedir24(),
8920
9431
  platform(),
8921
9432
  arch(),
8922
9433
  cpus()[0]?.model ?? ""
@@ -8925,8 +9436,8 @@ function getInstanceId3() {
8925
9436
  } catch {}
8926
9437
  const id = randomUUID();
8927
9438
  try {
8928
- mkdirSync5(idDir, { recursive: true });
8929
- writeFileSync4(idFile, id, "utf-8");
9439
+ mkdirSync7(idDir, { recursive: true });
9440
+ writeFileSync5(idFile, id, "utf-8");
8930
9441
  } catch {}
8931
9442
  return id;
8932
9443
  }
@@ -8962,15 +9473,15 @@ __export(exports_install_check, {
8962
9473
  compareSemver: () => compareSemver,
8963
9474
  checkHooks: () => checkHooks
8964
9475
  });
8965
- import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "node:fs";
8966
- import { resolve as resolve11 } from "node:path";
8967
- import { platform as platform2, arch as arch2, release, homedir as homedir23, hostname as hostname2 } from "node:os";
9476
+ import { existsSync as existsSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
9477
+ import { resolve as resolve13 } from "node:path";
9478
+ import { platform as platform2, arch as arch2, release, homedir as homedir25, hostname as hostname2 } from "node:os";
8968
9479
  import { createHmac as createHmac2 } from "node:crypto";
8969
9480
  function hashToId4(raw) {
8970
9481
  return createHmac2("sha256", NAMESPACE4).update(raw).digest("hex");
8971
9482
  }
8972
9483
  function lastVersionPath() {
8973
- return resolve11(homedir23(), ".failproofai", "last-version");
9484
+ return resolve13(homedir25(), ".failproofai", "last-version");
8974
9485
  }
8975
9486
  function compareSemver(a, b) {
8976
9487
  const parse = (v) => {
@@ -9017,13 +9528,13 @@ function compareSemver(a, b) {
9017
9528
  return 0;
9018
9529
  }
9019
9530
  function checkHooks() {
9020
- const hooksConfigPath = resolve11(homedir23(), ".failproofai", "policies-config.json");
9021
- if (!existsSync10(hooksConfigPath)) {
9531
+ const hooksConfigPath = resolve13(homedir25(), ".failproofai", "policies-config.json");
9532
+ if (!existsSync14(hooksConfigPath)) {
9022
9533
  return { configured: false, registered: false, policyCount: 0 };
9023
9534
  }
9024
9535
  let config;
9025
9536
  try {
9026
- config = JSON.parse(readFileSync11(hooksConfigPath, "utf8"));
9537
+ config = JSON.parse(readFileSync13(hooksConfigPath, "utf8"));
9027
9538
  } catch {
9028
9539
  return { configured: false, registered: false, policyCount: 0 };
9029
9540
  }
@@ -9031,12 +9542,12 @@ function checkHooks() {
9031
9542
  return { configured: false, registered: false, policyCount: 0 };
9032
9543
  }
9033
9544
  const policyCount = config.enabledPolicies.length;
9034
- const settingsPath = resolve11(homedir23(), ".claude", "settings.json");
9035
- if (!existsSync10(settingsPath))
9545
+ const settingsPath = resolve13(homedir25(), ".claude", "settings.json");
9546
+ if (!existsSync14(settingsPath))
9036
9547
  return { configured: true, registered: false, policyCount };
9037
9548
  let settings;
9038
9549
  try {
9039
- settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
9550
+ settings = JSON.parse(readFileSync13(settingsPath, "utf8"));
9040
9551
  } catch {
9041
9552
  return { configured: true, registered: false, policyCount };
9042
9553
  }
@@ -9060,8 +9571,8 @@ async function maybeReportInstall(version3) {
9060
9571
  const versionFile = lastVersionPath();
9061
9572
  let previousVersion = null;
9062
9573
  try {
9063
- if (existsSync10(versionFile)) {
9064
- previousVersion = readFileSync11(versionFile, "utf8").trim() || null;
9574
+ if (existsSync14(versionFile)) {
9575
+ previousVersion = readFileSync13(versionFile, "utf8").trim() || null;
9065
9576
  }
9066
9577
  } catch {}
9067
9578
  if (previousVersion === version3)
@@ -9096,8 +9607,8 @@ async function maybeReportInstall(version3) {
9096
9607
  enabled_policy_count: hooks.policyCount
9097
9608
  }, { version: version3, timeoutMs: REPORT_TIMEOUT_MS }));
9098
9609
  try {
9099
- mkdirSync6(resolve11(homedir23(), ".failproofai"), { recursive: true });
9100
- writeFileSync5(versionFile, version3, "utf8");
9610
+ mkdirSync8(resolve13(homedir25(), ".failproofai"), { recursive: true });
9611
+ writeFileSync6(versionFile, version3, "utf8");
9101
9612
  } catch {}
9102
9613
  await Promise.all(events.map((p) => p.catch(() => {})));
9103
9614
  } catch {}
@@ -16099,31 +16610,31 @@ var init_dist = __esm(() => {
16099
16610
 
16100
16611
  // src/hooks/integrations.ts
16101
16612
  import { execSync as execSync5 } from "node:child_process";
16102
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync7, unlinkSync as unlinkSync2 } from "node:fs";
16103
- import { resolve as resolve12, dirname as dirname5 } from "node:path";
16613
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync7, existsSync as existsSync15, mkdirSync as mkdirSync9, unlinkSync as unlinkSync2 } from "node:fs";
16614
+ import { resolve as resolve14, dirname as dirname6 } from "node:path";
16104
16615
  import { fileURLToPath } from "node:url";
16105
- import { homedir as homedir24 } from "node:os";
16616
+ import { homedir as homedir26 } from "node:os";
16106
16617
  function readJsonFile(path3) {
16107
- if (!existsSync11(path3))
16618
+ if (!existsSync15(path3))
16108
16619
  return {};
16109
- const raw = readFileSync12(path3, "utf8");
16620
+ const raw = readFileSync14(path3, "utf8");
16110
16621
  return JSON.parse(raw);
16111
16622
  }
16112
16623
  function writeJsonFile(path3, data) {
16113
- mkdirSync7(dirname5(path3), { recursive: true });
16114
- writeFileSync6(path3, JSON.stringify(data, null, 2) + `
16624
+ mkdirSync9(dirname6(path3), { recursive: true });
16625
+ writeFileSync7(path3, JSON.stringify(data, null, 2) + `
16115
16626
  `, "utf8");
16116
16627
  }
16117
16628
  function readYamlDoc(path3) {
16118
16629
  try {
16119
- return existsSync11(path3) ? $parseDocument(readFileSync12(path3, "utf8")) : $parseDocument("");
16630
+ return existsSync15(path3) ? $parseDocument(readFileSync14(path3, "utf8")) : $parseDocument("");
16120
16631
  } catch {
16121
16632
  return $parseDocument("");
16122
16633
  }
16123
16634
  }
16124
16635
  function writeYamlDoc(path3, doc) {
16125
- mkdirSync7(dirname5(path3), { recursive: true });
16126
- writeFileSync6(path3, doc.toString(), "utf8");
16636
+ mkdirSync9(dirname6(path3), { recursive: true });
16637
+ writeFileSync7(path3, doc.toString(), "utf8");
16127
16638
  }
16128
16639
  function isMarkedHook(hook) {
16129
16640
  if (!hook || typeof hook !== "object")
@@ -16166,7 +16677,7 @@ function isMarkedCopilotHook(hook) {
16166
16677
  return false;
16167
16678
  }
16168
16679
  function opencodePluginFilePath(settingsPath) {
16169
- return resolve12(dirname5(settingsPath), "plugins", "failproofai.mjs");
16680
+ return resolve14(dirname6(settingsPath), "plugins", "failproofai.mjs");
16170
16681
  }
16171
16682
  function buildOpenCodePluginShim(binaryPath, scope) {
16172
16683
  const useNpx = scope === "project";
@@ -16381,8 +16892,8 @@ export default async function failproofaiPlugin({ client, directory }) {
16381
16892
  function getPiExtensionPath() {
16382
16893
  const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
16383
16894
  if (fromEnv)
16384
- return resolve12(fromEnv, "pi-extension");
16385
- return resolve12(fileURLToPath(import.meta.url), "..", "..", "..", "pi-extension");
16895
+ return resolve14(fromEnv, "pi-extension");
16896
+ return resolve14(fileURLToPath(import.meta.url), "..", "..", "..", "pi-extension");
16386
16897
  }
16387
16898
  function isFailproofaiPiEntry(source) {
16388
16899
  if (typeof source !== "string")
@@ -16393,8 +16904,8 @@ function isFailproofaiPiEntry(source) {
16393
16904
  }
16394
16905
  function makePiProjectRelativeEntry(extPath) {
16395
16906
  const cwd = process.cwd();
16396
- const cwdResolved = resolve12(cwd);
16397
- const extResolved = resolve12(extPath);
16907
+ const cwdResolved = resolve14(cwd);
16908
+ const extResolved = resolve14(extPath);
16398
16909
  if (extResolved.startsWith(cwdResolved + "/") || extResolved === cwdResolved) {
16399
16910
  const fromSettingsDir = "../" + extResolved.slice(cwdResolved.length + 1);
16400
16911
  return fromSettingsDir;
@@ -16402,7 +16913,7 @@ function makePiProjectRelativeEntry(extPath) {
16402
16913
  return extResolved;
16403
16914
  }
16404
16915
  function hermesConfigHasHooks(settingsPath) {
16405
- if (!existsSync11(settingsPath))
16916
+ if (!existsSync15(settingsPath))
16406
16917
  return false;
16407
16918
  try {
16408
16919
  const doc = readYamlDoc(settingsPath);
@@ -16418,13 +16929,13 @@ function hermesConfigHasHooks(settingsPath) {
16418
16929
  return false;
16419
16930
  }
16420
16931
  function unhookedHermesProfiles() {
16421
- return listHermesProfiles().filter((p) => existsSync11(p.home)).filter((p) => !hermesConfigHasHooks(resolve12(p.home, "config.yaml"))).map((p) => p.name);
16932
+ return listHermesProfiles().filter((p) => existsSync15(p.home)).filter((p) => !hermesConfigHasHooks(resolve14(p.home, "config.yaml"))).map((p) => p.name);
16422
16933
  }
16423
16934
  function getOpenClawPluginPath() {
16424
16935
  const fromEnv = process.env.FAILPROOFAI_PACKAGE_ROOT;
16425
16936
  if (fromEnv)
16426
- return resolve12(fromEnv, "openclaw-plugin");
16427
- return resolve12(fileURLToPath(import.meta.url), "..", "..", "..", "openclaw-plugin");
16937
+ return resolve14(fromEnv, "openclaw-plugin");
16938
+ return resolve14(fileURLToPath(import.meta.url), "..", "..", "..", "openclaw-plugin");
16428
16939
  }
16429
16940
  function isFailproofaiOpenClawPath(p) {
16430
16941
  if (typeof p !== "string")
@@ -16461,14 +16972,14 @@ var init_integrations = __esm(() => {
16461
16972
  scopes: HOOK_SCOPES,
16462
16973
  eventTypes: CLAUDE_INSTALL_EVENT_TYPES,
16463
16974
  getSettingsPath(scope, cwd) {
16464
- const base = cwd ? resolve12(cwd) : process.cwd();
16975
+ const base = cwd ? resolve14(cwd) : process.cwd();
16465
16976
  switch (scope) {
16466
16977
  case "user":
16467
- return resolve12(homedir24(), ".claude", "settings.json");
16978
+ return resolve14(homedir26(), ".claude", "settings.json");
16468
16979
  case "project":
16469
- return resolve12(base, ".claude", "settings.json");
16980
+ return resolve14(base, ".claude", "settings.json");
16470
16981
  case "local":
16471
- return resolve12(base, ".claude", "settings.local.json");
16982
+ return resolve14(base, ".claude", "settings.local.json");
16472
16983
  }
16473
16984
  },
16474
16985
  readSettings(settingsPath) {
@@ -16564,7 +17075,7 @@ var init_integrations = __esm(() => {
16564
17075
  },
16565
17076
  hooksInstalledInSettings(scope, cwd) {
16566
17077
  const settingsPath = this.getSettingsPath(scope, cwd);
16567
- if (!existsSync11(settingsPath))
17078
+ if (!existsSync15(settingsPath))
16568
17079
  return false;
16569
17080
  try {
16570
17081
  const settings = this.readSettings(settingsPath);
@@ -16593,14 +17104,14 @@ var init_integrations = __esm(() => {
16593
17104
  scopes: CODEX_HOOK_SCOPES,
16594
17105
  eventTypes: CODEX_HOOK_EVENT_TYPES,
16595
17106
  getSettingsPath(scope, cwd) {
16596
- const base = cwd ? resolve12(cwd) : process.cwd();
17107
+ const base = cwd ? resolve14(cwd) : process.cwd();
16597
17108
  switch (scope) {
16598
17109
  case "user":
16599
- return resolve12(homedir24(), ".codex", "hooks.json");
17110
+ return resolve14(homedir26(), ".codex", "hooks.json");
16600
17111
  case "project":
16601
- return resolve12(base, ".codex", "hooks.json");
17112
+ return resolve14(base, ".codex", "hooks.json");
16602
17113
  case "local":
16603
- return resolve12(base, ".codex", "hooks.json");
17114
+ return resolve14(base, ".codex", "hooks.json");
16604
17115
  }
16605
17116
  },
16606
17117
  readSettings(settingsPath) {
@@ -16680,7 +17191,7 @@ var init_integrations = __esm(() => {
16680
17191
  },
16681
17192
  hooksInstalledInSettings(scope, cwd) {
16682
17193
  const settingsPath = this.getSettingsPath(scope, cwd);
16683
- if (!existsSync11(settingsPath))
17194
+ if (!existsSync15(settingsPath))
16684
17195
  return false;
16685
17196
  try {
16686
17197
  const settings = this.readSettings(settingsPath);
@@ -16709,14 +17220,14 @@ var init_integrations = __esm(() => {
16709
17220
  scopes: COPILOT_HOOK_SCOPES,
16710
17221
  eventTypes: COPILOT_HOOK_EVENT_TYPES,
16711
17222
  getSettingsPath(scope, cwd) {
16712
- const base = cwd ? resolve12(cwd) : process.cwd();
17223
+ const base = cwd ? resolve14(cwd) : process.cwd();
16713
17224
  switch (scope) {
16714
17225
  case "user":
16715
- return resolve12(homedir24(), ".copilot", "hooks", "failproofai.json");
17226
+ return resolve14(homedir26(), ".copilot", "hooks", "failproofai.json");
16716
17227
  case "project":
16717
- return resolve12(base, ".github", "hooks", "failproofai.json");
17228
+ return resolve14(base, ".github", "hooks", "failproofai.json");
16718
17229
  case "local":
16719
- return resolve12(base, ".github", "hooks", "failproofai.json");
17230
+ return resolve14(base, ".github", "hooks", "failproofai.json");
16720
17231
  }
16721
17232
  },
16722
17233
  readSettings(settingsPath) {
@@ -16794,7 +17305,7 @@ var init_integrations = __esm(() => {
16794
17305
  },
16795
17306
  hooksInstalledInSettings(scope, cwd) {
16796
17307
  const settingsPath = this.getSettingsPath(scope, cwd);
16797
- if (!existsSync11(settingsPath))
17308
+ if (!existsSync15(settingsPath))
16798
17309
  return false;
16799
17310
  try {
16800
17311
  const settings = this.readSettings(settingsPath);
@@ -16823,14 +17334,14 @@ var init_integrations = __esm(() => {
16823
17334
  scopes: CURSOR_HOOK_SCOPES,
16824
17335
  eventTypes: CURSOR_HOOK_EVENT_TYPES,
16825
17336
  getSettingsPath(scope, cwd) {
16826
- const base = cwd ? resolve12(cwd) : process.cwd();
17337
+ const base = cwd ? resolve14(cwd) : process.cwd();
16827
17338
  switch (scope) {
16828
17339
  case "user":
16829
- return resolve12(homedir24(), ".cursor", "hooks.json");
17340
+ return resolve14(homedir26(), ".cursor", "hooks.json");
16830
17341
  case "project":
16831
- return resolve12(base, ".cursor", "hooks.json");
17342
+ return resolve14(base, ".cursor", "hooks.json");
16832
17343
  case "local":
16833
- return resolve12(base, ".cursor", "hooks.json");
17344
+ return resolve14(base, ".cursor", "hooks.json");
16834
17345
  }
16835
17346
  },
16836
17347
  readSettings(settingsPath) {
@@ -16897,7 +17408,7 @@ var init_integrations = __esm(() => {
16897
17408
  },
16898
17409
  hooksInstalledInSettings(scope, cwd) {
16899
17410
  const settingsPath = this.getSettingsPath(scope, cwd);
16900
- if (!existsSync11(settingsPath))
17411
+ if (!existsSync15(settingsPath))
16901
17412
  return false;
16902
17413
  try {
16903
17414
  const settings = this.readSettings(settingsPath);
@@ -16922,14 +17433,14 @@ var init_integrations = __esm(() => {
16922
17433
  scopes: OPENCODE_HOOK_SCOPES,
16923
17434
  eventTypes: OPENCODE_HOOK_EVENT_TYPES,
16924
17435
  getSettingsPath(scope, cwd) {
16925
- const base = cwd ? resolve12(cwd) : process.cwd();
17436
+ const base = cwd ? resolve14(cwd) : process.cwd();
16926
17437
  switch (scope) {
16927
17438
  case "user":
16928
- return resolve12(homedir24(), ".config", "opencode", "opencode.json");
17439
+ return resolve14(homedir26(), ".config", "opencode", "opencode.json");
16929
17440
  case "project":
16930
- return resolve12(base, ".opencode", "opencode.json");
17441
+ return resolve14(base, ".opencode", "opencode.json");
16931
17442
  case "local":
16932
- return resolve12(base, ".opencode", "opencode.json");
17443
+ return resolve14(base, ".opencode", "opencode.json");
16933
17444
  }
16934
17445
  },
16935
17446
  readSettings(settingsPath) {
@@ -16940,7 +17451,7 @@ var init_integrations = __esm(() => {
16940
17451
  },
16941
17452
  buildHookEntry(_binaryPath, _eventType, scope) {
16942
17453
  if (scope === "user") {
16943
- const abs = resolve12(homedir24(), ".config", "opencode", "plugins", "failproofai.mjs");
17454
+ const abs = resolve14(homedir26(), ".config", "opencode", "plugins", "failproofai.mjs");
16944
17455
  return { spec: `file://${abs}`, [FAILPROOFAI_HOOK_MARKER]: true };
16945
17456
  }
16946
17457
  return { spec: OPENCODE_PLUGIN_REL_PATH, [FAILPROOFAI_HOOK_MARKER]: true };
@@ -16955,10 +17466,10 @@ var init_integrations = __esm(() => {
16955
17466
  writeHookEntries(settings, binaryPath, scope) {
16956
17467
  const s = settings;
16957
17468
  const effectiveScope = scope ?? "project";
16958
- const settingsPath = effectiveScope === "user" ? resolve12(homedir24(), ".config", "opencode", "opencode.json") : resolve12(process.cwd(), ".opencode", "opencode.json");
17469
+ const settingsPath = effectiveScope === "user" ? resolve14(homedir26(), ".config", "opencode", "opencode.json") : resolve14(process.cwd(), ".opencode", "opencode.json");
16959
17470
  const pluginPath = opencodePluginFilePath(settingsPath);
16960
- mkdirSync7(dirname5(pluginPath), { recursive: true });
16961
- writeFileSync6(pluginPath, buildOpenCodePluginShim(binaryPath, effectiveScope), "utf8");
17471
+ mkdirSync9(dirname6(pluginPath), { recursive: true });
17472
+ writeFileSync7(pluginPath, buildOpenCodePluginShim(binaryPath, effectiveScope), "utf8");
16962
17473
  if (!Array.isArray(s.plugin))
16963
17474
  s.plugin = [];
16964
17475
  const desired = effectiveScope === "user" ? `file://${pluginPath}` : OPENCODE_PLUGIN_REL_PATH;
@@ -16981,9 +17492,9 @@ var init_integrations = __esm(() => {
16981
17492
  }
16982
17493
  this.writeSettings(settingsPath, settings);
16983
17494
  const pluginPath = opencodePluginFilePath(settingsPath);
16984
- if (existsSync11(pluginPath)) {
17495
+ if (existsSync15(pluginPath)) {
16985
17496
  try {
16986
- const content = readFileSync12(pluginPath, "utf8");
17497
+ const content = readFileSync14(pluginPath, "utf8");
16987
17498
  if (content.includes(FAILPROOFAI_HOOK_MARKER)) {
16988
17499
  unlinkSync2(pluginPath);
16989
17500
  if (removed === 0)
@@ -16995,7 +17506,7 @@ var init_integrations = __esm(() => {
16995
17506
  },
16996
17507
  hooksInstalledInSettings(scope, cwd) {
16997
17508
  const settingsPath = this.getSettingsPath(scope, cwd);
16998
- if (!existsSync11(settingsPath))
17509
+ if (!existsSync15(settingsPath))
16999
17510
  return false;
17000
17511
  try {
17001
17512
  const settings = this.readSettings(settingsPath);
@@ -17005,9 +17516,9 @@ var init_integrations = __esm(() => {
17005
17516
  if (!hasEntry)
17006
17517
  return false;
17007
17518
  const pluginPath = opencodePluginFilePath(settingsPath);
17008
- if (!existsSync11(pluginPath))
17519
+ if (!existsSync15(pluginPath))
17009
17520
  return false;
17010
- const content = readFileSync12(pluginPath, "utf8");
17521
+ const content = readFileSync14(pluginPath, "utf8");
17011
17522
  return content.includes(FAILPROOFAI_HOOK_MARKER);
17012
17523
  } catch {
17013
17524
  return false;
@@ -17023,14 +17534,14 @@ var init_integrations = __esm(() => {
17023
17534
  scopes: PI_HOOK_SCOPES,
17024
17535
  eventTypes: PI_HOOK_EVENT_TYPES,
17025
17536
  getSettingsPath(scope, cwd) {
17026
- const base = cwd ? resolve12(cwd) : process.cwd();
17537
+ const base = cwd ? resolve14(cwd) : process.cwd();
17027
17538
  switch (scope) {
17028
17539
  case "user":
17029
- return resolve12(homedir24(), ".pi", "agent", "settings.json");
17540
+ return resolve14(homedir26(), ".pi", "agent", "settings.json");
17030
17541
  case "project":
17031
- return resolve12(base, ".pi", "settings.json");
17542
+ return resolve14(base, ".pi", "settings.json");
17032
17543
  case "local":
17033
- return resolve12(base, ".pi", "settings.json");
17544
+ return resolve14(base, ".pi", "settings.json");
17034
17545
  }
17035
17546
  },
17036
17547
  readSettings(settingsPath) {
@@ -17072,7 +17583,7 @@ var init_integrations = __esm(() => {
17072
17583
  }
17073
17584
  },
17074
17585
  removeHooksFromFile(settingsPath) {
17075
- if (!existsSync11(settingsPath))
17586
+ if (!existsSync15(settingsPath))
17076
17587
  return 0;
17077
17588
  const settings = this.readSettings(settingsPath);
17078
17589
  if (!Array.isArray(settings.packages))
@@ -17087,7 +17598,7 @@ var init_integrations = __esm(() => {
17087
17598
  },
17088
17599
  hooksInstalledInSettings(scope, cwd) {
17089
17600
  const settingsPath = this.getSettingsPath(scope, cwd);
17090
- if (!existsSync11(settingsPath))
17601
+ if (!existsSync15(settingsPath))
17091
17602
  return false;
17092
17603
  try {
17093
17604
  const settings = this.readSettings(settingsPath);
@@ -17108,10 +17619,10 @@ var init_integrations = __esm(() => {
17108
17619
  scopes: HERMES_HOOK_SCOPES,
17109
17620
  eventTypes: HERMES_HOOK_EVENT_TYPES,
17110
17621
  getSettingsPath() {
17111
- return resolve12(hermesRoot(), "config.yaml");
17622
+ return resolve14(hermesRoot(), "config.yaml");
17112
17623
  },
17113
17624
  getSettingsPaths() {
17114
- return listHermesProfiles().map((p) => resolve12(p.home, "config.yaml"));
17625
+ return listHermesProfiles().map((p) => resolve14(p.home, "config.yaml"));
17115
17626
  },
17116
17627
  readSettings(settingsPath) {
17117
17628
  return readYamlDoc(settingsPath);
@@ -17146,7 +17657,7 @@ var init_integrations = __esm(() => {
17146
17657
  doc.set("hooks_auto_accept", true);
17147
17658
  },
17148
17659
  removeHooksFromFile(settingsPath) {
17149
- if (!existsSync11(settingsPath))
17660
+ if (!existsSync15(settingsPath))
17150
17661
  return 0;
17151
17662
  const doc = readYamlDoc(settingsPath);
17152
17663
  const js = doc.toJS() ?? {};
@@ -17191,7 +17702,7 @@ var init_integrations = __esm(() => {
17191
17702
  scopes: OPENCLAW_HOOK_SCOPES,
17192
17703
  eventTypes: OPENCLAW_HOOK_EVENT_TYPES,
17193
17704
  getSettingsPath() {
17194
- return resolve12(homedir24(), ".openclaw", "openclaw.json");
17705
+ return resolve14(homedir26(), ".openclaw", "openclaw.json");
17195
17706
  },
17196
17707
  readSettings(settingsPath) {
17197
17708
  return readJsonFile(settingsPath);
@@ -17236,7 +17747,7 @@ var init_integrations = __esm(() => {
17236
17747
  hooks.allowConversationAccess = true;
17237
17748
  },
17238
17749
  removeHooksFromFile(settingsPath) {
17239
- if (!existsSync11(settingsPath))
17750
+ if (!existsSync15(settingsPath))
17240
17751
  return 0;
17241
17752
  const settings = this.readSettings(settingsPath);
17242
17753
  const plugins = settings.plugins;
@@ -17265,7 +17776,7 @@ var init_integrations = __esm(() => {
17265
17776
  },
17266
17777
  hooksInstalledInSettings(scope, cwd) {
17267
17778
  const settingsPath = this.getSettingsPath(scope, cwd);
17268
- if (!existsSync11(settingsPath))
17779
+ if (!existsSync15(settingsPath))
17269
17780
  return false;
17270
17781
  try {
17271
17782
  const settings = this.readSettings(settingsPath);
@@ -17287,14 +17798,14 @@ var init_integrations = __esm(() => {
17287
17798
  scopes: FACTORY_HOOK_SCOPES,
17288
17799
  eventTypes: FACTORY_HOOK_EVENT_TYPES,
17289
17800
  getSettingsPath(scope, cwd) {
17290
- const base = cwd ? resolve12(cwd) : process.cwd();
17801
+ const base = cwd ? resolve14(cwd) : process.cwd();
17291
17802
  switch (scope) {
17292
17803
  case "user":
17293
- return resolve12(homedir24(), ".factory", "hooks.json");
17804
+ return resolve14(homedir26(), ".factory", "hooks.json");
17294
17805
  case "project":
17295
- return resolve12(base, ".factory", "hooks.json");
17806
+ return resolve14(base, ".factory", "hooks.json");
17296
17807
  case "local":
17297
- return resolve12(base, ".factory", "hooks.json");
17808
+ return resolve14(base, ".factory", "hooks.json");
17298
17809
  }
17299
17810
  },
17300
17811
  readSettings(settingsPath) {
@@ -17362,7 +17873,7 @@ var init_integrations = __esm(() => {
17362
17873
  },
17363
17874
  hooksInstalledInSettings(scope, cwd) {
17364
17875
  const settingsPath = this.getSettingsPath(scope, cwd);
17365
- if (!existsSync11(settingsPath))
17876
+ if (!existsSync15(settingsPath))
17366
17877
  return false;
17367
17878
  try {
17368
17879
  const settings = this.readSettings(settingsPath);
@@ -17389,14 +17900,14 @@ var init_integrations = __esm(() => {
17389
17900
  scopes: DEVIN_HOOK_SCOPES,
17390
17901
  eventTypes: DEVIN_HOOK_EVENT_TYPES,
17391
17902
  getSettingsPath(scope, cwd) {
17392
- const base = cwd ? resolve12(cwd) : process.cwd();
17903
+ const base = cwd ? resolve14(cwd) : process.cwd();
17393
17904
  switch (scope) {
17394
17905
  case "user":
17395
- return resolve12(homedir24(), ".config", "devin", "config.json");
17906
+ return resolve14(homedir26(), ".config", "devin", "config.json");
17396
17907
  case "project":
17397
- return resolve12(base, ".devin", "config.json");
17908
+ return resolve14(base, ".devin", "config.json");
17398
17909
  case "local":
17399
- return resolve12(base, ".devin", "config.json");
17910
+ return resolve14(base, ".devin", "config.json");
17400
17911
  }
17401
17912
  },
17402
17913
  readSettings(settingsPath) {
@@ -17468,7 +17979,7 @@ var init_integrations = __esm(() => {
17468
17979
  },
17469
17980
  hooksInstalledInSettings(scope, cwd) {
17470
17981
  const settingsPath = this.getSettingsPath(scope, cwd);
17471
- if (!existsSync11(settingsPath))
17982
+ if (!existsSync15(settingsPath))
17472
17983
  return false;
17473
17984
  try {
17474
17985
  const settings = this.readSettings(settingsPath);
@@ -17498,14 +18009,14 @@ var init_integrations = __esm(() => {
17498
18009
  scopes: ANTIGRAVITY_HOOK_SCOPES,
17499
18010
  eventTypes: ANTIGRAVITY_HOOK_EVENT_TYPES,
17500
18011
  getSettingsPath(scope, cwd) {
17501
- const base = cwd ? resolve12(cwd) : process.cwd();
18012
+ const base = cwd ? resolve14(cwd) : process.cwd();
17502
18013
  switch (scope) {
17503
18014
  case "user":
17504
- return resolve12(homedir24(), ".gemini", "config", "hooks.json");
18015
+ return resolve14(homedir26(), ".gemini", "config", "hooks.json");
17505
18016
  case "project":
17506
- return resolve12(base, ".agents", "hooks.json");
18017
+ return resolve14(base, ".agents", "hooks.json");
17507
18018
  case "local":
17508
- return resolve12(base, ".agents", "hooks.json");
18019
+ return resolve14(base, ".agents", "hooks.json");
17509
18020
  }
17510
18021
  },
17511
18022
  readSettings(settingsPath) {
@@ -17607,7 +18118,7 @@ var init_integrations = __esm(() => {
17607
18118
  },
17608
18119
  hooksInstalledInSettings(scope, cwd) {
17609
18120
  const settingsPath = this.getSettingsPath(scope, cwd);
17610
- if (!existsSync11(settingsPath))
18121
+ if (!existsSync15(settingsPath))
17611
18122
  return false;
17612
18123
  try {
17613
18124
  const settings = this.readSettings(settingsPath);
@@ -17639,14 +18150,14 @@ var init_integrations = __esm(() => {
17639
18150
  scopes: GOOSE_HOOK_SCOPES,
17640
18151
  eventTypes: GOOSE_HOOK_EVENT_TYPES,
17641
18152
  getSettingsPath(scope, cwd) {
17642
- const base = cwd ? resolve12(cwd) : process.cwd();
18153
+ const base = cwd ? resolve14(cwd) : process.cwd();
17643
18154
  switch (scope) {
17644
18155
  case "user":
17645
- return resolve12(homedir24(), ".agents", "plugins", "failproofai", "hooks", "hooks.json");
18156
+ return resolve14(homedir26(), ".agents", "plugins", "failproofai", "hooks", "hooks.json");
17646
18157
  case "project":
17647
- return resolve12(base, ".agents", "plugins", "failproofai", "hooks", "hooks.json");
18158
+ return resolve14(base, ".agents", "plugins", "failproofai", "hooks", "hooks.json");
17648
18159
  case "local":
17649
- return resolve12(base, ".agents", "plugins", "failproofai", "hooks", "hooks.json");
18160
+ return resolve14(base, ".agents", "plugins", "failproofai", "hooks", "hooks.json");
17650
18161
  }
17651
18162
  },
17652
18163
  readSettings(settingsPath) {
@@ -17713,7 +18224,7 @@ var init_integrations = __esm(() => {
17713
18224
  },
17714
18225
  hooksInstalledInSettings(scope, cwd) {
17715
18226
  const settingsPath = this.getSettingsPath(scope, cwd);
17716
- if (!existsSync11(settingsPath))
18227
+ if (!existsSync15(settingsPath))
17717
18228
  return false;
17718
18229
  try {
17719
18230
  const settings = this.readSettings(settingsPath);
@@ -17981,7 +18492,7 @@ function runPrompt(p) {
17981
18492
  `${c.dim(BAR)} ${c.dim(p.summaryFor(result))}`
17982
18493
  ]);
17983
18494
  };
17984
- return new Promise((resolve13) => {
18495
+ return new Promise((resolve15) => {
17985
18496
  hideCursor(stdout);
17986
18497
  repaint(stdout, region, build());
17987
18498
  readline.emitKeypressEvents(stdin);
@@ -17997,7 +18508,7 @@ function runPrompt(p) {
17997
18508
  const finish = (result) => {
17998
18509
  cleanup();
17999
18510
  collapse(result);
18000
- resolve13(result);
18511
+ resolve15(result);
18001
18512
  };
18002
18513
  function onKey(_s, key) {
18003
18514
  if (!key)
@@ -18111,6 +18622,89 @@ function multiSelect(opts) {
18111
18622
  summaryFor: (values) => values === null ? "cancelled" : summarize(choices.filter((ch) => values.includes(ch.value) && !ch.summaryExclude).map((ch) => ch.label), noun)
18112
18623
  });
18113
18624
  }
18625
+ function promptText(opts) {
18626
+ const stdin = opts.stdin ?? process.stdin;
18627
+ const stdout = opts.stdout ?? process.stdout;
18628
+ const c = paint(colorsEnabled(stdout));
18629
+ const accept = (raw) => {
18630
+ const value = raw.trim() || opts.defaultValue || "";
18631
+ const error = opts.validate?.(value) ?? null;
18632
+ return error ? { ok: false, error } : { ok: true, value };
18633
+ };
18634
+ if (!stdin.isTTY) {
18635
+ return new Promise((resolve15) => {
18636
+ let buf = "";
18637
+ const onData = (chunk) => {
18638
+ buf += String(chunk);
18639
+ if (buf.includes(`
18640
+ `))
18641
+ done();
18642
+ };
18643
+ const done = () => {
18644
+ stdin.removeListener("data", onData);
18645
+ stdin.removeListener("end", done);
18646
+ const r = accept(buf.split(`
18647
+ `)[0] ?? "");
18648
+ resolve15(r.ok ? r.value : null);
18649
+ };
18650
+ stdin.on("data", onData);
18651
+ stdin.on("end", done);
18652
+ });
18653
+ }
18654
+ return new Promise((resolve15) => {
18655
+ let value = "";
18656
+ const draw = (error) => {
18657
+ const shown = opts.mask ? "•".repeat(value.length) : value;
18658
+ const hint = opts.hint ? ` ${c.dim(opts.hint)}` : "";
18659
+ const err = error ? `
18660
+ ${c.warn(error)}` : "";
18661
+ stdout.write(`\r\x1B[2K${c.bold(opts.message)} ${shown}${hint}${err}`);
18662
+ if (err)
18663
+ stdout.write("\x1B[1A");
18664
+ };
18665
+ draw();
18666
+ readline.emitKeypressEvents(stdin);
18667
+ const wasRaw = stdin.isRaw;
18668
+ stdin.setRawMode?.(true);
18669
+ stdin.resume();
18670
+ const cleanup = () => {
18671
+ stdin.removeListener("keypress", onKey);
18672
+ stdin.setRawMode?.(wasRaw ?? false);
18673
+ stdin.pause();
18674
+ stdout.write(`
18675
+ `);
18676
+ };
18677
+ function onKey(str2, key) {
18678
+ if (!key)
18679
+ return;
18680
+ if (key.ctrl && (key.name === "c" || key.name === "d") || key.name === "escape") {
18681
+ cleanup();
18682
+ resolve15(null);
18683
+ return;
18684
+ }
18685
+ if (key.name === "return" || key.name === "enter") {
18686
+ const r = accept(value);
18687
+ if (r.ok) {
18688
+ cleanup();
18689
+ resolve15(r.value);
18690
+ } else {
18691
+ draw(r.error);
18692
+ }
18693
+ return;
18694
+ }
18695
+ if (key.name === "backspace") {
18696
+ value = value.slice(0, -1);
18697
+ draw();
18698
+ return;
18699
+ }
18700
+ if (str2 && !key.ctrl && !key.meta) {
18701
+ value += str2;
18702
+ draw();
18703
+ }
18704
+ }
18705
+ stdin.on("keypress", onKey);
18706
+ });
18707
+ }
18114
18708
  var ESC = "\x1B", BAR = "│", BAR_END = "└", STEP_ACTIVE = "◆", STEP_DONE = "◇", RADIO_ON = "●", RADIO_OFF = "○", CHECK_ON = "◼", CHECK_OFF = "◻", CARET = "❯", FLOWER = "❋", HUES, ANSI_RESET, ANSI_BOLD, ANSI_DIM, LOGO_MIN_COLS = 22, TAGLINE = "end-to-end failure layer for ai agents", LOGO_GRID, LOGO_TEAL, LOGO_PINK, WINDOW = 8;
18115
18709
  var init_tui = __esm(() => {
18116
18710
  HUES = {
@@ -18298,7 +18892,7 @@ async function promptPolicySelection(preSelected, options = {}) {
18298
18892
  `);
18299
18893
  lastLineCount = lines.length;
18300
18894
  }
18301
- return new Promise((resolve13) => {
18895
+ return new Promise((resolve15) => {
18302
18896
  render();
18303
18897
  process.stdin.setRawMode(true);
18304
18898
  process.stdin.resume();
@@ -18335,7 +18929,7 @@ async function promptPolicySelection(preSelected, options = {}) {
18335
18929
  ${dim(STEP_DONE)} Policies
18336
18930
  ${dim(BAR)} ${dim(`${selected.length} selected`)}
18337
18931
  `);
18338
- resolve13(selected);
18932
+ resolve15(selected);
18339
18933
  } else if (key.name === "escape") {
18340
18934
  search = "";
18341
18935
  cursor2 = 0;
@@ -18397,9 +18991,9 @@ __export(exports_manager, {
18397
18991
  getSettingsPath: () => getSettingsPath
18398
18992
  });
18399
18993
  import { execSync as execSync6 } from "node:child_process";
18400
- import { existsSync as existsSync12 } from "node:fs";
18401
- import { resolve as resolve13, basename as basename4 } from "node:path";
18402
- import { homedir as homedir25, platform as platform3, arch as arch3, release as release2, hostname as hostname3 } from "node:os";
18994
+ import { existsSync as existsSync16 } from "node:fs";
18995
+ import { resolve as resolve15, basename as basename4 } from "node:path";
18996
+ import { homedir as homedir27, platform as platform3, arch as arch3, release as release2, hostname as hostname3 } from "node:os";
18403
18997
  function getSettingsPath(scope, cwd) {
18404
18998
  return claudeCode.getSettingsPath(scope, cwd);
18405
18999
  }
@@ -18506,9 +19100,9 @@ async function installHooksImpl(policyNames, scope = "user", cwd, includeBeta =
18506
19100
  delete configToWrite.customPoliciesPath;
18507
19101
  delete configToWrite.customPoliciesPaths;
18508
19102
  } else if (customPoliciesPath && (typeof customPoliciesPath === "string" || customPoliciesPath.length > 0)) {
18509
- const incoming = (typeof customPoliciesPath === "string" ? [customPoliciesPath] : customPoliciesPath).map((path3) => resolve13(path3));
18510
- const carried = (previousConfig.customPoliciesPaths ?? (previousConfig.customPoliciesPath ? [previousConfig.customPoliciesPath] : [])).map((path3) => resolve13(path3)).filter((path3) => {
18511
- if (existsSync12(path3))
19103
+ const incoming = (typeof customPoliciesPath === "string" ? [customPoliciesPath] : customPoliciesPath).map((path3) => resolve15(path3));
19104
+ const carried = (previousConfig.customPoliciesPaths ?? (previousConfig.customPoliciesPath ? [previousConfig.customPoliciesPath] : [])).map((path3) => resolve15(path3)).filter((path3) => {
19105
+ if (existsSync16(path3))
18512
19106
  return true;
18513
19107
  console.log(`Dropping custom policies path (file no longer exists): ${path3}`);
18514
19108
  return false;
@@ -18700,7 +19294,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
18700
19294
  const scopesToRemove = scope === "all" ? [...integration.scopes] : integration.scopes.includes(scope) ? [scope] : [];
18701
19295
  for (const s of scopesToRemove) {
18702
19296
  const settingsPaths = settingsPathsFor(integration, s, cwd);
18703
- const existing = settingsPaths.filter((p) => existsSync12(p));
19297
+ const existing = settingsPaths.filter((p) => existsSync16(p));
18704
19298
  if (existing.length === 0) {
18705
19299
  if (scope !== "all" && selectedClis.length === 1) {
18706
19300
  console.log("No settings file found. Nothing to remove.");
@@ -18899,9 +19493,9 @@ Failproof AI Hook Policies
18899
19493
  console.log(`
18900
19494
  ── Custom Policies ───────────────────────`);
18901
19495
  for (const path3 of explicitPaths) {
18902
- const absPath = resolve13(findProjectConfigDir(cwd ?? process.cwd()), path3);
19496
+ const absPath = resolve15(findProjectConfigDir(cwd ?? process.cwd()), path3);
18903
19497
  console.log(` ${absPath}`);
18904
- if (!existsSync12(absPath)) {
19498
+ if (!existsSync16(absPath)) {
18905
19499
  console.log(` \x1B[31m✗ File not found: ${absPath}\x1B[0m`);
18906
19500
  continue;
18907
19501
  }
@@ -18920,8 +19514,8 @@ Failproof AI Hook Policies
18920
19514
  console.log();
18921
19515
  }
18922
19516
  const base = findProjectConfigDir(cwd ?? process.cwd());
18923
- const projectDir = resolve13(base, ".failproofai", "policies");
18924
- const userDir = resolve13(homedir25(), ".failproofai", "policies");
19517
+ const projectDir = resolve15(base, ".failproofai", "policies");
19518
+ const userDir = resolve15(homedir27(), ".failproofai", "policies");
18925
19519
  const sameDir = userDir === projectDir;
18926
19520
  const conventionDirs = [
18927
19521
  { label: sameDir ? "Project + User" : "Project", dir: projectDir },
@@ -19151,7 +19745,7 @@ async function promptCliTargetSelection(detected, action = "install") {
19151
19745
  lastLineCount = lines.length;
19152
19746
  }
19153
19747
  const itemCount = options.length;
19154
- return new Promise((resolve14) => {
19748
+ return new Promise((resolve16) => {
19155
19749
  render();
19156
19750
  readline3.emitKeypressEvents(process.stdin);
19157
19751
  const wasRaw = process.stdin.isRaw;
@@ -19184,7 +19778,7 @@ async function promptCliTargetSelection(detected, action = "install") {
19184
19778
  cleanup();
19185
19779
  process.stdout.write(`
19186
19780
  `);
19187
- resolve14(options[cursor2].value);
19781
+ resolve16(options[cursor2].value);
19188
19782
  }
19189
19783
  }
19190
19784
  process.stdin.on("keypress", onKey);
@@ -19337,7 +19931,7 @@ async function promptPolicySelection2(preSelected, options = {}) {
19337
19931
  `);
19338
19932
  lastLineCount = lines.length;
19339
19933
  }
19340
- return new Promise((resolve14) => {
19934
+ return new Promise((resolve16) => {
19341
19935
  render();
19342
19936
  process.stdin.setRawMode(true);
19343
19937
  process.stdin.resume();
@@ -19374,7 +19968,7 @@ async function promptPolicySelection2(preSelected, options = {}) {
19374
19968
  ${dim(STEP_DONE)} Policies
19375
19969
  ${dim(BAR)} ${dim(`${selected.length} selected`)}
19376
19970
  `);
19377
- resolve14(selected);
19971
+ resolve16(selected);
19378
19972
  } else if (key.name === "escape") {
19379
19973
  search = "";
19380
19974
  cursor2 = 0;
@@ -19435,12 +20029,18 @@ var init_telemetry = __esm(() => {
19435
20029
  });
19436
20030
 
19437
20031
  // lib/fetch-with-timeout.ts
20032
+ function fetchWithTimeout(input, init = {}, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
20033
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
20034
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
20035
+ return fetch(input, { ...init, signal });
20036
+ }
19438
20037
  function isAbortError(err) {
19439
20038
  if (err === null || typeof err !== "object")
19440
20039
  return false;
19441
20040
  const name = err.name;
19442
20041
  return name === "AbortError" || name === "TimeoutError";
19443
20042
  }
20043
+ var DEFAULT_FETCH_TIMEOUT_MS = 15000;
19444
20044
 
19445
20045
  // lib/auth/api-server-client.ts
19446
20046
  function getApiBase() {
@@ -19494,7 +20094,7 @@ function pathFromUrl(url) {
19494
20094
  return url;
19495
20095
  }
19496
20096
  }
19497
- async function fetchWithTimeout(url, init) {
20097
+ async function fetchWithTimeout2(url, init) {
19498
20098
  try {
19499
20099
  return await fetch(url, { ...init, signal: timeoutSignal(init.signal ?? undefined) });
19500
20100
  } catch (err) {
@@ -19514,7 +20114,7 @@ async function postJson(path3, body, init) {
19514
20114
  const headers = { "content-type": "application/json" };
19515
20115
  if (init?.accessToken)
19516
20116
  headers["authorization"] = `Bearer ${init.accessToken}`;
19517
- const res = await fetchWithTimeout(`${getApiBase()}${path3}`, {
20117
+ const res = await fetchWithTimeout2(`${getApiBase()}${path3}`, {
19518
20118
  method: "POST",
19519
20119
  headers,
19520
20120
  body: JSON.stringify(body)
@@ -19551,64 +20151,25 @@ var init_api_server_client = __esm(() => {
19551
20151
  };
19552
20152
  });
19553
20153
 
19554
- // lib/atomic-write.ts
19555
- import {
19556
- chmodSync,
19557
- existsSync as existsSync13,
19558
- mkdirSync as mkdirSync8,
19559
- renameSync as renameSync3,
19560
- rmSync,
19561
- statSync as statSync12,
19562
- writeFileSync as writeFileSync7
19563
- } from "node:fs";
19564
- import { dirname as dirname6 } from "node:path";
19565
- import { randomBytes } from "node:crypto";
19566
- function writeJsonAtomically(filePath, value, opts = {}) {
19567
- const mode = opts.mode ?? 384;
19568
- const dirMode = opts.dirMode ?? 448;
19569
- const dir = dirname6(filePath);
19570
- if (!existsSync13(dir))
19571
- mkdirSync8(dir, { recursive: true, mode: dirMode });
19572
- const tmp = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
19573
- try {
19574
- writeFileSync7(tmp, JSON.stringify(value, null, 2), { mode });
19575
- try {
19576
- if ((statSync12(tmp).mode & 63) !== 0)
19577
- chmodSync(tmp, mode);
19578
- } catch {}
19579
- renameSync3(tmp, filePath);
19580
- try {
19581
- if ((statSync12(filePath).mode & 63) !== 0)
19582
- chmodSync(filePath, mode);
19583
- } catch {}
19584
- } catch (err) {
19585
- try {
19586
- rmSync(tmp, { force: true });
19587
- } catch {}
19588
- throw err;
19589
- }
19590
- }
19591
- var init_atomic_write = () => {};
19592
-
19593
20154
  // lib/auth/auth-store.ts
19594
- import { existsSync as existsSync14, readFileSync as readFileSync13, rmSync as rmSync2 } from "node:fs";
19595
- import { homedir as homedir26 } from "node:os";
19596
- import { join as join26 } from "node:path";
20155
+ import { existsSync as existsSync17, readFileSync as readFileSync15, rmSync as rmSync3 } from "node:fs";
20156
+ import { homedir as homedir28 } from "node:os";
20157
+ import { join as join27 } from "node:path";
19597
20158
  function getAuthDir() {
19598
20159
  const override = process.env.FAILPROOFAI_AUTH_DIR;
19599
20160
  if (override)
19600
20161
  return override;
19601
- return join26(homedir26(), ".failproofai");
20162
+ return join27(homedir28(), ".failproofai");
19602
20163
  }
19603
20164
  function getAuthFilePath() {
19604
- return join26(getAuthDir(), "auth.json");
20165
+ return join27(getAuthDir(), "auth.json");
19605
20166
  }
19606
20167
  function readAuth() {
19607
20168
  const p = getAuthFilePath();
19608
- if (!existsSync14(p))
20169
+ if (!existsSync17(p))
19609
20170
  return null;
19610
20171
  try {
19611
- const raw = readFileSync13(p, "utf-8");
20172
+ const raw = readFileSync15(p, "utf-8");
19612
20173
  const parsed = JSON.parse(raw);
19613
20174
  if (typeof parsed.access_token !== "string" || typeof parsed.refresh_token !== "string" || typeof parsed.access_expires_at !== "number" || typeof parsed.user !== "object" || !parsed.user || typeof parsed.user.id !== "string" || typeof parsed.user.email !== "string") {
19614
20175
  return null;
@@ -19632,8 +20193,8 @@ function writeAuth(auth) {
19632
20193
  }
19633
20194
  function deleteAuth() {
19634
20195
  const p = getAuthFilePath();
19635
- if (existsSync14(p))
19636
- rmSync2(p, { force: true });
20196
+ if (existsSync17(p))
20197
+ rmSync3(p, { force: true });
19637
20198
  }
19638
20199
  function authFromTokenResponse(token, existingUser) {
19639
20200
  const now = Math.floor(Date.now() / 1000);
@@ -19709,13 +20270,13 @@ function prompt(question, opts = {}) {
19709
20270
  orig(s);
19710
20271
  };
19711
20272
  }
19712
- return new Promise((resolve14) => {
20273
+ return new Promise((resolve16) => {
19713
20274
  rl.question(question, (answer) => {
19714
20275
  rl.close();
19715
20276
  if (opts.hidden && process.stdin.isTTY)
19716
20277
  process.stdout.write(`
19717
20278
  `);
19718
- resolve14(answer.trim());
20279
+ resolve16(answer.trim());
19719
20280
  });
19720
20281
  });
19721
20282
  }
@@ -19964,8 +20525,8 @@ EXAMPLES
19964
20525
  });
19965
20526
 
19966
20527
  // lib/claude-sessions.ts
19967
- import { readdirSync as readdirSync11, statSync as statSync13, existsSync as existsSync15 } from "node:fs";
19968
- import { join as join27, basename as basename5 } from "node:path";
20528
+ import { readdirSync as readdirSync12, statSync as statSync13, existsSync as existsSync18 } from "node:fs";
20529
+ import { join as join28, basename as basename5 } from "node:path";
19969
20530
  function getClaudeProjectsRoot() {
19970
20531
  return getClaudeProjectsPath();
19971
20532
  }
@@ -19973,21 +20534,21 @@ function listClaudeProjects() {
19973
20534
  const root = getClaudeProjectsRoot();
19974
20535
  let entries;
19975
20536
  try {
19976
- entries = readdirSync11(root, { withFileTypes: true });
20537
+ entries = readdirSync12(root, { withFileTypes: true });
19977
20538
  } catch {
19978
20539
  return [];
19979
20540
  }
19980
20541
  return entries.filter((e) => e.isDirectory()).map((e) => ({
19981
20542
  name: e.name,
19982
20543
  cwd: decodeFolderName(e.name),
19983
- path: join27(root, e.name)
20544
+ path: join28(root, e.name)
19984
20545
  }));
19985
20546
  }
19986
20547
  function listClaudeTranscripts(project) {
19987
20548
  const out = [];
19988
20549
  let entries;
19989
20550
  try {
19990
- entries = readdirSync11(project.path, { withFileTypes: true });
20551
+ entries = readdirSync12(project.path, { withFileTypes: true });
19991
20552
  } catch {
19992
20553
  return out;
19993
20554
  }
@@ -19996,7 +20557,7 @@ function listClaudeTranscripts(project) {
19996
20557
  const sessionId = entry.name.slice(0, -".jsonl".length);
19997
20558
  if (!UUID_RE2.test(sessionId))
19998
20559
  continue;
19999
- const transcriptPath = join27(project.path, entry.name);
20560
+ const transcriptPath = join28(project.path, entry.name);
20000
20561
  try {
20001
20562
  const s = statSync13(transcriptPath);
20002
20563
  out.push({
@@ -20010,12 +20571,12 @@ function listClaudeTranscripts(project) {
20010
20571
  });
20011
20572
  } catch {}
20012
20573
  } else if (entry.isDirectory() && UUID_RE2.test(entry.name)) {
20013
- const subDir = join27(project.path, entry.name, "subagents");
20014
- if (!existsSync15(subDir))
20574
+ const subDir = join28(project.path, entry.name, "subagents");
20575
+ if (!existsSync18(subDir))
20015
20576
  continue;
20016
20577
  let subEntries;
20017
20578
  try {
20018
- subEntries = readdirSync11(subDir, { withFileTypes: true });
20579
+ subEntries = readdirSync12(subDir, { withFileTypes: true });
20019
20580
  } catch {
20020
20581
  continue;
20021
20582
  }
@@ -20023,7 +20584,7 @@ function listClaudeTranscripts(project) {
20023
20584
  if (!sub.isFile() || !sub.name.endsWith(".jsonl"))
20024
20585
  continue;
20025
20586
  const agentId = sub.name.slice(0, -".jsonl".length);
20026
- const transcriptPath = join27(subDir, sub.name);
20587
+ const transcriptPath = join28(subDir, sub.name);
20027
20588
  try {
20028
20589
  const s = statSync13(transcriptPath);
20029
20590
  out.push({
@@ -20099,7 +20660,7 @@ var init_shared = __esm(() => {
20099
20660
  });
20100
20661
 
20101
20662
  // src/audit/cli-adapters/claude.ts
20102
- import { readFile as readFile14 } from "node:fs/promises";
20663
+ import { readFile as readFile15 } from "node:fs/promises";
20103
20664
  async function listClaudeTranscriptMetadata(opts = {}) {
20104
20665
  const projectFilter = opts.projects ? new Set(opts.projects) : null;
20105
20666
  const sinceMs = opts.sinceMs ?? 0;
@@ -20131,7 +20692,7 @@ async function listClaudeTranscriptMetadata(opts = {}) {
20131
20692
  async function streamClaudeEvents(meta) {
20132
20693
  let content;
20133
20694
  try {
20134
- content = await readFile14(meta.transcriptPath, "utf-8");
20695
+ content = await readFile15(meta.transcriptPath, "utf-8");
20135
20696
  } catch {
20136
20697
  return [];
20137
20698
  }
@@ -20663,7 +21224,7 @@ var init_openclaw = __esm(() => {
20663
21224
  });
20664
21225
 
20665
21226
  // src/audit/cli-adapters/factory.ts
20666
- import { readFile as readFile15 } from "node:fs/promises";
21227
+ import { readFile as readFile16 } from "node:fs/promises";
20667
21228
  async function listFactoryTranscriptMetadata(opts = {}) {
20668
21229
  const projectFilter = opts.projects ? new Set(opts.projects) : null;
20669
21230
  const sinceMs = opts.sinceMs ?? 0;
@@ -20687,7 +21248,7 @@ async function listFactoryTranscriptMetadata(opts = {}) {
20687
21248
  async function streamFactoryEvents(meta) {
20688
21249
  let content;
20689
21250
  try {
20690
- content = await readFile15(meta.transcriptPath, "utf-8");
21251
+ content = await readFile16(meta.transcriptPath, "utf-8");
20691
21252
  } catch {
20692
21253
  return [];
20693
21254
  }
@@ -20717,7 +21278,7 @@ var init_factory = __esm(() => {
20717
21278
  });
20718
21279
 
20719
21280
  // src/audit/cli-adapters/antigravity.ts
20720
- import { readFile as readFile16 } from "node:fs/promises";
21281
+ import { readFile as readFile17 } from "node:fs/promises";
20721
21282
  async function listAntigravityTranscriptMetadata(opts = {}) {
20722
21283
  const projectFilter = opts.projects ? new Set(opts.projects) : null;
20723
21284
  const sinceMs = opts.sinceMs ?? 0;
@@ -20743,7 +21304,7 @@ async function listAntigravityTranscriptMetadata(opts = {}) {
20743
21304
  async function streamAntigravityEvents(meta) {
20744
21305
  let content;
20745
21306
  try {
20746
- content = await readFile16(meta.transcriptPath, "utf-8");
21307
+ content = await readFile17(meta.transcriptPath, "utf-8");
20747
21308
  } catch {
20748
21309
  return [];
20749
21310
  }
@@ -21278,16 +21839,16 @@ var init_features = __esm(() => {
21278
21839
  });
21279
21840
 
21280
21841
  // src/audit/cache.ts
21281
- import { createHash } from "node:crypto";
21282
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync8, chmodSync as chmodSync2 } from "node:fs";
21283
- import { join as join28 } from "node:path";
21284
- import { homedir as homedir27 } from "node:os";
21842
+ import { createHash as createHash4 } from "node:crypto";
21843
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync8, chmodSync as chmodSync2 } from "node:fs";
21844
+ import { join as join29 } from "node:path";
21845
+ import { homedir as homedir29 } from "node:os";
21285
21846
  function getEngineVersion() {
21286
21847
  if (cachedEngineVersion)
21287
21848
  return cachedEngineVersion;
21288
21849
  const blob = BUILTIN_POLICIES.map((p) => `${p.name}|${p.fn.toString()}`).sort().join(`
21289
21850
  `);
21290
- cachedEngineVersion = createHash("sha1").update(blob).digest("hex").slice(0, 16);
21851
+ cachedEngineVersion = createHash4("sha1").update(blob).digest("hex").slice(0, 16);
21291
21852
  return cachedEngineVersion;
21292
21853
  }
21293
21854
  function getDetectorVersion() {
@@ -21295,22 +21856,22 @@ function getDetectorVersion() {
21295
21856
  return cachedDetectorVersion;
21296
21857
  const blob = AUDIT_DETECTORS.map((d) => `${d.name}|${d.detect.toString()}`).sort().join(`
21297
21858
  `);
21298
- cachedDetectorVersion = createHash("sha1").update(blob).digest("hex").slice(0, 16);
21859
+ cachedDetectorVersion = createHash4("sha1").update(blob).digest("hex").slice(0, 16);
21299
21860
  return cachedDetectorVersion;
21300
21861
  }
21301
21862
  function getCachePathFor(transcriptPath) {
21302
- const root = join28(homedir27(), ".failproofai", "cache", "audit");
21303
- const key = createHash("sha1").update(transcriptPath).digest("hex");
21304
- return join28(root, `${key}.json`);
21863
+ const root = join29(homedir29(), ".failproofai", "cache", "audit");
21864
+ const key = createHash4("sha1").update(transcriptPath).digest("hex");
21865
+ return join29(root, `${key}.json`);
21305
21866
  }
21306
21867
  function readCachedTranscriptResult(transcriptPath, mtimeMs, sizeBytes) {
21307
21868
  if (sizeBytes === 0)
21308
21869
  return null;
21309
21870
  const cachePath = getCachePathFor(transcriptPath);
21310
- if (!existsSync16(cachePath))
21871
+ if (!existsSync19(cachePath))
21311
21872
  return null;
21312
21873
  try {
21313
- const raw = readFileSync14(cachePath, "utf-8");
21874
+ const raw = readFileSync16(cachePath, "utf-8");
21314
21875
  const entry = JSON.parse(raw);
21315
21876
  if (entry.schemaVersion !== CACHE_SCHEMA_VERSION)
21316
21877
  return null;
@@ -21334,7 +21895,7 @@ function writeCachedTranscriptResult(transcriptPath, mtimeMs, sizeBytes, result)
21334
21895
  return;
21335
21896
  const cachePath = getCachePathFor(transcriptPath);
21336
21897
  try {
21337
- mkdirSync9(join28(homedir27(), ".failproofai", "cache", "audit"), { recursive: true });
21898
+ mkdirSync10(join29(homedir29(), ".failproofai", "cache", "audit"), { recursive: true });
21338
21899
  const entry = {
21339
21900
  schemaVersion: CACHE_SCHEMA_VERSION,
21340
21901
  cachedAt: Date.now(),
@@ -21706,10 +22267,10 @@ var init_audit = __esm(() => {
21706
22267
  });
21707
22268
 
21708
22269
  // src/audit/dashboard-cache.ts
21709
- import { join as join29 } from "node:path";
21710
- import { homedir as homedir28 } from "node:os";
22270
+ import { join as join30 } from "node:path";
22271
+ import { homedir as homedir30 } from "node:os";
21711
22272
  function getCachePath() {
21712
- return join29(homedir28(), ".failproofai", "audit-dashboard.json");
22273
+ return join30(homedir30(), ".failproofai", "audit-dashboard.json");
21713
22274
  }
21714
22275
  function writeDashboardCache(params, result) {
21715
22276
  try {
@@ -21799,14 +22360,14 @@ var POLL_INTERVAL_MS = 150, POLL_TIMEOUT_MS = 30000;
21799
22360
  var init_open_browser = () => {};
21800
22361
 
21801
22362
  // scripts/parse-script-args.ts
21802
- import { resolve as resolve14 } from "path";
22363
+ import { resolve as resolve16 } from "path";
21803
22364
  function parseStringFlag(flagName, errorLabel, inlineValue, args, index, options) {
21804
22365
  const raw = inlineValue ?? args[index + 1];
21805
22366
  if (raw === undefined || inlineValue === null && raw.startsWith("-")) {
21806
22367
  console.error(`Error: ${flagName} requires ${errorLabel}`);
21807
22368
  process.exit(1);
21808
22369
  }
21809
- const value = options?.resolve ? resolve14(raw) : raw;
22370
+ const value = options?.resolve ? resolve16(raw) : raw;
21810
22371
  return { value, spliceCount: inlineValue !== null ? 1 : 2 };
21811
22372
  }
21812
22373
  function parseScriptArgs(argv) {
@@ -21854,21 +22415,21 @@ function parseScriptArgs(argv) {
21854
22415
  var init_parse_script_args = () => {};
21855
22416
 
21856
22417
  // scripts/install-diagnosis.mjs
21857
- import { existsSync as existsSync17, readFileSync as readFileSync15, realpathSync } from "node:fs";
21858
- import { dirname as dirname7, resolve as resolve15 } from "node:path";
21859
- import { homedir as homedir29, platform as platform4 } from "node:os";
22418
+ import { existsSync as existsSync20, readFileSync as readFileSync17, realpathSync as realpathSync2 } from "node:fs";
22419
+ import { dirname as dirname7, resolve as resolve17 } from "node:path";
22420
+ import { homedir as homedir31, platform as platform4 } from "node:os";
21860
22421
  import { spawnSync } from "node:child_process";
21861
22422
  function findPackageRoot(start) {
21862
22423
  try {
21863
- let dir = realpathSync(start);
21864
- if (existsSync17(dir) && !existsSync17(resolve15(dir, "package.json"))) {
22424
+ let dir = realpathSync2(start);
22425
+ if (existsSync20(dir) && !existsSync20(resolve17(dir, "package.json"))) {
21865
22426
  dir = dirname7(dir);
21866
22427
  }
21867
22428
  while (true) {
21868
- const pkgPath = resolve15(dir, "package.json");
21869
- if (existsSync17(pkgPath)) {
22429
+ const pkgPath = resolve17(dir, "package.json");
22430
+ if (existsSync20(pkgPath)) {
21870
22431
  try {
21871
- const pkg = JSON.parse(readFileSync15(pkgPath, "utf8"));
22432
+ const pkg = JSON.parse(readFileSync17(pkgPath, "utf8"));
21872
22433
  if (pkg.name === PKG_NAME)
21873
22434
  return dir;
21874
22435
  } catch {}
@@ -21886,7 +22447,7 @@ function readPackageVersion(packageRoot) {
21886
22447
  if (!packageRoot)
21887
22448
  return null;
21888
22449
  try {
21889
- const pkg = JSON.parse(readFileSync15(resolve15(packageRoot, "package.json"), "utf8"));
22450
+ const pkg = JSON.parse(readFileSync17(resolve17(packageRoot, "package.json"), "utf8"));
21890
22451
  return typeof pkg.version === "string" ? pkg.version : null;
21891
22452
  } catch {
21892
22453
  return null;
@@ -21912,16 +22473,16 @@ function locateNpmGlobal() {
21912
22473
  const root = (res.stdout || "").trim();
21913
22474
  if (!root)
21914
22475
  return null;
21915
- const candidate = resolve15(root, PKG_NAME);
21916
- return existsSync17(resolve15(candidate, "package.json")) ? candidate : null;
22476
+ const candidate = resolve17(root, PKG_NAME);
22477
+ return existsSync20(resolve17(candidate, "package.json")) ? candidate : null;
21917
22478
  } catch {
21918
22479
  return null;
21919
22480
  }
21920
22481
  }
21921
22482
  function locateBunGlobal() {
21922
22483
  try {
21923
- const candidate = resolve15(homedir29(), ".bun", "install", "global", "node_modules", PKG_NAME);
21924
- return existsSync17(resolve15(candidate, "package.json")) ? candidate : null;
22484
+ const candidate = resolve17(homedir31(), ".bun", "install", "global", "node_modules", PKG_NAME);
22485
+ return existsSync20(resolve17(candidate, "package.json")) ? candidate : null;
21925
22486
  } catch {
21926
22487
  return null;
21927
22488
  }
@@ -21929,8 +22490,8 @@ function locateBunGlobal() {
21929
22490
  function buildRecommendation(pathFirstBin) {
21930
22491
  if (!pathFirstBin)
21931
22492
  return null;
21932
- const bunBinPrefix = resolve15(homedir29(), ".bun", "bin") + "/";
21933
- const bunGlobalPrefix = resolve15(homedir29(), ".bun", "install", "global") + "/";
22493
+ const bunBinPrefix = resolve17(homedir31(), ".bun", "bin") + "/";
22494
+ const bunGlobalPrefix = resolve17(homedir31(), ".bun", "install", "global") + "/";
21934
22495
  const isBun = pathFirstBin.startsWith(bunBinPrefix) || pathFirstBin.startsWith(bunGlobalPrefix);
21935
22496
  if (isBun) {
21936
22497
  return `rm -f ~/.bun/bin/${PKG_NAME} && rm -rf ~/.bun/install/global/node_modules/${PKG_NAME}`;
@@ -21940,7 +22501,7 @@ function buildRecommendation(pathFirstBin) {
21940
22501
  function diagnoseShadow(self) {
21941
22502
  const selfPackageRoot = (() => {
21942
22503
  try {
21943
- return self?.selfPackageRoot ? realpathSync(self.selfPackageRoot) : null;
22504
+ return self?.selfPackageRoot ? realpathSync2(self.selfPackageRoot) : null;
21944
22505
  } catch {
21945
22506
  return self?.selfPackageRoot ?? null;
21946
22507
  }
@@ -22010,8 +22571,8 @@ __export(exports_launch, {
22010
22571
  launch: () => launch
22011
22572
  });
22012
22573
  import { spawn as spawn2 } from "child_process";
22013
- import { realpathSync as realpathSync2, existsSync as existsSync18 } from "node:fs";
22014
- import { resolve as resolve16, dirname as dirname8 } from "node:path";
22574
+ import { realpathSync as realpathSync3, existsSync as existsSync21 } from "node:fs";
22575
+ import { resolve as resolve18, dirname as dirname8 } from "node:path";
22015
22576
  import { fileURLToPath as fileURLToPath2 } from "node:url";
22016
22577
  import { createInterface as createInterface2 } from "node:readline";
22017
22578
  function launch(mode) {
@@ -22026,9 +22587,9 @@ function launch(mode) {
22026
22587
  process.env.PORT = port;
22027
22588
  process.env.HOSTNAME = "0.0.0.0";
22028
22589
  cmd = "node";
22029
- const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve16(dirname8(realpathSync2(fileURLToPath2(import.meta.url))), "..");
22030
- const serverJsPath = resolve16(packageRoot, ".next/standalone/server.js");
22031
- if (!existsSync18(serverJsPath)) {
22590
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve18(dirname8(realpathSync3(fileURLToPath2(import.meta.url))), "..");
22591
+ const serverJsPath = resolve18(packageRoot, ".next/standalone/server.js");
22592
+ if (!existsSync21(serverJsPath)) {
22032
22593
  let shadowMessage = null;
22033
22594
  try {
22034
22595
  const diag = diagnoseShadow({ selfPackageRoot: packageRoot, selfVersion: version2 });
@@ -22378,381 +22939,182 @@ WHAT IT DOES
22378
22939
  SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
22379
22940
  });
22380
22941
 
22381
- // src/hooks/manager.ts
22382
- import { execSync as execSync7 } from "node:child_process";
22383
- import { existsSync as existsSync19 } from "node:fs";
22384
- import { resolve as resolve17, basename as basename6 } from "node:path";
22385
- import { homedir as homedir30, platform as platform5, arch as arch4, release as release3, hostname as hostname4 } from "node:os";
22386
- function getSettingsPath2(scope, cwd) {
22387
- return claudeCode.getSettingsPath(scope, cwd);
22942
+ // src/hooks/cloud-enrollment.ts
22943
+ import { existsSync as existsSync22, readFileSync as readFileSync18, rmSync as rmSync4 } from "node:fs";
22944
+ import { homedir as homedir32 } from "node:os";
22945
+ import { join as join31 } from "node:path";
22946
+ function cloudCredentialPath() {
22947
+ const override = process.env.FAILPROOFAI_CLOUD_CREDENTIALS;
22948
+ if (override)
22949
+ return override;
22950
+ return join31(homedir32(), ".failproofai", "cloud.json");
22388
22951
  }
22389
- function scopeLabel2(scope) {
22390
- switch (scope) {
22391
- case "user":
22392
- return `~/.claude/settings.json`;
22393
- case "project":
22394
- return `{cwd}/.claude/settings.json`;
22395
- case "local":
22396
- return `{cwd}/.claude/settings.local.json`;
22952
+ function validateCloudUrl(raw) {
22953
+ let parsed;
22954
+ try {
22955
+ parsed = new URL(raw);
22956
+ } catch {
22957
+ return { ok: false, reason: `"${raw}" is not a valid URL. Expected something like https://be.failproof.ai` };
22958
+ }
22959
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
22960
+ return { ok: false, reason: `Unsupported scheme "${parsed.protocol}" — use https (or http for localhost).` };
22397
22961
  }
22962
+ if (parsed.protocol === "http:") {
22963
+ const host = parsed.hostname;
22964
+ const loopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
22965
+ if (!loopback) {
22966
+ return {
22967
+ ok: false,
22968
+ reason: `Refusing to send the machine token to ${parsed.origin} over plain http. ` + `Use https, or http only for localhost during development.`
22969
+ };
22970
+ }
22971
+ }
22972
+ return { ok: true, url: parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "")) };
22398
22973
  }
22399
- function resolveFailproofaiBinary2() {
22400
- const override = process.env.FAILPROOFAI_BINARY_OVERRIDE;
22401
- if (override && override.trim())
22402
- return override.trim();
22974
+ function maskToken(token) {
22975
+ if (token.length <= 4)
22976
+ return "****";
22977
+ return `****${token.slice(-4)}`;
22978
+ }
22979
+ function readCloudCredentials() {
22980
+ const path3 = cloudCredentialPath();
22981
+ if (!existsSync22(path3))
22982
+ return null;
22403
22983
  try {
22404
- const cmd = process.platform === "win32" ? "where failproofai" : "which failproofai";
22405
- const result = execSync7(cmd, { encoding: "utf8" }).trim();
22406
- return result.split(`
22407
- `)[0].trim();
22984
+ const raw = JSON.parse(readFileSync18(path3, "utf8"));
22985
+ if (raw.schemaVersion !== SCHEMA_VERSION2)
22986
+ return null;
22987
+ if (typeof raw.url !== "string" || typeof raw.machineId !== "string" || typeof raw.token !== "string") {
22988
+ return null;
22989
+ }
22990
+ if (!raw.url || !raw.machineId || !raw.token)
22991
+ return null;
22992
+ return { url: raw.url, machineId: raw.machineId, token: raw.token };
22408
22993
  } catch {
22409
- throw new CliError(`failproofai binary not found in PATH.
22410
- ` + "Install it globally first: npm install -g failproofai");
22994
+ return null;
22411
22995
  }
22412
22996
  }
22413
- function validatePolicyNames2(names) {
22414
- const invalid = names.filter((n) => !VALID_POLICY_NAMES2.has(n));
22415
- if (invalid.length > 0) {
22416
- const validList = [...VALID_POLICY_NAMES2].join(", ");
22417
- throw new CliError(`Unknown policy name(s): ${invalid.join(", ")}
22418
- ` + `Valid policies: ${validList}`);
22419
- }
22997
+ function writeCloudCredentials(creds) {
22998
+ writeJsonAtomically(cloudCredentialPath(), { schemaVersion: SCHEMA_VERSION2, ...creds });
22420
22999
  }
22421
- function deduplicateScopes2(scopes, cwd) {
22422
- const seen = new Set;
22423
- return scopes.filter((s) => {
22424
- const p = getSettingsPath2(s, cwd);
22425
- if (seen.has(p))
22426
- return false;
22427
- seen.add(p);
23000
+ function clearCloudCredentials() {
23001
+ const path3 = cloudCredentialPath();
23002
+ if (!existsSync22(path3))
23003
+ return false;
23004
+ try {
23005
+ rmSync4(path3);
22428
23006
  return true;
22429
- });
22430
- }
22431
- function hooksInstalledInSettings2(scope, cwd) {
22432
- return claudeCode.hooksInstalledInSettings(scope, cwd);
23007
+ } catch {
23008
+ return false;
23009
+ }
22433
23010
  }
22434
- async function installHooks2(policyNames, scope = "user", cwd, includeBeta = false, source, customPoliciesPath, removeCustomHooks = false, cli, options = {}) {
22435
- const { replace = false, quiet = false } = options;
22436
- if (!quiet) {
22437
- return installHooksImpl2(policyNames, scope, cwd, includeBeta, source, customPoliciesPath, removeCustomHooks, cli, replace);
23011
+ async function verifyCloudCredentials(creds) {
23012
+ const url = `${creds.url}/enforcement/v1/desired-state?machineId=${encodeURIComponent(creds.machineId)}`;
23013
+ let response;
23014
+ try {
23015
+ response = await fetchWithTimeout(url, { headers: { Authorization: `Bearer ${creds.token}` } }, VERIFY_TIMEOUT_MS);
23016
+ } catch (err) {
23017
+ if (isAbortError(err))
23018
+ return { ok: false, reason: `No response from ${creds.url} within ${VERIFY_TIMEOUT_MS / 1000}s.` };
23019
+ return { ok: false, reason: `Could not reach ${creds.url}: ${err instanceof Error ? err.message : String(err)}` };
23020
+ }
23021
+ if (response.status === 401) {
23022
+ return { ok: false, reason: "The server rejected this token (401). Check the key was copied whole." };
23023
+ }
23024
+ if (response.status === 403) {
23025
+ return {
23026
+ ok: false,
23027
+ reason: "This key is valid but lacks the `policies:pull` permission (403). Create a machine key with that permission."
23028
+ };
23029
+ }
23030
+ if (!response.ok) {
23031
+ return { ok: false, reason: `The server answered ${response.status} ${response.statusText}.` };
22438
23032
  }
22439
- const origLog = console.log;
22440
- console.log = () => {};
22441
23033
  try {
22442
- return await installHooksImpl2(policyNames, scope, cwd, includeBeta, source, customPoliciesPath, removeCustomHooks, cli, replace);
22443
- } finally {
22444
- console.log = origLog;
23034
+ const body = await response.json();
23035
+ return {
23036
+ ok: true,
23037
+ policyCount: Array.isArray(body.policies) ? body.policies.length : 0,
23038
+ generation: typeof body.generation === "number" ? body.generation : 0
23039
+ };
23040
+ } catch {
23041
+ return {
23042
+ ok: false,
23043
+ reason: `${creds.url} answered, but not with a desired-state document. Is this a Failproof Cloud URL?`
23044
+ };
22445
23045
  }
22446
23046
  }
22447
- async function installHooksImpl2(policyNames, scope = "user", cwd, includeBeta = false, source, customPoliciesPath, removeCustomHooks = false, cli, replace = false) {
22448
- if (policyNames !== undefined && policyNames.length > 0) {
22449
- const nonAllNames = policyNames.filter((n) => n !== "all");
22450
- if (nonAllNames.length > 0)
22451
- validatePolicyNames2(nonAllNames);
22452
- if (policyNames.includes("all") && nonAllNames.length > 0) {
22453
- throw new CliError(`"all" cannot be combined with specific policy names.
22454
- ` + `Use either: --install all or --install block-sudo sanitize-jwt ...`);
22455
- }
23047
+ var SCHEMA_VERSION2 = 1, VERIFY_TIMEOUT_MS = 1e4;
23048
+ var init_cloud_enrollment = __esm(() => {
23049
+ init_atomic_write();
23050
+ });
23051
+
23052
+ // src/hooks/daemon-download.ts
23053
+ import { createHash as createHash5 } from "node:crypto";
23054
+ import { chmodSync as chmodSync3, existsSync as existsSync23, mkdirSync as mkdirSync11, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "node:fs";
23055
+ import { homedir as homedir33 } from "node:os";
23056
+ import { resolve as resolve19 } from "node:path";
23057
+ import { gunzipSync } from "node:zlib";
23058
+ function baseUrl() {
23059
+ return (process.env.FAILPROOFAI_DAEMON_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
23060
+ }
23061
+ function daemonBinaryDir() {
23062
+ return resolve19(homedir33(), ".failproofai", "bin");
23063
+ }
23064
+ function installedBinaryPath(ver = version2) {
23065
+ return resolve19(daemonBinaryDir(), `failproofaid-${ver}`);
23066
+ }
23067
+ function daemonAssetUrl(key, ver = version2) {
23068
+ return `${baseUrl()}/v${ver}/failproofaid-${key}.gz`;
23069
+ }
23070
+ function checksumsUrl(ver = version2) {
23071
+ return `${baseUrl()}/v${ver}/SHA256SUMS`;
23072
+ }
23073
+ async function fetchBytes(url) {
23074
+ const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
23075
+ if (!response.ok)
23076
+ throw new Error(`GET ${url} returned ${response.status}`);
23077
+ return Buffer.from(await response.arrayBuffer());
23078
+ }
23079
+ function digestFor(manifest, assetName) {
23080
+ for (const line of manifest.split(`
23081
+ `)) {
23082
+ const match = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/i);
23083
+ if (match && match[2].trim() === assetName)
23084
+ return match[1].toLowerCase();
22456
23085
  }
22457
- const selectedClis = cli && cli.length > 0 ? [...new Set(cli)] : ["claude"];
22458
- for (const cliId of selectedClis) {
22459
- const integration = getIntegration(cliId);
22460
- if (!integration.scopes.includes(scope)) {
22461
- try {
22462
- await trackHookEvent2(getInstanceId2(), "scope_validation_failed", {
22463
- cli: cliId,
22464
- scope,
22465
- supported_scopes: integration.scopes
22466
- });
22467
- } catch {}
22468
- throw new CliError(`Scope "${scope}" is not supported by ${integration.displayName}. ` + `Valid scopes: ${integration.scopes.join(", ")}`);
22469
- }
23086
+ return null;
23087
+ }
23088
+ async function downloadFailproofaidBinary(key) {
23089
+ const target = installedBinaryPath();
23090
+ if (existsSync23(target))
23091
+ return { path: target };
23092
+ if (process.env.FAILPROOFAI_NO_DOWNLOAD) {
23093
+ return { error: "daemon downloads are disabled (FAILPROOFAI_NO_DOWNLOAD)" };
22470
23094
  }
22471
- const binaryPath = resolveFailproofaiBinary2();
22472
- const previousConfig = readScopedHooksConfig(scope, cwd);
22473
- const previousEnabled = new Set(previousConfig.enabledPolicies);
22474
- let selectedPolicies;
22475
- if (policyNames !== undefined) {
22476
- let incoming;
22477
- if (policyNames.length === 1 && policyNames[0] === "all") {
22478
- incoming = BUILTIN_POLICIES.filter((p) => includeBeta || !p.beta).map((p) => p.name);
22479
- } else {
22480
- incoming = policyNames;
23095
+ const assetName = `failproofaid-${key}.gz`;
23096
+ const tempPath = `${target}.${process.pid}.tmp`;
23097
+ try {
23098
+ const [asset, manifest] = await Promise.all([
23099
+ fetchBytes(daemonAssetUrl(key)),
23100
+ fetchBytes(checksumsUrl()).then((b) => b.toString("utf8"))
23101
+ ]);
23102
+ const expected = digestFor(manifest, assetName);
23103
+ if (!expected) {
23104
+ return { error: `SHA256SUMS for v${version2} has no entry for ${assetName}` };
22481
23105
  }
22482
- selectedPolicies = replace ? [...new Set(incoming)] : [...new Set([...previousConfig.enabledPolicies, ...incoming])];
22483
- } else {
22484
- const preSelected = previousConfig.enabledPolicies.length > 0 ? previousConfig.enabledPolicies : undefined;
22485
- selectedPolicies = await promptPolicySelection(preSelected, { includeBeta });
22486
- }
22487
- const configToWrite = { ...previousConfig, enabledPolicies: selectedPolicies };
22488
- if (removeCustomHooks) {
22489
- delete configToWrite.customPoliciesPath;
22490
- delete configToWrite.customPoliciesPaths;
22491
- } else if (customPoliciesPath && (typeof customPoliciesPath === "string" || customPoliciesPath.length > 0)) {
22492
- const incoming = (typeof customPoliciesPath === "string" ? [customPoliciesPath] : customPoliciesPath).map((path3) => resolve17(path3));
22493
- const carried = (previousConfig.customPoliciesPaths ?? (previousConfig.customPoliciesPath ? [previousConfig.customPoliciesPath] : [])).map((path3) => resolve17(path3)).filter((path3) => {
22494
- if (existsSync19(path3))
22495
- return true;
22496
- console.log(`Dropping custom policies path (file no longer exists): ${path3}`);
22497
- return false;
22498
- });
22499
- configToWrite.customPoliciesPaths = replace ? [...new Set(incoming)] : [...new Set([...carried, ...incoming])];
22500
- delete configToWrite.customPoliciesPath;
22501
- for (const path3 of incoming) {
22502
- let validatedHooks = [];
22503
- try {
22504
- validatedHooks = await loadCustomHooks(path3, { strict: true });
22505
- } catch (err) {
22506
- const msg = err instanceof Error ? err.message : String(err);
22507
- try {
22508
- await trackHookEvent2(getInstanceId2(), "custom_policy_validation_failed", {
22509
- scope,
22510
- error_type: /not found/i.test(msg) ? "file_not_found" : "load_error"
22511
- });
22512
- } catch {}
22513
- console.error(`Error: ${msg}`);
22514
- process.exit(1);
22515
- }
22516
- if (validatedHooks.length === 0) {
22517
- try {
22518
- await trackHookEvent2(getInstanceId2(), "custom_policy_validation_failed", {
22519
- scope,
22520
- error_type: "no_hooks_registered"
22521
- });
22522
- } catch {}
22523
- console.error(`Error: no hooks registered in ${path3}. ` + `Make sure your file calls customPolicies.add(...) at least once.`);
22524
- process.exit(1);
22525
- }
22526
- console.log(`
22527
- Validated ${validatedHooks.length} custom hook(s) from ${path3}: ${validatedHooks.map((h) => h.name).join(", ")}`);
22528
- }
22529
- }
22530
- writeScopedHooksConfig(configToWrite, scope, cwd);
22531
- console.log(`
22532
- Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}
22533
- `);
22534
- if (removeCustomHooks) {
22535
- console.log("Custom hooks path cleared.");
22536
- } else if (configToWrite.customPoliciesPaths?.length || configToWrite.customPoliciesPath) {
22537
- const paths = configToWrite.customPoliciesPaths ?? [configToWrite.customPoliciesPath];
22538
- console.log(`Custom hooks paths: ${paths.join(", ")}`);
22539
- }
22540
- const writtenSettingsPaths = [];
22541
- for (const cliId of selectedClis) {
22542
- const integration = getIntegration(cliId);
22543
- const settingsPaths = settingsPathsFor(integration, scope, cwd);
22544
- try {
22545
- for (const settingsPath of settingsPaths) {
22546
- const settings = integration.readSettings(settingsPath);
22547
- integration.writeHookEntries(settings, binaryPath, scope);
22548
- integration.writeSettings(settingsPath, settings);
22549
- writtenSettingsPaths.push({ cli: cliId, path: settingsPath });
22550
- }
22551
- } catch (err) {
22552
- const errorType = err instanceof Error && /EACCES|EPERM/.test(err.message) ? "permission_denied" : err instanceof Error && /ENOENT|ENOTDIR/.test(err.message) ? "path_not_found" : "write_error";
22553
- try {
22554
- await trackHookEvent2(getInstanceId2(), "hook_write_failed", {
22555
- cli: cliId,
22556
- scope,
22557
- error_type: errorType
22558
- });
22559
- } catch {}
22560
- throw err;
22561
- }
22562
- }
22563
- try {
22564
- const newSet = new Set(selectedPolicies);
22565
- const policiesAdded = selectedPolicies.filter((p) => !previousEnabled.has(p));
22566
- const policiesRemoved = [...previousEnabled].filter((p) => !newSet.has(p));
22567
- const distinctId = getInstanceId2();
22568
- await trackHookEvent2(distinctId, "hooks_installed", {
22569
- scope,
22570
- cli: selectedClis,
22571
- cli_count: selectedClis.length,
22572
- policies: selectedPolicies,
22573
- policy_count: selectedPolicies.length,
22574
- policies_added: policiesAdded,
22575
- policies_removed: policiesRemoved,
22576
- ...source ? { source } : {},
22577
- platform: platform5(),
22578
- arch: arch4(),
22579
- os_release: release3(),
22580
- hostname_hash: hashToId2(hostname4()),
22581
- has_custom_hooks_path: !!(configToWrite.customPoliciesPaths?.length || configToWrite.customPoliciesPath),
22582
- has_policy_params: !!(configToWrite.policyParams && Object.keys(configToWrite.policyParams).length > 0),
22583
- param_policy_names: configToWrite.policyParams ? Object.keys(configToWrite.policyParams) : [],
22584
- command_format: scope === "project" ? "npx" : "absolute"
22585
- });
22586
- if (includeBeta) {
22587
- const betaNames = new Set(BUILTIN_POLICIES.filter((p) => p.beta).map((p) => p.name));
22588
- const installedBeta = selectedPolicies.filter((p) => betaNames.has(p));
22589
- if (installedBeta.length > 0) {
22590
- await trackHookEvent2(distinctId, "beta_policies_installed", {
22591
- scope,
22592
- cli: selectedClis,
22593
- beta_count: installedBeta.length,
22594
- beta_policy_names: installedBeta,
22595
- ...source ? { source } : {}
22596
- });
22597
- }
22598
- }
22599
- } catch {}
22600
- for (const { cli: cliId, path: path3 } of writtenSettingsPaths) {
22601
- const integration = getIntegration(cliId);
22602
- console.log(`Failproof AI hooks installed for ${integration.displayName} ` + `(${integration.eventTypes.length} event types, scope: ${scope}).`);
22603
- console.log(`Settings: ${path3}`);
22604
- }
22605
- if (scope === "project") {
22606
- console.log(`Command: npx -y failproofai`);
22607
- console.log(`
22608
- This file can be committed to git — no machine-specific paths.`);
22609
- } else {
22610
- console.log(`Binary: ${binaryPath}`);
22611
- }
22612
- const otherScopes = deduplicateScopes2(HOOK_SCOPES, cwd).filter((s) => s !== scope);
22613
- const duplicates = otherScopes.filter((s) => hooksInstalledInSettings2(s, cwd));
22614
- if (duplicates.length > 0) {
22615
- const scopeList = duplicates.map((s) => `${s} (${scopeLabel2(s)})`).join(", ");
22616
- console.log();
22617
- console.log(`\x1B[33mWarning: Failproof AI hooks are also installed at ${scopeList}.\x1B[0m`);
22618
- console.log(`Having hooks in multiple scopes may cause duplicate policy evaluation.`);
22619
- console.log(`Use \`failproofai policies --uninstall --scope ${duplicates[0]}\` to remove the other installation,`);
22620
- console.log(`or \`failproofai policies\` to see all scopes.`);
22621
- try {
22622
- await trackHookEvent2(getInstanceId2(), "multi_scope_warning_shown", {
22623
- new_scope: scope,
22624
- existing_scopes: duplicates,
22625
- cli: selectedClis
22626
- });
22627
- } catch {}
22628
- }
22629
- }
22630
- var VALID_POLICY_NAMES2;
22631
- var init_manager2 = __esm(() => {
22632
- init_types();
22633
- init_integrations();
22634
- init_install_prompt();
22635
- init_hooks_config();
22636
- init_builtin_policies();
22637
- init_custom_hooks_loader();
22638
- init_hook_telemetry2();
22639
- init_telemetry_id2();
22640
- init_cli_error();
22641
- init_hook_logger();
22642
- VALID_POLICY_NAMES2 = new Set(BUILTIN_POLICIES.map((p) => p.name));
22643
- });
22644
-
22645
- // src/hooks/policy-presets.ts
22646
- function resolvePreset(id) {
22647
- const preset = POLICY_PRESETS.find((p) => p.id === id);
22648
- if (!preset)
22649
- return [];
22650
- const cats = new Set(preset.categories);
22651
- const fromCategories = BUILTIN_POLICIES.filter((p) => !p.beta && cats.has(p.category)).map((p) => p.name);
22652
- const extras = (preset.extra ?? []).filter((name) => BUILTIN_POLICIES.some((p) => p.name === name && !p.beta));
22653
- return [...new Set([...fromCategories, ...extras])];
22654
- }
22655
- function resolveEverything() {
22656
- return BUILTIN_POLICIES.filter((p) => !p.beta).map((p) => p.name);
22657
- }
22658
- var POLICY_PRESETS;
22659
- var init_policy_presets = __esm(() => {
22660
- init_builtin_policies();
22661
- POLICY_PRESETS = [
22662
- {
22663
- id: "secrets",
22664
- label: "Secrets & data",
22665
- description: "Redact secrets in tool output, block .env & secret-file writes, keep reads inside the repo",
22666
- categories: ["Sanitize", "Environment"],
22667
- extra: ["block-secrets-write"]
22668
- },
22669
- {
22670
- id: "git",
22671
- label: "Git safety",
22672
- description: "Block force-push & pushes to main, warn on history-rewriting git ops",
22673
- categories: ["Git"]
22674
- },
22675
- {
22676
- id: "ship",
22677
- label: "Ship discipline",
22678
- description: "Don't let the agent finish until changes are committed, pushed, PR'd and CI is green",
22679
- categories: ["Workflow"]
22680
- },
22681
- {
22682
- id: "infra",
22683
- label: "Cloud & infra",
22684
- description: "Block kubectl / terraform / aws / gcloud / az / helm / gh pipeline commands",
22685
- categories: ["Infra Commands"]
22686
- }
22687
- ];
22688
- });
22689
-
22690
- // src/hooks/daemon-download.ts
22691
- import { createHash as createHash2 } from "node:crypto";
22692
- import { chmodSync as chmodSync3, existsSync as existsSync20, mkdirSync as mkdirSync10, renameSync as renameSync4, rmSync as rmSync3, writeFileSync as writeFileSync9 } from "node:fs";
22693
- import { homedir as homedir31 } from "node:os";
22694
- import { resolve as resolve18 } from "node:path";
22695
- import { gunzipSync } from "node:zlib";
22696
- function baseUrl() {
22697
- return (process.env.FAILPROOFAI_DAEMON_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
22698
- }
22699
- function daemonBinaryDir() {
22700
- return resolve18(homedir31(), ".failproofai", "bin");
22701
- }
22702
- function installedBinaryPath(ver = version2) {
22703
- return resolve18(daemonBinaryDir(), `failproofaid-${ver}`);
22704
- }
22705
- function daemonAssetUrl(key, ver = version2) {
22706
- return `${baseUrl()}/v${ver}/failproofaid-${key}.gz`;
22707
- }
22708
- function checksumsUrl(ver = version2) {
22709
- return `${baseUrl()}/v${ver}/SHA256SUMS`;
22710
- }
22711
- async function fetchBytes(url) {
22712
- const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
22713
- if (!response.ok)
22714
- throw new Error(`GET ${url} returned ${response.status}`);
22715
- return Buffer.from(await response.arrayBuffer());
22716
- }
22717
- function digestFor(manifest, assetName) {
22718
- for (const line of manifest.split(`
22719
- `)) {
22720
- const match = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/i);
22721
- if (match && match[2].trim() === assetName)
22722
- return match[1].toLowerCase();
22723
- }
22724
- return null;
22725
- }
22726
- async function downloadFailproofaidBinary(key) {
22727
- const target = installedBinaryPath();
22728
- if (existsSync20(target))
22729
- return { path: target };
22730
- if (process.env.FAILPROOFAI_NO_DOWNLOAD) {
22731
- return { error: "daemon downloads are disabled (FAILPROOFAI_NO_DOWNLOAD)" };
22732
- }
22733
- const assetName = `failproofaid-${key}.gz`;
22734
- const tempPath = `${target}.${process.pid}.tmp`;
22735
- try {
22736
- const [asset, manifest] = await Promise.all([
22737
- fetchBytes(daemonAssetUrl(key)),
22738
- fetchBytes(checksumsUrl()).then((b) => b.toString("utf8"))
22739
- ]);
22740
- const expected = digestFor(manifest, assetName);
22741
- if (!expected) {
22742
- return { error: `SHA256SUMS for v${version2} has no entry for ${assetName}` };
22743
- }
22744
- const actual = createHash2("sha256").update(asset).digest("hex");
22745
- if (actual !== expected) {
22746
- return { error: `checksum mismatch for ${assetName} (expected ${expected}, got ${actual})` };
23106
+ const actual = createHash5("sha256").update(asset).digest("hex");
23107
+ if (actual !== expected) {
23108
+ return { error: `checksum mismatch for ${assetName} (expected ${expected}, got ${actual})` };
22747
23109
  }
22748
23110
  const binary = gunzipSync(asset);
22749
- mkdirSync10(daemonBinaryDir(), { recursive: true });
23111
+ mkdirSync11(daemonBinaryDir(), { recursive: true });
22750
23112
  writeFileSync9(tempPath, binary);
22751
23113
  chmodSync3(tempPath, 493);
22752
23114
  renameSync4(tempPath, target);
22753
23115
  return { path: target };
22754
23116
  } catch (err) {
22755
- rmSync3(tempPath, { force: true });
23117
+ rmSync5(tempPath, { force: true });
22756
23118
  const message = err instanceof Error ? err.message : String(err);
22757
23119
  return { error: `failed to download failproofaid v${version2} for ${key}: ${message}` };
22758
23120
  }
@@ -22764,23 +23126,23 @@ var init_daemon_download = __esm(() => {
22764
23126
 
22765
23127
  // src/hooks/daemon-service.ts
22766
23128
  import {
22767
- existsSync as existsSync21,
22768
- mkdirSync as mkdirSync11,
23129
+ existsSync as existsSync24,
23130
+ mkdirSync as mkdirSync12,
22769
23131
  mkdtempSync,
22770
- readFileSync as readFileSync16,
23132
+ readFileSync as readFileSync19,
22771
23133
  writeFileSync as writeFileSync10,
22772
23134
  unlinkSync as unlinkSync3,
22773
- rmSync as rmSync4
23135
+ rmSync as rmSync6
22774
23136
  } from "node:fs";
22775
- import { homedir as homedir32, tmpdir, userInfo } from "node:os";
22776
- import { resolve as resolve19, dirname as dirname9 } from "node:path";
23137
+ import { homedir as homedir34, tmpdir, userInfo } from "node:os";
23138
+ import { resolve as resolve20, dirname as dirname9 } from "node:path";
22777
23139
  import { execFileSync as execFileSync4 } from "node:child_process";
22778
23140
  function setDaemonConfigured(value) {
22779
23141
  const path3 = getConfigPathForScope("user");
22780
23142
  let config = {};
22781
23143
  try {
22782
- if (existsSync21(path3))
22783
- config = JSON.parse(readFileSync16(path3, "utf8"));
23144
+ if (existsSync24(path3))
23145
+ config = JSON.parse(readFileSync19(path3, "utf8"));
22784
23146
  } catch {
22785
23147
  return;
22786
23148
  }
@@ -22789,7 +23151,7 @@ function setDaemonConfigured(value) {
22789
23151
  else
22790
23152
  delete config.daemonConfigured;
22791
23153
  try {
22792
- mkdirSync11(dirname9(path3), { recursive: true });
23154
+ mkdirSync12(dirname9(path3), { recursive: true });
22793
23155
  writeFileSync10(path3, JSON.stringify(config, null, 2) + `
22794
23156
  `, "utf8");
22795
23157
  } catch {}
@@ -22799,22 +23161,22 @@ function isDaemonSupportedPlatform() {
22799
23161
  }
22800
23162
  function platformKey() {
22801
23163
  const os4 = process.platform === "linux" ? "linux" : process.platform === "darwin" ? "darwin" : null;
22802
- const arch5 = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : null;
22803
- if (!os4 || !arch5)
23164
+ const arch4 = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : null;
23165
+ if (!os4 || !arch4)
22804
23166
  return null;
22805
- return `${os4}-${arch5}`;
23167
+ return `${os4}-${arch4}`;
22806
23168
  }
22807
23169
  function resolveFailproofaidBinaryPath() {
22808
23170
  if (process.env.FAILPROOFAI_DAEMON_BINARY)
22809
23171
  return process.env.FAILPROOFAI_DAEMON_BINARY;
22810
23172
  const downloaded = installedBinaryPath();
22811
- if (existsSync21(downloaded))
23173
+ if (existsSync24(downloaded))
22812
23174
  return downloaded;
22813
23175
  const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT;
22814
23176
  if (packageRoot) {
22815
23177
  for (const profile of ["release", "debug"]) {
22816
- const candidate = resolve19(packageRoot, "target", profile, "failproofaid");
22817
- if (existsSync21(candidate))
23178
+ const candidate = resolve20(packageRoot, "target", profile, "failproofaid");
23179
+ if (existsSync24(candidate))
22818
23180
  return candidate;
22819
23181
  }
22820
23182
  }
@@ -22839,8 +23201,8 @@ function resolveWorkerCommand() {
22839
23201
  const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT;
22840
23202
  if (!packageRoot)
22841
23203
  return null;
22842
- const workerScript = resolve19(packageRoot, "dist", "worker.mjs");
22843
- if (!existsSync21(workerScript))
23204
+ const workerScript = resolve20(packageRoot, "dist", "worker.mjs");
23205
+ if (!existsSync24(workerScript))
22844
23206
  return null;
22845
23207
  return `${shellQuote(process.execPath)} ${shellQuote(workerScript)}`;
22846
23208
  }
@@ -22854,10 +23216,10 @@ function systemdUnitName(user = serviceUser()) {
22854
23216
  return `failproofaid@${user}.service`;
22855
23217
  }
22856
23218
  function systemdUnitPath(user = serviceUser()) {
22857
- return resolve19("/etc/systemd/system", systemdUnitName(user));
23219
+ return resolve20("/etc/systemd/system", systemdUnitName(user));
22858
23220
  }
22859
23221
  function legacySystemdUserUnitPath() {
22860
- return resolve19(homedir32(), ".config", "systemd", "user", "failproofaid.service");
23222
+ return resolve20(homedir34(), ".config", "systemd", "user", "failproofaid.service");
22861
23223
  }
22862
23224
  function launchdLabel(user = serviceUser()) {
22863
23225
  return `ai.failproof.failproofaid.${user}`;
@@ -22866,7 +23228,7 @@ function launchdPlistPath(user = serviceUser()) {
22866
23228
  return `/Library/LaunchDaemons/${launchdLabel(user)}.plist`;
22867
23229
  }
22868
23230
  function legacyLaunchAgentPlistPath() {
22869
- return resolve19(homedir32(), "Library", "LaunchAgents", "ai.failproof.failproofaid.plist");
23231
+ return resolve20(homedir34(), "Library", "LaunchAgents", "ai.failproof.failproofaid.plist");
22870
23232
  }
22871
23233
  function legacySharedLaunchDaemonPath() {
22872
23234
  return "/Library/LaunchDaemons/ai.failproof.failproofaid.plist";
@@ -22904,20 +23266,20 @@ function runPrivileged(command, args) {
22904
23266
  execFileSync4(cmd, argv, { stdio: "ignore", timeout: SERVICE_CMD_TIMEOUT_MS });
22905
23267
  }
22906
23268
  function writePrivilegedFile(destination, contents, mode = "0644") {
22907
- const stagingDir = mkdtempSync(resolve19(tmpdir(), "failproofaid-stage-"));
22908
- const staging = resolve19(stagingDir, "service-definition");
23269
+ const stagingDir = mkdtempSync(resolve20(tmpdir(), "failproofaid-stage-"));
23270
+ const staging = resolve20(stagingDir, "service-definition");
22909
23271
  try {
22910
23272
  writeFileSync10(staging, contents, "utf8");
22911
23273
  runPrivileged("install", ["-m", mode, staging, destination]);
22912
23274
  } finally {
22913
- rmSync4(stagingDir, { recursive: true, force: true });
23275
+ rmSync6(stagingDir, { recursive: true, force: true });
22914
23276
  }
22915
23277
  }
22916
23278
  function removeLegacyUserService() {
22917
23279
  try {
22918
23280
  if (process.platform === "linux") {
22919
23281
  const legacy = legacySystemdUserUnitPath();
22920
- if (!existsSync21(legacy))
23282
+ if (!existsSync24(legacy))
22921
23283
  return;
22922
23284
  try {
22923
23285
  execFileSync4("systemctl", ["--user", "disable", "--now", "failproofaid.service"], {
@@ -22934,7 +23296,7 @@ function removeLegacyUserService() {
22934
23296
  } catch {}
22935
23297
  } else {
22936
23298
  const legacyAgent = legacyLaunchAgentPlistPath();
22937
- if (existsSync21(legacyAgent)) {
23299
+ if (existsSync24(legacyAgent)) {
22938
23300
  try {
22939
23301
  execFileSync4("launchctl", ["unload", "-w", legacyAgent], {
22940
23302
  stdio: "ignore",
@@ -22944,7 +23306,7 @@ function removeLegacyUserService() {
22944
23306
  unlinkSync3(legacyAgent);
22945
23307
  }
22946
23308
  const legacyDaemon = legacySharedLaunchDaemonPath();
22947
- if (existsSync21(legacyDaemon)) {
23309
+ if (existsSync24(legacyDaemon)) {
22948
23310
  try {
22949
23311
  runPrivileged("launchctl", ["unload", "-w", legacyDaemon]);
22950
23312
  } catch {}
@@ -22970,7 +23332,7 @@ User=${user}
22970
23332
  # failproofaid is user-scope by construction and refuses to start without
22971
23333
  # HOME ("HOME is not set; failproofaid is user-scope only"), so the one
22972
23334
  # variable it cannot do without is not left to a version-dependent default.
22973
- Environment="HOME=${homedir32()}"
23335
+ Environment="HOME=${homedir34()}"
22974
23336
  ${envLine}ExecStart=${binaryPath}
22975
23337
  Restart=on-failure
22976
23338
  RestartSec=2
@@ -23013,9 +23375,9 @@ function launchdPlistContents(binaryPath, logDir2, workerCmd) {
23013
23375
  <key>UserName</key>
23014
23376
  <string>${escapeXml(serviceUser())}</string>
23015
23377
  ${envBlock} <key>StandardOutPath</key>
23016
- <string>${escapeXml(resolve19(logDir2, "failproofaid.log"))}</string>
23378
+ <string>${escapeXml(resolve20(logDir2, "failproofaid.log"))}</string>
23017
23379
  <key>StandardErrorPath</key>
23018
- <string>${escapeXml(resolve19(logDir2, "failproofaid.err.log"))}</string>
23380
+ <string>${escapeXml(resolve20(logDir2, "failproofaid.err.log"))}</string>
23019
23381
  </dict>
23020
23382
  </plist>
23021
23383
  `;
@@ -23057,77 +23419,874 @@ async function installDaemonService() {
23057
23419
  runPrivileged("systemctl", ["enable", "--now", systemdUnitName()]);
23058
23420
  } else {
23059
23421
  const plistPath = launchdPlistPath();
23060
- const logDir2 = resolve19(homedir32(), ".failproofai", "logs");
23061
- mkdirSync11(logDir2, { recursive: true });
23422
+ const logDir2 = resolve20(homedir34(), ".failproofai", "logs");
23423
+ mkdirSync12(logDir2, { recursive: true });
23424
+ try {
23425
+ runPrivileged("launchctl", ["unload", plistPath]);
23426
+ } catch {}
23427
+ writePrivilegedFile(plistPath, launchdPlistContents(binaryPath, logDir2, workerCmd));
23428
+ runPrivileged("launchctl", ["load", "-w", plistPath]);
23429
+ }
23430
+ } catch (err) {
23431
+ const msg = err instanceof Error ? err.message : String(err);
23432
+ hookLogWarn(`daemon service install failed: ${msg}`);
23433
+ return { installed: false, reason: msg };
23434
+ }
23435
+ if (!await waitForDaemonRunning()) {
23436
+ const reason = `failproofaid was installed but did not reach a running state within ${SERVICE_START_TIMEOUT_MS}ms (status: ${daemonServiceStatus()})`;
23437
+ hookLogWarn(`daemon service install failed: ${reason}`);
23438
+ return { installed: false, reason };
23439
+ }
23440
+ return { installed: true };
23441
+ }
23442
+ async function waitForDaemonRunning() {
23443
+ const deadline = Date.now() + SERVICE_START_TIMEOUT_MS;
23444
+ for (;; ) {
23445
+ if (daemonServiceStatus() === "running")
23446
+ break;
23447
+ if (Date.now() >= deadline)
23448
+ return false;
23449
+ await new Promise((r) => setTimeout(r, SERVICE_START_POLL_MS));
23450
+ }
23451
+ await new Promise((r) => setTimeout(r, SERVICE_SETTLE_MS));
23452
+ return daemonServiceStatus() === "running";
23453
+ }
23454
+ function daemonServiceStatus() {
23455
+ if (!isDaemonSupportedPlatform())
23456
+ return "unsupported-platform";
23457
+ if (process.platform === "linux") {
23458
+ if (!existsSync24(systemdUnitPath()))
23459
+ return "not-installed";
23460
+ try {
23461
+ const out = execFileSync4("systemctl", ["is-active", systemdUnitName()], {
23462
+ stdio: ["ignore", "pipe", "ignore"],
23463
+ timeout: SERVICE_CMD_TIMEOUT_MS
23464
+ }).toString().trim();
23465
+ return out === "active" ? "running" : "stopped";
23466
+ } catch {
23467
+ return "stopped";
23468
+ }
23469
+ }
23470
+ const plistPath = launchdPlistPath();
23471
+ if (!existsSync24(plistPath))
23472
+ return "not-installed";
23473
+ try {
23474
+ const root = typeof process.getuid === "function" && process.getuid() === 0;
23475
+ const args = ["print", `system/${launchdLabel()}`];
23476
+ const out = root ? execFileSync4("launchctl", args, {
23477
+ stdio: ["ignore", "pipe", "ignore"],
23478
+ timeout: SERVICE_CMD_TIMEOUT_MS
23479
+ }).toString() : execFileSync4("sudo", ["-n", "launchctl", ...args], {
23480
+ stdio: ["ignore", "pipe", "ignore"],
23481
+ timeout: SERVICE_CMD_TIMEOUT_MS
23482
+ }).toString();
23483
+ return /state\s*=\s*running/.test(out) ? "running" : "stopped";
23484
+ } catch {
23485
+ return "stopped";
23486
+ }
23487
+ }
23488
+ var SERVICE_CMD_TIMEOUT_MS = 1e4, SERVICE_START_TIMEOUT_MS = 5000, SERVICE_START_POLL_MS = 100, SERVICE_SETTLE_MS = 750, SUDO_PROMPT_TIMEOUT_MS = 120000;
23489
+ var init_daemon_service = __esm(() => {
23490
+ init_hook_logger();
23491
+ init_hooks_config();
23492
+ init_daemon_download();
23493
+ });
23494
+
23495
+ // src/hooks/daemon-client.ts
23496
+ import { resolve as resolve21 } from "node:path";
23497
+ import { homedir as homedir35 } from "node:os";
23498
+ import { existsSync as existsSync25 } from "node:fs";
23499
+ function daemonSocketPresent2() {
23500
+ try {
23501
+ return existsSync25(socketPath2());
23502
+ } catch {
23503
+ return false;
23504
+ }
23505
+ }
23506
+ function socketPath2() {
23507
+ if (process.env.FAILPROOFAI_DAEMON_SOCKET)
23508
+ return process.env.FAILPROOFAI_DAEMON_SOCKET;
23509
+ return resolve21(homedir35(), ".failproofai", "run", "failproofaid.sock");
23510
+ }
23511
+ var MAX_FRAME_LEN2;
23512
+ var init_daemon_client2 = __esm(() => {
23513
+ init_hooks_config();
23514
+ MAX_FRAME_LEN2 = 16 * 1024 * 1024;
23515
+ });
23516
+
23517
+ // src/hooks/collector-config.ts
23518
+ import {
23519
+ writeFileSync as writeFileSync11,
23520
+ mkdirSync as mkdirSync13,
23521
+ existsSync as existsSync26,
23522
+ chmodSync as chmodSync4,
23523
+ statSync as statSync18,
23524
+ readFileSync as readFileSync20,
23525
+ rmSync as rmSync7
23526
+ } from "node:fs";
23527
+ import { join as join32 } from "node:path";
23528
+ import { homedir as homedir36 } from "node:os";
23529
+ function failproofaiHome() {
23530
+ return process.env.FAILPROOFAI_HOME || join32(homedir36(), ".failproofai");
23531
+ }
23532
+ function ingestPath() {
23533
+ return join32(failproofaiHome(), "ingest.json");
23534
+ }
23535
+ function hasIngestCredential() {
23536
+ return existsSync26(ingestPath());
23537
+ }
23538
+ function readIngestCredential() {
23539
+ try {
23540
+ const raw = JSON.parse(readFileSync20(ingestPath(), "utf8"));
23541
+ if (typeof raw.url !== "string" || typeof raw.key !== "string")
23542
+ return null;
23543
+ if (!raw.url || !raw.key)
23544
+ return null;
23545
+ return { url: raw.url, key: raw.key };
23546
+ } catch {
23547
+ return null;
23548
+ }
23549
+ }
23550
+ function clearIngestCredential() {
23551
+ try {
23552
+ rmSync7(ingestPath());
23553
+ return true;
23554
+ } catch {
23555
+ return false;
23556
+ }
23557
+ }
23558
+ function writeIngestCredential(cred) {
23559
+ const home = failproofaiHome();
23560
+ mkdirSync13(home, { recursive: true });
23561
+ try {
23562
+ const mode = statSync18(home).mode & 511;
23563
+ if (mode & 63)
23564
+ chmodSync4(home, 448);
23565
+ } catch {}
23566
+ const path3 = ingestPath();
23567
+ writeFileSync11(path3, JSON.stringify(cred, null, 2) + `
23568
+ `, { mode: 384 });
23569
+ try {
23570
+ chmodSync4(path3, 384);
23571
+ } catch {}
23572
+ return path3;
23573
+ }
23574
+ function writeCollectorSettings(settings) {
23575
+ const config = readHooksConfig();
23576
+ writeHooksConfig({
23577
+ ...config,
23578
+ collector: {
23579
+ sessions: settings.sessions,
23580
+ hooks: settings.hooks,
23581
+ hooksVerbosity: settings.hooksVerbosity ?? "decisions",
23582
+ redact: settings.redact ?? "minimal",
23583
+ environment: settings.environment ?? "local"
23584
+ }
23585
+ });
23586
+ }
23587
+ async function validateIngestKey(cred, fetchImpl = fetch, timeoutMs = 1e4) {
23588
+ const controller = new AbortController;
23589
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
23590
+ try {
23591
+ const res = await fetchImpl(cred.url, {
23592
+ method: "POST",
23593
+ headers: {
23594
+ Authorization: `Bearer ${cred.key}`,
23595
+ "Content-Type": "application/x-ndjson"
23596
+ },
23597
+ body: "",
23598
+ redirect: "manual",
23599
+ signal: controller.signal
23600
+ });
23601
+ if (res.status >= 300 && res.status < 400) {
23602
+ return {
23603
+ ok: false,
23604
+ reason: "that URL redirects rather than accepting events — it looks like the dashboard, not the ingest API"
23605
+ };
23606
+ }
23607
+ if (res.status === 401 || res.status === 403) {
23608
+ return { ok: false, reason: `the server rejected that key (${res.status})` };
23609
+ }
23610
+ if (res.status === 404) {
23611
+ return { ok: false, reason: `no ingest endpoint at that URL (404)` };
23612
+ }
23613
+ if (!res.ok) {
23614
+ return { ok: false, reason: `the server answered ${res.status}` };
23615
+ }
23616
+ let body;
23617
+ try {
23618
+ body = await res.json();
23619
+ } catch {
23620
+ return {
23621
+ ok: false,
23622
+ reason: "that URL answered without an ingest response — it is not the events endpoint"
23623
+ };
23624
+ }
23625
+ const shaped = typeof body === "object" && body !== null && typeof body.accepted === "number";
23626
+ if (!shaped) {
23627
+ return {
23628
+ ok: false,
23629
+ reason: "that URL answered, but not with an ingest response — it is not the events endpoint"
23630
+ };
23631
+ }
23632
+ return { ok: true };
23633
+ } catch (err) {
23634
+ const msg = err instanceof Error ? err.message : String(err);
23635
+ return { ok: false, reason: `could not reach the server (${msg})` };
23636
+ } finally {
23637
+ clearTimeout(timer);
23638
+ }
23639
+ }
23640
+ var DEFAULT_INGEST_URL = "https://server.befailproof.ai/events";
23641
+ var init_collector_config = __esm(() => {
23642
+ init_hooks_config();
23643
+ });
23644
+
23645
+ // src/hooks/cloud-connection.ts
23646
+ function ingestUrlFor(baseUrl2) {
23647
+ return `${baseUrl2.replace(/\/$/, "")}${INGEST_PATH}`;
23648
+ }
23649
+ function cloudBaseFor(url) {
23650
+ const trimmed = url.replace(/\/$/, "");
23651
+ return trimmed.endsWith(INGEST_PATH) ? trimmed.slice(0, -INGEST_PATH.length) : trimmed;
23652
+ }
23653
+ async function connectToCloud(input) {
23654
+ const creds = {
23655
+ url: input.url,
23656
+ machineId: input.machineId,
23657
+ token: input.token
23658
+ };
23659
+ const verifyPolicy = input.verifyPolicy ?? verifyCloudCredentials;
23660
+ const verifyIngest = input.verifyIngest ?? validateIngestKey;
23661
+ const ingestUrl = ingestUrlFor(input.url);
23662
+ const [policyResult, ingestResult] = await Promise.all([
23663
+ verifyPolicy(creds),
23664
+ verifyIngest({ url: ingestUrl, key: input.token })
23665
+ ]);
23666
+ const outcome = {
23667
+ policy: policyResult.ok ? { ok: true, policyCount: policyResult.policyCount, generation: policyResult.generation } : { ok: false, reason: policyResult.reason },
23668
+ ingest: ingestResult.ok ? { ok: true } : { ok: false, reason: ingestResult.reason },
23669
+ anyConfigured: false
23670
+ };
23671
+ if (policyResult.ok) {
23672
+ writeCloudCredentials(creds);
23673
+ outcome.anyConfigured = true;
23674
+ }
23675
+ if (ingestResult.ok) {
23676
+ writeIngestCredential({ url: ingestUrl, key: input.token });
23677
+ const settings = {
23678
+ hooks: true,
23679
+ sessions: input.sessions === true,
23680
+ environment: input.environment
23681
+ };
23682
+ writeCollectorSettings(settings);
23683
+ outcome.anyConfigured = true;
23684
+ }
23685
+ return outcome;
23686
+ }
23687
+ function describeOutcome(outcome, machineId, url) {
23688
+ const lines = [];
23689
+ if (outcome.policy.ok && outcome.ingest.ok) {
23690
+ lines.push(`Connected to ${url} as ${machineId}.`);
23691
+ const n = outcome.policy.policyCount ?? 0;
23692
+ lines.push(` Policy ${n} polic${n === 1 ? "y" : "ies"} assigned (generation ${outcome.policy.generation ?? 0}).`);
23693
+ lines.push(` Dashboard hook activity will be sent to ${ingestUrlFor(url)}.`);
23694
+ return lines;
23695
+ }
23696
+ if (outcome.policy.ok && !outcome.ingest.ok) {
23697
+ const n = outcome.policy.policyCount ?? 0;
23698
+ lines.push(`Connected to ${url} as ${machineId} for policy only.`);
23699
+ lines.push(` Policy ${n} polic${n === 1 ? "y" : "ies"} assigned (generation ${outcome.policy.generation ?? 0}).`);
23700
+ lines.push(` Dashboard NOT configured: ${outcome.ingest.reason}`);
23701
+ lines.push("");
23702
+ lines.push(" Enforcement works. Nothing will appear in the dashboard until this");
23703
+ lines.push(" key also carries `events:add`, or you re-run --connect with one that does.");
23704
+ return lines;
23705
+ }
23706
+ if (!outcome.policy.ok && outcome.ingest.ok) {
23707
+ lines.push(`Connected to ${url} as ${machineId} for dashboard reporting only.`);
23708
+ lines.push(` Dashboard hook activity will be sent to ${ingestUrlFor(url)}.`);
23709
+ lines.push(` Cloud policy NOT configured: ${outcome.policy.reason}`);
23710
+ lines.push("");
23711
+ lines.push(" This machine keeps enforcing its LOCAL policies and will report what");
23712
+ lines.push(" they decide, but will not receive centrally-managed ones.");
23713
+ return lines;
23714
+ }
23715
+ lines.push(`Could not connect to ${url}.`);
23716
+ lines.push(` Policy: ${outcome.policy.reason}`);
23717
+ lines.push(` Dashboard: ${outcome.ingest.reason}`);
23718
+ return lines;
23719
+ }
23720
+ function configuredPaths(outcome) {
23721
+ const paths = [];
23722
+ if (outcome.policy.ok)
23723
+ paths.push(cloudCredentialPath());
23724
+ if (outcome.ingest.ok)
23725
+ paths.push(ingestPath());
23726
+ return paths;
23727
+ }
23728
+ var INGEST_PATH = "/events";
23729
+ var init_cloud_connection = __esm(() => {
23730
+ init_cloud_enrollment();
23731
+ init_collector_config();
23732
+ });
23733
+
23734
+ // src/hooks/cloud-enrollment-cli.ts
23735
+ var exports_cloud_enrollment_cli = {};
23736
+ __export(exports_cloud_enrollment_cli, {
23737
+ runDisconnectCommand: () => runDisconnectCommand,
23738
+ runConnectCommand: () => runConnectCommand,
23739
+ connectionStatusLines: () => connectionStatusLines
23740
+ });
23741
+ function daemonWarning(status) {
23742
+ if (status === "running")
23743
+ return [];
23744
+ if (daemonSocketPresent2()) {
23745
+ return [
23746
+ "",
23747
+ " A daemon is running outside the service manager, so policy is being pulled.",
23748
+ " Install it as a service to survive reboot and logout: failproofai config"
23749
+ ];
23750
+ }
23751
+ if (status === "unsupported-platform") {
23752
+ return [
23753
+ "",
23754
+ "! failproofaid does not run on this platform yet, so nothing will be pulled.",
23755
+ " The credentials are stored and will work on a supported machine."
23756
+ ];
23757
+ }
23758
+ if (status === "not-installed") {
23759
+ return [
23760
+ "",
23761
+ "! failproofaid is not installed as a service, so nothing will be pulled yet.",
23762
+ " Install it with: failproofai config"
23763
+ ];
23764
+ }
23765
+ return [
23766
+ "",
23767
+ "! failproofaid is installed but not running, so nothing will be pulled yet.",
23768
+ " Check it with: failproofai config --status"
23769
+ ];
23770
+ }
23771
+ async function runConnectCommand(opts) {
23772
+ if (!opts.url)
23773
+ return { exitCode: 1, lines: ["--connect needs a URL, e.g. --connect https://be.failproof.ai"] };
23774
+ if (!opts.token) {
23775
+ return {
23776
+ exitCode: 1,
23777
+ lines: [
23778
+ "--connect needs a machine token: --token <key>",
23779
+ "",
23780
+ "Create an API key carrying only the `policies:pull` permission for this",
23781
+ "machine. Do not use an admin key."
23782
+ ]
23783
+ };
23784
+ }
23785
+ const validated = validateCloudUrl(cloudBaseFor(opts.url));
23786
+ if (!validated.ok)
23787
+ return { exitCode: 1, lines: [validated.reason] };
23788
+ const machineId = (opts.machineId ?? opts.defaultMachineId ?? "").trim();
23789
+ if (!machineId) {
23790
+ return { exitCode: 1, lines: ["Could not determine a machine id — pass one with --machine-id <id>."] };
23791
+ }
23792
+ const outcome = await connectToCloud({
23793
+ url: validated.url,
23794
+ token: opts.token,
23795
+ machineId,
23796
+ sessions: opts.sessions,
23797
+ environment: opts.environment,
23798
+ verifyPolicy: opts.verify,
23799
+ verifyIngest: opts.verifyIngest
23800
+ });
23801
+ if (!outcome.anyConfigured) {
23802
+ return { exitCode: 1, lines: describeOutcome(outcome, machineId, validated.url) };
23803
+ }
23804
+ const exitCode = outcome.policy.ok ? 0 : 1;
23805
+ const status = (opts.daemonStatus ?? daemonServiceStatus)();
23806
+ const paths = configuredPaths(outcome);
23807
+ const lines = [
23808
+ ...describeOutcome(outcome, machineId, validated.url),
23809
+ ` Token ${maskToken(opts.token)} stored in ${paths.join(" and ")} (owner-only).`
23810
+ ];
23811
+ if (outcome.ingest.ok && opts.sessions !== true) {
23812
+ lines.push(" Session transcripts are NOT being sent. Add --send-transcripts to include them.");
23813
+ }
23814
+ lines.push(...daemonWarning(status));
23815
+ return { exitCode, lines };
23816
+ }
23817
+ function runDisconnectCommand() {
23818
+ const existing = readCloudCredentials();
23819
+ const hadIngest = hasIngestCredential();
23820
+ const removed = clearCloudCredentials();
23821
+ const removedIngest = clearIngestCredential();
23822
+ if (!removed && !existing && !hadIngest) {
23823
+ return { exitCode: 0, lines: ["This machine is not connected to Failproof Cloud."] };
23824
+ }
23825
+ const lines = [
23826
+ "Disconnected from Failproof Cloud.",
23827
+ "",
23828
+ " Cloud-managed policies already on disk stop being refreshed. Local",
23829
+ " builtin, custom and convention policies are unaffected."
23830
+ ];
23831
+ if (removedIngest) {
23832
+ lines.push(" Hook activity and transcripts stop being sent.");
23833
+ }
23834
+ return { exitCode: 0, lines };
23835
+ }
23836
+ function connectionStatusLines(daemonStatus = daemonServiceStatus) {
23837
+ const envUrl = process.env.FAILPROOFAI_CLOUD_URL;
23838
+ if (envUrl) {
23839
+ return [
23840
+ `Cloud: configured by environment (${envUrl}), which takes precedence over ${cloudCredentialPath()}.`
23841
+ ];
23842
+ }
23843
+ const creds = readCloudCredentials();
23844
+ const ingest = readIngestCredential();
23845
+ if (!creds && !ingest)
23846
+ return ["Cloud: not connected."];
23847
+ const lines = [];
23848
+ if (creds) {
23849
+ lines.push(`Cloud: connected to ${creds.url} as ${creds.machineId} (token ${maskToken(creds.token)}).`);
23850
+ lines.push(` Policy pulling centrally-managed policies.`);
23851
+ } else {
23852
+ lines.push(`Cloud: connected to ${cloudBaseFor(ingest.url)} for reporting only.`);
23853
+ lines.push(` Policy NOT pulling — this machine enforces only its local policies.`);
23854
+ }
23855
+ if (ingest) {
23856
+ lines.push(` Dashboard sending hook activity to ${ingest.url}.`);
23857
+ } else {
23858
+ lines.push(` Dashboard NOT sending — nothing from this machine appears in the dashboard.`);
23859
+ lines.push(` Re-run: failproofai config --connect ${creds.url} --token <key>`);
23860
+ }
23861
+ lines.push(...daemonWarning(daemonStatus()).map((l) => l === "" ? "" : ` ${l.trim()}`));
23862
+ return lines;
23863
+ }
23864
+ var init_cloud_enrollment_cli = __esm(() => {
23865
+ init_cloud_enrollment();
23866
+ init_daemon_service();
23867
+ init_daemon_client2();
23868
+ init_cloud_connection();
23869
+ init_collector_config();
23870
+ });
23871
+
23872
+ // src/hooks/session-pause-cli.ts
23873
+ var exports_session_pause_cli = {};
23874
+ __export(exports_session_pause_cli, {
23875
+ runPauseCommand: () => runPauseCommand,
23876
+ effectiveCeilingMs: () => effectiveCeilingMs
23877
+ });
23878
+ function effectiveCeilingMs(cwd) {
23879
+ try {
23880
+ const configured = readMergedHooksConfig(cwd).maxPauseMs;
23881
+ if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
23882
+ return Math.min(configured, PAUSE_CEILING_MS);
23883
+ }
23884
+ } catch {}
23885
+ return PAUSE_CEILING_MS;
23886
+ }
23887
+ function describe(pause, now) {
23888
+ const remaining = formatDuration2(Math.max(0, pause.expiresAt - now));
23889
+ const at = new Date(pause.expiresAt).toLocaleTimeString();
23890
+ const where = pause.cwd ? ` · ${pause.cwd}` : "";
23891
+ return ` ${pause.sessionId} ${remaining} left (until ${at})${where}`;
23892
+ }
23893
+ function runPauseCommand(opts) {
23894
+ const now = opts.now ?? Date.now();
23895
+ const cwd = opts.cwd ?? process.cwd();
23896
+ if (opts.action === "status") {
23897
+ const active = listActivePauses(now);
23898
+ if (active.length === 0) {
23899
+ return { exitCode: 0, lines: ["Enforcement is active — nothing is paused."], affected: 0 };
23900
+ }
23901
+ return {
23902
+ exitCode: 0,
23903
+ lines: [
23904
+ `Enforcement paused for ${active.length} session${active.length === 1 ? "" : "s"}:`,
23905
+ ...active.map((p) => describe(p, now)),
23906
+ "",
23907
+ "Cloud-managed policies keep enforcing regardless.",
23908
+ "Resume early with: failproofai config --resume"
23909
+ ],
23910
+ affected: active.length
23911
+ };
23912
+ }
23913
+ if (opts.action === "resume") {
23914
+ if (opts.all) {
23915
+ const active = listActivePauses(now);
23916
+ let cleared2 = 0;
23917
+ for (const pause2 of active)
23918
+ if (clearPause(pause2.sessionId))
23919
+ cleared2++;
23920
+ return {
23921
+ exitCode: 0,
23922
+ lines: [cleared2 === 0 ? "Nothing was paused." : `Resumed enforcement for ${cleared2} session(s).`],
23923
+ affected: cleared2
23924
+ };
23925
+ }
23926
+ const sessionId2 = opts.sessionId ?? resolveSessionForCwd(cwd, undefined, now);
23927
+ if (!sessionId2) {
23928
+ return { exitCode: 0, lines: ["Nothing was paused for this directory."], affected: 0 };
23929
+ }
23930
+ const cleared = clearPause(sessionId2);
23931
+ return {
23932
+ exitCode: 0,
23933
+ lines: [cleared ? "Enforcement resumed." : "Nothing was paused for that session."],
23934
+ affected: cleared ? 1 : 0
23935
+ };
23936
+ }
23937
+ let durationMs;
23938
+ try {
23939
+ durationMs = parsePauseDuration(opts.duration, effectiveCeilingMs(cwd));
23940
+ } catch (err) {
23941
+ return { exitCode: 1, lines: [err instanceof Error ? err.message : String(err)], affected: 0 };
23942
+ }
23943
+ const sessionId = opts.sessionId ?? resolveSessionForCwd(cwd, undefined, now);
23944
+ if (!sessionId) {
23945
+ return {
23946
+ exitCode: 1,
23947
+ lines: [
23948
+ "No recent agent session found for this directory.",
23949
+ "",
23950
+ "A pause applies to one agent session, and the session id only appears once",
23951
+ "an agent has run at least one tool call here. Either run your agent first,",
23952
+ "or name the session explicitly:",
23953
+ "",
23954
+ " failproofai config --pause --session <id>",
23955
+ "",
23956
+ "Session ids are listed in the dashboard's activity view."
23957
+ ],
23958
+ affected: 0
23959
+ };
23960
+ }
23961
+ const existing = readActivePause(sessionId, now);
23962
+ const pause = writePause({ sessionId, durationMs, cwd, setBy: "cli", now });
23963
+ const until = new Date(pause.expiresAt).toLocaleTimeString();
23964
+ return {
23965
+ exitCode: 0,
23966
+ lines: [
23967
+ existing ? `Enforcement pause extended · ${formatDuration2(durationMs)} · resumes at ${until}` : `Enforcement paused · ${formatDuration2(durationMs)} · resumes at ${until}`,
23968
+ "",
23969
+ " Builtin, custom and convention policies are suspended for this session.",
23970
+ " Cloud-managed policies keep enforcing.",
23971
+ "",
23972
+ "It lifts on its own — no action needed. To end it early:",
23973
+ " failproofai config --resume"
23974
+ ],
23975
+ affected: 1
23976
+ };
23977
+ }
23978
+ var init_session_pause_cli = __esm(() => {
23979
+ init_session_pause();
23980
+ init_hooks_config();
23981
+ });
23982
+
23983
+ // src/hooks/manager.ts
23984
+ import { execSync as execSync7 } from "node:child_process";
23985
+ import { existsSync as existsSync27 } from "node:fs";
23986
+ import { resolve as resolve22, basename as basename6 } from "node:path";
23987
+ import { homedir as homedir37, platform as platform5, arch as arch4, release as release3, hostname as hostname4 } from "node:os";
23988
+ function getSettingsPath2(scope, cwd) {
23989
+ return claudeCode.getSettingsPath(scope, cwd);
23990
+ }
23991
+ function scopeLabel2(scope) {
23992
+ switch (scope) {
23993
+ case "user":
23994
+ return `~/.claude/settings.json`;
23995
+ case "project":
23996
+ return `{cwd}/.claude/settings.json`;
23997
+ case "local":
23998
+ return `{cwd}/.claude/settings.local.json`;
23999
+ }
24000
+ }
24001
+ function resolveFailproofaiBinary2() {
24002
+ const override = process.env.FAILPROOFAI_BINARY_OVERRIDE;
24003
+ if (override && override.trim())
24004
+ return override.trim();
24005
+ try {
24006
+ const cmd = process.platform === "win32" ? "where failproofai" : "which failproofai";
24007
+ const result = execSync7(cmd, { encoding: "utf8" }).trim();
24008
+ return result.split(`
24009
+ `)[0].trim();
24010
+ } catch {
24011
+ throw new CliError(`failproofai binary not found in PATH.
24012
+ ` + "Install it globally first: npm install -g failproofai");
24013
+ }
24014
+ }
24015
+ function validatePolicyNames2(names) {
24016
+ const invalid = names.filter((n) => !VALID_POLICY_NAMES2.has(n));
24017
+ if (invalid.length > 0) {
24018
+ const validList = [...VALID_POLICY_NAMES2].join(", ");
24019
+ throw new CliError(`Unknown policy name(s): ${invalid.join(", ")}
24020
+ ` + `Valid policies: ${validList}`);
24021
+ }
24022
+ }
24023
+ function deduplicateScopes2(scopes, cwd) {
24024
+ const seen = new Set;
24025
+ return scopes.filter((s) => {
24026
+ const p = getSettingsPath2(s, cwd);
24027
+ if (seen.has(p))
24028
+ return false;
24029
+ seen.add(p);
24030
+ return true;
24031
+ });
24032
+ }
24033
+ function hooksInstalledInSettings2(scope, cwd) {
24034
+ return claudeCode.hooksInstalledInSettings(scope, cwd);
24035
+ }
24036
+ async function installHooks2(policyNames, scope = "user", cwd, includeBeta = false, source, customPoliciesPath, removeCustomHooks = false, cli, options = {}) {
24037
+ const { replace = false, quiet = false } = options;
24038
+ if (!quiet) {
24039
+ return installHooksImpl2(policyNames, scope, cwd, includeBeta, source, customPoliciesPath, removeCustomHooks, cli, replace);
24040
+ }
24041
+ const origLog = console.log;
24042
+ console.log = () => {};
24043
+ try {
24044
+ return await installHooksImpl2(policyNames, scope, cwd, includeBeta, source, customPoliciesPath, removeCustomHooks, cli, replace);
24045
+ } finally {
24046
+ console.log = origLog;
24047
+ }
24048
+ }
24049
+ async function installHooksImpl2(policyNames, scope = "user", cwd, includeBeta = false, source, customPoliciesPath, removeCustomHooks = false, cli, replace = false) {
24050
+ if (policyNames !== undefined && policyNames.length > 0) {
24051
+ const nonAllNames = policyNames.filter((n) => n !== "all");
24052
+ if (nonAllNames.length > 0)
24053
+ validatePolicyNames2(nonAllNames);
24054
+ if (policyNames.includes("all") && nonAllNames.length > 0) {
24055
+ throw new CliError(`"all" cannot be combined with specific policy names.
24056
+ ` + `Use either: --install all or --install block-sudo sanitize-jwt ...`);
24057
+ }
24058
+ }
24059
+ const selectedClis = cli && cli.length > 0 ? [...new Set(cli)] : ["claude"];
24060
+ for (const cliId of selectedClis) {
24061
+ const integration = getIntegration(cliId);
24062
+ if (!integration.scopes.includes(scope)) {
24063
+ try {
24064
+ await trackHookEvent2(getInstanceId2(), "scope_validation_failed", {
24065
+ cli: cliId,
24066
+ scope,
24067
+ supported_scopes: integration.scopes
24068
+ });
24069
+ } catch {}
24070
+ throw new CliError(`Scope "${scope}" is not supported by ${integration.displayName}. ` + `Valid scopes: ${integration.scopes.join(", ")}`);
24071
+ }
24072
+ }
24073
+ const binaryPath = resolveFailproofaiBinary2();
24074
+ const previousConfig = readScopedHooksConfig(scope, cwd);
24075
+ const previousEnabled = new Set(previousConfig.enabledPolicies);
24076
+ let selectedPolicies;
24077
+ if (policyNames !== undefined) {
24078
+ let incoming;
24079
+ if (policyNames.length === 1 && policyNames[0] === "all") {
24080
+ incoming = BUILTIN_POLICIES.filter((p) => includeBeta || !p.beta).map((p) => p.name);
24081
+ } else {
24082
+ incoming = policyNames;
24083
+ }
24084
+ selectedPolicies = replace ? [...new Set(incoming)] : [...new Set([...previousConfig.enabledPolicies, ...incoming])];
24085
+ } else {
24086
+ const preSelected = previousConfig.enabledPolicies.length > 0 ? previousConfig.enabledPolicies : undefined;
24087
+ selectedPolicies = await promptPolicySelection(preSelected, { includeBeta });
24088
+ }
24089
+ const configToWrite = { ...previousConfig, enabledPolicies: selectedPolicies };
24090
+ if (removeCustomHooks) {
24091
+ delete configToWrite.customPoliciesPath;
24092
+ delete configToWrite.customPoliciesPaths;
24093
+ } else if (customPoliciesPath && (typeof customPoliciesPath === "string" || customPoliciesPath.length > 0)) {
24094
+ const incoming = (typeof customPoliciesPath === "string" ? [customPoliciesPath] : customPoliciesPath).map((path3) => resolve22(path3));
24095
+ const carried = (previousConfig.customPoliciesPaths ?? (previousConfig.customPoliciesPath ? [previousConfig.customPoliciesPath] : [])).map((path3) => resolve22(path3)).filter((path3) => {
24096
+ if (existsSync27(path3))
24097
+ return true;
24098
+ console.log(`Dropping custom policies path (file no longer exists): ${path3}`);
24099
+ return false;
24100
+ });
24101
+ configToWrite.customPoliciesPaths = replace ? [...new Set(incoming)] : [...new Set([...carried, ...incoming])];
24102
+ delete configToWrite.customPoliciesPath;
24103
+ for (const path3 of incoming) {
24104
+ let validatedHooks = [];
24105
+ try {
24106
+ validatedHooks = await loadCustomHooks(path3, { strict: true });
24107
+ } catch (err) {
24108
+ const msg = err instanceof Error ? err.message : String(err);
24109
+ try {
24110
+ await trackHookEvent2(getInstanceId2(), "custom_policy_validation_failed", {
24111
+ scope,
24112
+ error_type: /not found/i.test(msg) ? "file_not_found" : "load_error"
24113
+ });
24114
+ } catch {}
24115
+ console.error(`Error: ${msg}`);
24116
+ process.exit(1);
24117
+ }
24118
+ if (validatedHooks.length === 0) {
24119
+ try {
24120
+ await trackHookEvent2(getInstanceId2(), "custom_policy_validation_failed", {
24121
+ scope,
24122
+ error_type: "no_hooks_registered"
24123
+ });
24124
+ } catch {}
24125
+ console.error(`Error: no hooks registered in ${path3}. ` + `Make sure your file calls customPolicies.add(...) at least once.`);
24126
+ process.exit(1);
24127
+ }
24128
+ console.log(`
24129
+ Validated ${validatedHooks.length} custom hook(s) from ${path3}: ${validatedHooks.map((h) => h.name).join(", ")}`);
24130
+ }
24131
+ }
24132
+ writeScopedHooksConfig(configToWrite, scope, cwd);
24133
+ console.log(`
24134
+ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}
24135
+ `);
24136
+ if (removeCustomHooks) {
24137
+ console.log("Custom hooks path cleared.");
24138
+ } else if (configToWrite.customPoliciesPaths?.length || configToWrite.customPoliciesPath) {
24139
+ const paths = configToWrite.customPoliciesPaths ?? [configToWrite.customPoliciesPath];
24140
+ console.log(`Custom hooks paths: ${paths.join(", ")}`);
24141
+ }
24142
+ const writtenSettingsPaths = [];
24143
+ for (const cliId of selectedClis) {
24144
+ const integration = getIntegration(cliId);
24145
+ const settingsPaths = settingsPathsFor(integration, scope, cwd);
24146
+ try {
24147
+ for (const settingsPath of settingsPaths) {
24148
+ const settings = integration.readSettings(settingsPath);
24149
+ integration.writeHookEntries(settings, binaryPath, scope);
24150
+ integration.writeSettings(settingsPath, settings);
24151
+ writtenSettingsPaths.push({ cli: cliId, path: settingsPath });
24152
+ }
24153
+ } catch (err) {
24154
+ const errorType = err instanceof Error && /EACCES|EPERM/.test(err.message) ? "permission_denied" : err instanceof Error && /ENOENT|ENOTDIR/.test(err.message) ? "path_not_found" : "write_error";
23062
24155
  try {
23063
- runPrivileged("launchctl", ["unload", plistPath]);
24156
+ await trackHookEvent2(getInstanceId2(), "hook_write_failed", {
24157
+ cli: cliId,
24158
+ scope,
24159
+ error_type: errorType
24160
+ });
23064
24161
  } catch {}
23065
- writePrivilegedFile(plistPath, launchdPlistContents(binaryPath, logDir2, workerCmd));
23066
- runPrivileged("launchctl", ["load", "-w", plistPath]);
24162
+ throw err;
23067
24163
  }
23068
- } catch (err) {
23069
- const msg = err instanceof Error ? err.message : String(err);
23070
- hookLogWarn(`daemon service install failed: ${msg}`);
23071
- return { installed: false, reason: msg };
23072
24164
  }
23073
- if (!await waitForDaemonRunning()) {
23074
- const reason = `failproofaid was installed but did not reach a running state within ${SERVICE_START_TIMEOUT_MS}ms (status: ${daemonServiceStatus()})`;
23075
- hookLogWarn(`daemon service install failed: ${reason}`);
23076
- return { installed: false, reason };
24165
+ try {
24166
+ const newSet = new Set(selectedPolicies);
24167
+ const policiesAdded = selectedPolicies.filter((p) => !previousEnabled.has(p));
24168
+ const policiesRemoved = [...previousEnabled].filter((p) => !newSet.has(p));
24169
+ const distinctId = getInstanceId2();
24170
+ await trackHookEvent2(distinctId, "hooks_installed", {
24171
+ scope,
24172
+ cli: selectedClis,
24173
+ cli_count: selectedClis.length,
24174
+ policies: selectedPolicies,
24175
+ policy_count: selectedPolicies.length,
24176
+ policies_added: policiesAdded,
24177
+ policies_removed: policiesRemoved,
24178
+ ...source ? { source } : {},
24179
+ platform: platform5(),
24180
+ arch: arch4(),
24181
+ os_release: release3(),
24182
+ hostname_hash: hashToId2(hostname4()),
24183
+ has_custom_hooks_path: !!(configToWrite.customPoliciesPaths?.length || configToWrite.customPoliciesPath),
24184
+ has_policy_params: !!(configToWrite.policyParams && Object.keys(configToWrite.policyParams).length > 0),
24185
+ param_policy_names: configToWrite.policyParams ? Object.keys(configToWrite.policyParams) : [],
24186
+ command_format: scope === "project" ? "npx" : "absolute"
24187
+ });
24188
+ if (includeBeta) {
24189
+ const betaNames = new Set(BUILTIN_POLICIES.filter((p) => p.beta).map((p) => p.name));
24190
+ const installedBeta = selectedPolicies.filter((p) => betaNames.has(p));
24191
+ if (installedBeta.length > 0) {
24192
+ await trackHookEvent2(distinctId, "beta_policies_installed", {
24193
+ scope,
24194
+ cli: selectedClis,
24195
+ beta_count: installedBeta.length,
24196
+ beta_policy_names: installedBeta,
24197
+ ...source ? { source } : {}
24198
+ });
24199
+ }
24200
+ }
24201
+ } catch {}
24202
+ for (const { cli: cliId, path: path3 } of writtenSettingsPaths) {
24203
+ const integration = getIntegration(cliId);
24204
+ console.log(`Failproof AI hooks installed for ${integration.displayName} ` + `(${integration.eventTypes.length} event types, scope: ${scope}).`);
24205
+ console.log(`Settings: ${path3}`);
23077
24206
  }
23078
- return { installed: true };
23079
- }
23080
- async function waitForDaemonRunning() {
23081
- const deadline = Date.now() + SERVICE_START_TIMEOUT_MS;
23082
- for (;; ) {
23083
- if (daemonServiceStatus() === "running")
23084
- break;
23085
- if (Date.now() >= deadline)
23086
- return false;
23087
- await new Promise((r) => setTimeout(r, SERVICE_START_POLL_MS));
24207
+ if (scope === "project") {
24208
+ console.log(`Command: npx -y failproofai`);
24209
+ console.log(`
24210
+ This file can be committed to git — no machine-specific paths.`);
24211
+ } else {
24212
+ console.log(`Binary: ${binaryPath}`);
23088
24213
  }
23089
- await new Promise((r) => setTimeout(r, SERVICE_SETTLE_MS));
23090
- return daemonServiceStatus() === "running";
23091
- }
23092
- function daemonServiceStatus() {
23093
- if (!isDaemonSupportedPlatform())
23094
- return "unsupported-platform";
23095
- if (process.platform === "linux") {
23096
- if (!existsSync21(systemdUnitPath()))
23097
- return "not-installed";
24214
+ const otherScopes = deduplicateScopes2(HOOK_SCOPES, cwd).filter((s) => s !== scope);
24215
+ const duplicates = otherScopes.filter((s) => hooksInstalledInSettings2(s, cwd));
24216
+ if (duplicates.length > 0) {
24217
+ const scopeList = duplicates.map((s) => `${s} (${scopeLabel2(s)})`).join(", ");
24218
+ console.log();
24219
+ console.log(`\x1B[33mWarning: Failproof AI hooks are also installed at ${scopeList}.\x1B[0m`);
24220
+ console.log(`Having hooks in multiple scopes may cause duplicate policy evaluation.`);
24221
+ console.log(`Use \`failproofai policies --uninstall --scope ${duplicates[0]}\` to remove the other installation,`);
24222
+ console.log(`or \`failproofai policies\` to see all scopes.`);
23098
24223
  try {
23099
- const out = execFileSync4("systemctl", ["is-active", systemdUnitName()], {
23100
- stdio: ["ignore", "pipe", "ignore"],
23101
- timeout: SERVICE_CMD_TIMEOUT_MS
23102
- }).toString().trim();
23103
- return out === "active" ? "running" : "stopped";
23104
- } catch {
23105
- return "stopped";
23106
- }
23107
- }
23108
- const plistPath = launchdPlistPath();
23109
- if (!existsSync21(plistPath))
23110
- return "not-installed";
23111
- try {
23112
- const root = typeof process.getuid === "function" && process.getuid() === 0;
23113
- const args = ["print", `system/${launchdLabel()}`];
23114
- const out = root ? execFileSync4("launchctl", args, {
23115
- stdio: ["ignore", "pipe", "ignore"],
23116
- timeout: SERVICE_CMD_TIMEOUT_MS
23117
- }).toString() : execFileSync4("sudo", ["-n", "launchctl", ...args], {
23118
- stdio: ["ignore", "pipe", "ignore"],
23119
- timeout: SERVICE_CMD_TIMEOUT_MS
23120
- }).toString();
23121
- return /state\s*=\s*running/.test(out) ? "running" : "stopped";
23122
- } catch {
23123
- return "stopped";
24224
+ await trackHookEvent2(getInstanceId2(), "multi_scope_warning_shown", {
24225
+ new_scope: scope,
24226
+ existing_scopes: duplicates,
24227
+ cli: selectedClis
24228
+ });
24229
+ } catch {}
23124
24230
  }
23125
24231
  }
23126
- var SERVICE_CMD_TIMEOUT_MS = 1e4, SERVICE_START_TIMEOUT_MS = 5000, SERVICE_START_POLL_MS = 100, SERVICE_SETTLE_MS = 750, SUDO_PROMPT_TIMEOUT_MS = 120000;
23127
- var init_daemon_service = __esm(() => {
23128
- init_hook_logger();
24232
+ var VALID_POLICY_NAMES2;
24233
+ var init_manager2 = __esm(() => {
24234
+ init_types();
24235
+ init_integrations();
24236
+ init_install_prompt();
23129
24237
  init_hooks_config();
23130
- init_daemon_download();
24238
+ init_builtin_policies();
24239
+ init_custom_hooks_loader();
24240
+ init_hook_telemetry2();
24241
+ init_telemetry_id2();
24242
+ init_cli_error();
24243
+ init_hook_logger();
24244
+ VALID_POLICY_NAMES2 = new Set(BUILTIN_POLICIES.map((p) => p.name));
24245
+ });
24246
+
24247
+ // src/hooks/policy-presets.ts
24248
+ function resolvePreset(id) {
24249
+ const preset = POLICY_PRESETS.find((p) => p.id === id);
24250
+ if (!preset)
24251
+ return [];
24252
+ const cats = new Set(preset.categories);
24253
+ const fromCategories = BUILTIN_POLICIES.filter((p) => !p.beta && cats.has(p.category)).map((p) => p.name);
24254
+ const extras = (preset.extra ?? []).filter((name) => BUILTIN_POLICIES.some((p) => p.name === name && !p.beta));
24255
+ return [...new Set([...fromCategories, ...extras])];
24256
+ }
24257
+ function resolveEverything() {
24258
+ return BUILTIN_POLICIES.filter((p) => !p.beta).map((p) => p.name);
24259
+ }
24260
+ var POLICY_PRESETS;
24261
+ var init_policy_presets = __esm(() => {
24262
+ init_builtin_policies();
24263
+ POLICY_PRESETS = [
24264
+ {
24265
+ id: "secrets",
24266
+ label: "Secrets & data",
24267
+ description: "Redact secrets in tool output, block .env & secret-file writes, keep reads inside the repo",
24268
+ categories: ["Sanitize", "Environment"],
24269
+ extra: ["block-secrets-write"]
24270
+ },
24271
+ {
24272
+ id: "git",
24273
+ label: "Git safety",
24274
+ description: "Block force-push & pushes to main, warn on history-rewriting git ops",
24275
+ categories: ["Git"]
24276
+ },
24277
+ {
24278
+ id: "ship",
24279
+ label: "Ship discipline",
24280
+ description: "Don't let the agent finish until changes are committed, pushed, PR'd and CI is green",
24281
+ categories: ["Workflow"]
24282
+ },
24283
+ {
24284
+ id: "infra",
24285
+ label: "Cloud & infra",
24286
+ description: "Block kubectl / terraform / aws / gcloud / az / helm / gh pipeline commands",
24287
+ categories: ["Infra Commands"]
24288
+ }
24289
+ ];
23131
24290
  });
23132
24291
 
23133
24292
  // src/audit/cli.ts
@@ -23421,16 +24580,16 @@ __export(exports_configure_wizard, {
23421
24580
  buildPresetChoices: () => buildPresetChoices,
23422
24581
  buildAgentChoices: () => buildAgentChoices
23423
24582
  });
23424
- import { existsSync as existsSync22, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11, readFileSync as readFileSync17 } from "node:fs";
23425
- import { homedir as homedir33 } from "node:os";
23426
- import { resolve as resolve20, sep as sep5 } from "node:path";
24583
+ import { existsSync as existsSync28, mkdirSync as mkdirSync14, writeFileSync as writeFileSync12, readFileSync as readFileSync21 } from "node:fs";
24584
+ import { homedir as homedir38, hostname as hostname5 } from "node:os";
24585
+ import { resolve as resolve23, sep as sep5 } from "node:path";
23427
24586
  async function emit(event, props) {
23428
24587
  try {
23429
24588
  await trackHookEvent2(getInstanceId2(), event, props);
23430
24589
  } catch {}
23431
24590
  }
23432
24591
  function homeify(p) {
23433
- const home = homedir33();
24592
+ const home = homedir38();
23434
24593
  if (p === home)
23435
24594
  return "~";
23436
24595
  if (p.startsWith(home + sep5))
@@ -23544,8 +24703,8 @@ function setCustomPoliciesEnabled(scope, cwd, enabled) {
23544
24703
  const path3 = getConfigPathForScope(scope, cwd);
23545
24704
  let config = {};
23546
24705
  try {
23547
- if (existsSync22(path3))
23548
- config = JSON.parse(readFileSync17(path3, "utf8"));
24706
+ if (existsSync28(path3))
24707
+ config = JSON.parse(readFileSync21(path3, "utf8"));
23549
24708
  } catch {
23550
24709
  return;
23551
24710
  }
@@ -23554,7 +24713,7 @@ function setCustomPoliciesEnabled(scope, cwd, enabled) {
23554
24713
  else
23555
24714
  config.customPoliciesEnabled = false;
23556
24715
  try {
23557
- writeFileSync11(path3, JSON.stringify(config, null, 2) + `
24716
+ writeFileSync12(path3, JSON.stringify(config, null, 2) + `
23558
24717
  `, "utf8");
23559
24718
  } catch {}
23560
24719
  }
@@ -23585,8 +24744,8 @@ function describeCustomPolicies(cwd) {
23585
24744
  const scopes = [];
23586
24745
  let fileCount = 0;
23587
24746
  const dirs = [
23588
- { dir: resolve20(cwd, ".failproofai", "policies"), label: "project" },
23589
- { dir: resolve20(homedir33(), ".failproofai", "policies"), label: "global" }
24747
+ { dir: resolve23(cwd, ".failproofai", "policies"), label: "project" },
24748
+ { dir: resolve23(homedir38(), ".failproofai", "policies"), label: "global" }
23590
24749
  ];
23591
24750
  for (const { dir, label } of dirs) {
23592
24751
  const found = discoverPolicyFiles(dir);
@@ -23596,7 +24755,7 @@ function describeCustomPolicies(cwd) {
23596
24755
  fileCount += found.length;
23597
24756
  }
23598
24757
  for (const name of findSkippedPolicyFiles(dir)) {
23599
- warnings.push(`! ${homeify(resolve20(dir, name))} is NOT loaded — rename to ` + `${name.replace(/\.(js|mjs|ts)$/, "-policies.$1")}`);
24758
+ warnings.push(`! ${homeify(resolve23(dir, name))} is NOT loaded — rename to ` + `${name.replace(/\.(js|mjs|ts)$/, "-policies.$1")}`);
23600
24759
  }
23601
24760
  }
23602
24761
  return { active, warnings, fileCount, scopes };
@@ -23608,7 +24767,7 @@ function reviewLines(state) {
23608
24767
  const assistantNames = clis.map((c3) => getIntegration(c3).displayName);
23609
24768
  lines.push(` Where : ${where}`);
23610
24769
  lines.push(` Assistants : ${assistantNames.length ? summarize(assistantNames, "assistants") : "(none)"}`);
23611
- lines.push(` Policies : ${policies.length} enabled`);
24770
+ lines.push(policies.length === 0 ? " Policies : none enabled (add later: failproofai policies --install)" : ` Policies : ${policies.length} enabled`);
23612
24771
  if (installDaemon && isDaemonSupportedPlatform()) {
23613
24772
  lines.push(` Daemon : failproofaid, installed as a system service running as you`);
23614
24773
  }
@@ -23635,15 +24794,15 @@ function reviewLines(state) {
23635
24794
  return lines;
23636
24795
  }
23637
24796
  function firstRunMarkerPath() {
23638
- return resolve20(homedir33(), ".failproofai", ".launcher-configured");
24797
+ return resolve23(homedir38(), ".failproofai", ".launcher-configured");
23639
24798
  }
23640
24799
  function hasSeenLauncher() {
23641
- return existsSync22(firstRunMarkerPath());
24800
+ return existsSync28(firstRunMarkerPath());
23642
24801
  }
23643
24802
  function markLauncherSeen() {
23644
24803
  try {
23645
- mkdirSync12(resolve20(homedir33(), ".failproofai"), { recursive: true });
23646
- writeFileSync11(firstRunMarkerPath(), "1", "utf8");
24804
+ mkdirSync14(resolve23(homedir38(), ".failproofai"), { recursive: true });
24805
+ writeFileSync12(firstRunMarkerPath(), "1", "utf8");
23647
24806
  } catch {}
23648
24807
  }
23649
24808
  function anyHooksInstalledGlobally() {
@@ -23781,9 +24940,8 @@ Installing the failproofaid service needs root. Your password stays with sudo.
23781
24940
  const presets = await multiSelect({
23782
24941
  message: "What should we guard against?",
23783
24942
  choices: presetChoices,
23784
- minSelected: 1,
23785
24943
  summaryNoun: "bundles",
23786
- hint: "space toggles · combine presets · ↵ confirm",
24944
+ hint: "space toggles · combine presets · ↵ confirm · none is fine",
23787
24945
  stdin,
23788
24946
  stdout
23789
24947
  });
@@ -23791,6 +24949,138 @@ Installing the failproofaid service needs root. Your password stays with sudo.
23791
24949
  return cancel();
23792
24950
  const policies = resolvePresetSelection(presets);
23793
24951
  const customEnabled = hasCustomFiles ? presets.includes(CUSTOM) : undefined;
24952
+ let collector = null;
24953
+ if (daemonWanted) {
24954
+ const send = await selectOne({
24955
+ message: "Send session and hook data to AgentEye?",
24956
+ choices: [
24957
+ {
24958
+ label: "Not now",
24959
+ value: "no",
24960
+ hint: "nothing leaves this machine"
24961
+ },
24962
+ {
24963
+ label: "Yes — connect to a dashboard",
24964
+ value: "yes",
24965
+ hint: "needs an events:add API key"
24966
+ }
24967
+ ],
24968
+ stdin,
24969
+ stdout
24970
+ });
24971
+ if (send === null)
24972
+ return cancel();
24973
+ if (send === "yes") {
24974
+ const existing = readCloudCredentials();
24975
+ let url = null;
24976
+ let key = null;
24977
+ if (existing) {
24978
+ const reuse = await selectOne({
24979
+ message: `Use this machine's existing connection to ${existing.url}?`,
24980
+ choices: [
24981
+ {
24982
+ label: `Yes — reuse it`,
24983
+ value: "reuse",
24984
+ hint: `as ${existing.machineId}, same token`
24985
+ },
24986
+ { label: "No — use a different endpoint and key", value: "other", hint: "" }
24987
+ ],
24988
+ stdin,
24989
+ stdout
24990
+ });
24991
+ if (reuse === null)
24992
+ return cancel();
24993
+ if (reuse === "reuse") {
24994
+ url = ingestUrlFor(existing.url);
24995
+ key = existing.token;
24996
+ }
24997
+ }
24998
+ if (url === null) {
24999
+ const entered = await promptText({
25000
+ message: "Failproof Cloud URL",
25001
+ defaultValue: cloudBaseFor(DEFAULT_INGEST_URL),
25002
+ hint: "Enter for the hosted endpoint",
25003
+ validate: (v) => /^https?:\/\//.test(v) ? null : "must be an http(s) URL",
25004
+ stdin,
25005
+ stdout
25006
+ });
25007
+ if (entered === null)
25008
+ return cancel();
25009
+ url = ingestUrlFor(cloudBaseFor(entered));
25010
+ }
25011
+ if (key === null) {
25012
+ key = await promptText({
25013
+ message: "API key",
25014
+ hint: "needs events:add",
25015
+ mask: true,
25016
+ validate: (v) => v.length >= 8 ? null : "that looks too short to be a key",
25017
+ stdin,
25018
+ stdout
25019
+ });
25020
+ if (key === null)
25021
+ return cancel();
25022
+ }
25023
+ stdout.write(`
25024
+ Checking the key… `);
25025
+ const check = await validateIngestKey({ url, key });
25026
+ if (!check.ok) {
25027
+ stdout.write(`
25028
+ That did not work: ${check.reason}
25029
+ `);
25030
+ const retry = await selectOne({
25031
+ message: "Carry on without sending data?",
25032
+ choices: [
25033
+ { label: "Yes, skip it", value: "skip", hint: "re-run failproofai config later" },
25034
+ { label: "Save it anyway", value: "anyway", hint: "if you know the server is just down" }
25035
+ ],
25036
+ stdin,
25037
+ stdout
25038
+ });
25039
+ if (retry === null)
25040
+ return cancel();
25041
+ if (retry === "skip") {
25042
+ stdout.write(`Skipping data collection. Everything else still applies.
25043
+
25044
+ `);
25045
+ } else {
25046
+ collector = { cred: { url, key }, sessions: false };
25047
+ }
25048
+ } else {
25049
+ stdout.write(`looks good.
25050
+ `);
25051
+ const sessions = await selectOne({
25052
+ message: "What should it send?",
25053
+ choices: [
25054
+ {
25055
+ label: "Policy decisions only",
25056
+ value: "hooks",
25057
+ hint: "which hooks fired and what they decided · no file contents"
25058
+ },
25059
+ {
25060
+ label: "Decisions and full session transcripts",
25061
+ value: "both",
25062
+ hint: "includes prompts, file contents and command output"
25063
+ }
25064
+ ],
25065
+ stdin,
25066
+ stdout
25067
+ });
25068
+ if (sessions === null)
25069
+ return cancel();
25070
+ collector = { cred: { url, key }, sessions: sessions === "both" };
25071
+ if (!readCloudCredentials()) {
25072
+ const base = cloudBaseFor(url);
25073
+ const machineId = hostname5();
25074
+ const enrol = await verifyCloudCredentials({ url: base, machineId, token: key });
25075
+ if (enrol.ok) {
25076
+ writeCloudCredentials({ url: base, machineId, token: key });
25077
+ stdout.write(`Also connected for cloud-managed policy (${enrol.policyCount} assigned, generation ${enrol.generation}).
25078
+ `);
25079
+ }
25080
+ }
25081
+ }
25082
+ }
25083
+ }
23794
25084
  const decision = await selectOne({
23795
25085
  message: "Ready to apply?",
23796
25086
  body: reviewLines({ scope, clis, policies, cwd, customEnabled, installDaemon: daemonWanted }),
@@ -23827,6 +25117,22 @@ Installing the failproofaid service needs root. Your password stays with sudo.
23827
25117
  platform: process.platform
23828
25118
  });
23829
25119
  }
25120
+ if (collector && daemonInstalled) {
25121
+ try {
25122
+ writeIngestCredential(collector.cred);
25123
+ writeCollectorSettings({
25124
+ sessions: collector.sessions,
25125
+ hooks: true,
25126
+ hooksVerbosity: "decisions",
25127
+ redact: "minimal"
25128
+ });
25129
+ emit("configure_collector", { sessions: collector.sessions });
25130
+ } catch (err) {
25131
+ hookLogWarn(`collector configuration was not written: ${err instanceof Error ? err.message : String(err)}`);
25132
+ }
25133
+ } else if (collector && !daemonInstalled) {
25134
+ hookLogWarn("skipped collector configuration because the daemon was not installed");
25135
+ }
23830
25136
  await applied;
23831
25137
  markLauncherSeen();
23832
25138
  const customNote = customEnabled === true ? " + your custom policies" : customEnabled === false ? " · custom policies DISABLED" : "";
@@ -23838,6 +25144,7 @@ Installing the failproofaid service needs root. Your password stays with sudo.
23838
25144
  var EVERYTHING = "__everything__", ALL_CLIS = "__all_clis__", CUSTOM = "__custom__", DIM_NOTE = "(auto-loaded)";
23839
25145
  var init_configure_wizard = __esm(() => {
23840
25146
  init_tui();
25147
+ init_collector_config();
23841
25148
  init_integrations();
23842
25149
  init_types();
23843
25150
  init_manager2();
@@ -23848,6 +25155,8 @@ var init_configure_wizard = __esm(() => {
23848
25155
  init_telemetry_id2();
23849
25156
  init_daemon_service();
23850
25157
  init_hook_logger();
25158
+ init_cloud_enrollment();
25159
+ init_cloud_connection();
23851
25160
  });
23852
25161
 
23853
25162
  // scripts/launch.ts
@@ -23856,8 +25165,8 @@ __export(exports_launch2, {
23856
25165
  launch: () => launch2
23857
25166
  });
23858
25167
  import { spawn as spawn3 } from "child_process";
23859
- import { realpathSync as realpathSync3, existsSync as existsSync23 } from "node:fs";
23860
- import { resolve as resolve21, dirname as dirname10 } from "node:path";
25168
+ import { realpathSync as realpathSync4, existsSync as existsSync29 } from "node:fs";
25169
+ import { resolve as resolve24, dirname as dirname10 } from "node:path";
23861
25170
  import { fileURLToPath as fileURLToPath3 } from "node:url";
23862
25171
  import { createInterface as createInterface3 } from "node:readline";
23863
25172
  function launch2(mode) {
@@ -23872,9 +25181,9 @@ function launch2(mode) {
23872
25181
  process.env.PORT = port;
23873
25182
  process.env.HOSTNAME = "0.0.0.0";
23874
25183
  cmd = "node";
23875
- const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve21(dirname10(realpathSync3(fileURLToPath3(import.meta.url))), "..");
23876
- const serverJsPath = resolve21(packageRoot, ".next/standalone/server.js");
23877
- if (!existsSync23(serverJsPath)) {
25184
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve24(dirname10(realpathSync4(fileURLToPath3(import.meta.url))), "..");
25185
+ const serverJsPath = resolve24(packageRoot, ".next/standalone/server.js");
25186
+ if (!existsSync29(serverJsPath)) {
23878
25187
  let shadowMessage = null;
23879
25188
  try {
23880
25189
  const diag = diagnoseShadow({ selfPackageRoot: packageRoot, selfVersion: version2 });
@@ -23968,18 +25277,18 @@ var init_cli_error2 = __esm(() => {
23968
25277
  });
23969
25278
 
23970
25279
  // bin/failproofai.mjs
23971
- import { realpathSync as realpathSync4 } from "fs";
23972
- import { dirname as dirname11, resolve as resolve22 } from "path";
25280
+ import { realpathSync as realpathSync5 } from "fs";
25281
+ import { dirname as dirname11, resolve as resolve25 } from "path";
23973
25282
  import { fileURLToPath as fileURLToPath4 } from "url";
23974
25283
  // package.json
23975
- var version = "1.0.0-beta.2";
25284
+ var version = "1.0.0-beta.3";
23976
25285
 
23977
25286
  // bin/failproofai.mjs
23978
25287
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
23979
- process.env.FAILPROOFAI_PACKAGE_ROOT = resolve22(dirname11(realpathSync4(fileURLToPath4(import.meta.url))), "..");
25288
+ process.env.FAILPROOFAI_PACKAGE_ROOT = resolve25(dirname11(realpathSync5(fileURLToPath4(import.meta.url))), "..");
23980
25289
  }
23981
25290
  if (!process.env.FAILPROOFAI_DIST_PATH) {
23982
- process.env.FAILPROOFAI_DIST_PATH = resolve22(dirname11(realpathSync4(fileURLToPath4(import.meta.url))), "..", "dist");
25291
+ process.env.FAILPROOFAI_DIST_PATH = resolve25(dirname11(realpathSync5(fileURLToPath4(import.meta.url))), "..", "dist");
23983
25292
  }
23984
25293
  var args = process.argv.slice(2);
23985
25294
  if (args[0] === "p")
@@ -24516,11 +25825,127 @@ WHAT IT DOES
24516
25825
  3. Policies \u2014 presets (combine any), Everything, or a custom pick
24517
25826
  4. Review \u2014 confirms the exact files it will change, then applies
24518
25827
 
25828
+ FAILPROOF CLOUD
25829
+ failproofai config --connect <url> --token <key> [--machine-id <id>]
25830
+ Connect this machine to Failproof Cloud
25831
+ [--send-transcripts] also send full session transcripts
25832
+ failproofai config --disconnect Stop pulling policy and sending activity
25833
+ failproofai config --status Show connection and pause state
25834
+
25835
+ One connection, two capabilities: this machine PULLS centrally-managed
25836
+ policies and SENDS what its hooks decided, so the dashboard shows the fleet
25837
+ it is enforcing on. Both are checked against the server before anything is
25838
+ written, and reported separately \u2014 a key carrying policies:pull but not
25839
+ events:add connects for policy and says exactly why the dashboard is empty.
25840
+
25841
+ Tokens are stored owner-only in ~/.failproofai/, never in the service unit \u2014
25842
+ that file is world-readable. Connecting needs no sudo, and the machine id
25843
+ defaults to this host's name.
25844
+
25845
+ Session transcripts carry prompts, file contents and whatever was pasted into
25846
+ a terminal, so they are NEVER sent unless --send-transcripts is passed.
25847
+
25848
+ PAUSING ENFORCEMENT (one session, always time-boxed)
25849
+ failproofai config --pause Pause this directory's newest agent session (30m)
25850
+ failproofai config --pause 10m Pause for a given time (max 8h; s/m/h, bare = minutes)
25851
+ failproofai config --resume End the pause early
25852
+ failproofai config --status Show what is paused and when it lifts
25853
+ --session <id> Target a specific session
25854
+ --all With --resume, end every active pause
25855
+
25856
+ A pause suspends builtin, custom and convention policies for that session
25857
+ only, and always expires on its own. Cloud-managed policies keep enforcing.
25858
+
24519
25859
  Prefer flags? See \`failproofai policies --help\`.
24520
25860
  `.trimStart());
24521
25861
  process.exit(0);
24522
25862
  }
24523
25863
  lastSubcommand = "config";
25864
+ const connectIdx = args.indexOf("--connect");
25865
+ const wantsDisconnect = args.includes("--disconnect");
25866
+ if (connectIdx >= 0 || wantsDisconnect) {
25867
+ if (connectIdx >= 0 && wantsDisconnect) {
25868
+ throw new CliError3("--connect and --disconnect cannot be combined.");
25869
+ }
25870
+ const valueAfter = (flag) => {
25871
+ const i = args.indexOf(flag);
25872
+ if (i < 0)
25873
+ return;
25874
+ const v = args[i + 1];
25875
+ if (!v || v.startsWith("-"))
25876
+ throw new CliError3(`Missing value after ${flag}.`);
25877
+ return v;
25878
+ };
25879
+ let result2;
25880
+ if (wantsDisconnect) {
25881
+ const { runDisconnectCommand: runDisconnectCommand2 } = await Promise.resolve().then(() => (init_cloud_enrollment_cli(), exports_cloud_enrollment_cli));
25882
+ result2 = runDisconnectCommand2();
25883
+ } else {
25884
+ const { hostname: hostname6 } = await import("os");
25885
+ const { runConnectCommand: runConnectCommand2 } = await Promise.resolve().then(() => (init_cloud_enrollment_cli(), exports_cloud_enrollment_cli));
25886
+ result2 = await runConnectCommand2({
25887
+ url: valueAfter("--connect"),
25888
+ token: valueAfter("--token"),
25889
+ machineId: valueAfter("--machine-id"),
25890
+ defaultMachineId: hostname6(),
25891
+ sessions: args.includes("--send-transcripts")
25892
+ });
25893
+ }
25894
+ for (const line of result2.lines) {
25895
+ if (result2.exitCode === 0)
25896
+ console.log(line);
25897
+ else
25898
+ console.error(line);
25899
+ }
25900
+ await track("cli_cloud_enrollment", {
25901
+ action: wantsDisconnect ? "disconnect" : "connect",
25902
+ ok: result2.exitCode === 0
25903
+ });
25904
+ await exitAfterFlush(result2.exitCode);
25905
+ return;
25906
+ }
25907
+ const pauseIdx = args.indexOf("--pause");
25908
+ const wantsResume = args.includes("--resume");
25909
+ const wantsStatus = args.includes("--status");
25910
+ if (pauseIdx >= 0 || wantsResume || wantsStatus) {
25911
+ const chosen = [pauseIdx >= 0 && "--pause", wantsResume && "--resume", wantsStatus && "--status"].filter(Boolean);
25912
+ if (chosen.length > 1) {
25913
+ throw new CliError3(`${chosen.join(" and ")} cannot be combined.`);
25914
+ }
25915
+ const sessionIdx = args.indexOf("--session");
25916
+ if (sessionIdx >= 0 && !args[sessionIdx + 1]) {
25917
+ throw new CliError3("Missing session id after --session.");
25918
+ }
25919
+ const next = pauseIdx >= 0 ? args[pauseIdx + 1] : undefined;
25920
+ const duration = next && !next.startsWith("-") ? next : undefined;
25921
+ const { runPauseCommand: runPauseCommand2 } = await Promise.resolve().then(() => (init_session_pause_cli(), exports_session_pause_cli));
25922
+ const result2 = runPauseCommand2({
25923
+ action: pauseIdx >= 0 ? "pause" : wantsResume ? "resume" : "status",
25924
+ duration,
25925
+ sessionId: sessionIdx >= 0 ? args[sessionIdx + 1] : undefined,
25926
+ all: args.includes("--all"),
25927
+ cwd: process.cwd()
25928
+ });
25929
+ if (wantsStatus) {
25930
+ const { connectionStatusLines: connectionStatusLines2 } = await Promise.resolve().then(() => (init_cloud_enrollment_cli(), exports_cloud_enrollment_cli));
25931
+ for (const line of connectionStatusLines2())
25932
+ console.log(line);
25933
+ console.log("");
25934
+ }
25935
+ for (const line of result2.lines) {
25936
+ if (result2.exitCode === 0)
25937
+ console.log(line);
25938
+ else
25939
+ console.error(line);
25940
+ }
25941
+ await track("cli_pause_invoked", {
25942
+ action: pauseIdx >= 0 ? "pause" : wantsResume ? "resume" : "status",
25943
+ ok: result2.exitCode === 0,
25944
+ affected: result2.affected
25945
+ });
25946
+ await exitAfterFlush(result2.exitCode);
25947
+ return;
25948
+ }
24524
25949
  const { runConfigureWizard: runConfigureWizard2 } = await Promise.resolve().then(() => (init_configure_wizard(), exports_configure_wizard));
24525
25950
  const result = await runConfigureWizard2();
24526
25951
  await track("cli_configure_invoked", {