failproofai 1.0.0-beta.7 → 1.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  36. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/index.html +1 -1
  39. package/.next/standalone/.next/server/app/index.rsc +14 -14
  40. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  42. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  43. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
  44. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  45. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  46. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
  49. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  62. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +3 -3
  68. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0c_qcgp._.js → [root-of-the-server]__0abann8._.js} +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f9901b._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tyf0tc._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1v185mo._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_04mekdy._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/_0r-dsre._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/_0se484m._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/_0uz7o9y._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/_1fahg9e._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/{_194yh9u._.js → _1t3vvw3._.js} +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/_1w8sola._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/_1w_6p9g._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  89. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  90. package/.next/standalone/.next/server/pages/404.html +1 -1
  91. package/.next/standalone/.next/server/pages/500.html +1 -1
  92. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  93. package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
  94. package/.next/standalone/.next/static/chunks/{2p2kz4aj1tooj.js → 0jibhdbjojuq5.js} +1 -1
  95. package/.next/standalone/.next/static/chunks/{36pc3_8fpb6ic.js → 0qubxfmqjdk8-.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{0m-m7wgtxdoav.js → 159mxiauw9a8_.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{11l6oh9cyb35a.js → 1xxzquhtvg72x.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/270jpx60b2kuj.js +1 -0
  99. package/.next/standalone/.next/static/chunks/{2aej0vi1fm87m.js → 2f295j8d9ta-h.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/{2bxld6-rym3ah.js → 2g7p545cy5458.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/{0gr76ps8_zm26.js → 2nsm1u-n43hfa.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/3940dugfmx1ai.js +6 -0
  103. package/.next/standalone/.next/static/chunks/{3t1wr_kx6brwc.js → 3dpc4c0l_hf32.js} +1 -1
  104. package/.next/standalone/package.json +5 -5
  105. package/.next/standalone/server.js +1 -1
  106. package/bin/failproofai.mjs +118 -1
  107. package/dist/cli.mjs +1199 -98
  108. package/dist/worker.mjs +77 -6
  109. package/package.json +5 -5
  110. package/scripts/repro-npm-install.sh +155 -0
  111. package/src/hooks/builtin-policies.ts +154 -2
  112. package/src/hooks/collector-config.ts +18 -2
  113. package/src/hooks/configure-wizard.ts +65 -34
  114. package/src/hooks/daemon-client.ts +35 -0
  115. package/src/hooks/daemon-service.ts +227 -22
  116. package/src/hooks/first-run-gate.ts +9 -1
  117. package/src/hooks/fp-home.ts +23 -3
  118. package/src/hooks/fp-reset.ts +137 -5
  119. package/src/hooks/uninstall-cli.ts +345 -0
  120. package/src/hooks/worker-server.ts +12 -2
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kbc9bs._.js +0 -3
  122. package/.next/standalone/.next/static/chunks/0945ybz3fsvba.js +0 -1
  123. package/.next/standalone/.next/static/chunks/166f8qnh03jqc.js +0 -6
  124. /package/.next/standalone/.next/static/{LUaLkKg40nSkHrIajexIk → biwmgAwO6vVswC0z-soDU}/_buildManifest.js +0 -0
  125. /package/.next/standalone/.next/static/{LUaLkKg40nSkHrIajexIk → biwmgAwO6vVswC0z-soDU}/_clientMiddlewareManifest.js +0 -0
  126. /package/.next/standalone/.next/static/{LUaLkKg40nSkHrIajexIk → biwmgAwO6vVswC0z-soDU}/_ssgManifest.js +0 -0
package/dist/worker.mjs CHANGED
@@ -990,11 +990,82 @@ function blockEnvFiles(ctx) {
990
990
  }
991
991
  return allow();
992
992
  }
993
+ function quoteAwareSegments(command) {
994
+ const segments = [];
995
+ let tokens = [];
996
+ let token = "";
997
+ let quote = null;
998
+ const endToken = () => {
999
+ if (token)
1000
+ tokens.push(token);
1001
+ token = "";
1002
+ };
1003
+ const endSegment = () => {
1004
+ endToken();
1005
+ if (tokens.length)
1006
+ segments.push(tokens);
1007
+ tokens = [];
1008
+ };
1009
+ for (let i = 0;i < command.length; i++) {
1010
+ const c = command[i];
1011
+ if (quote) {
1012
+ if (c === "\\" && quote === '"' && i + 1 < command.length) {
1013
+ token += command[++i];
1014
+ continue;
1015
+ }
1016
+ if (c === quote) {
1017
+ quote = null;
1018
+ continue;
1019
+ }
1020
+ token += c;
1021
+ continue;
1022
+ }
1023
+ if (c === "\\" && i + 1 < command.length) {
1024
+ token += command[++i];
1025
+ continue;
1026
+ }
1027
+ if (c === '"' || c === "'") {
1028
+ quote = c;
1029
+ continue;
1030
+ }
1031
+ if (/\s/.test(c)) {
1032
+ endToken();
1033
+ continue;
1034
+ }
1035
+ if (/[;&|\n\r(){}`]/.test(c)) {
1036
+ endSegment();
1037
+ continue;
1038
+ }
1039
+ token += c;
1040
+ }
1041
+ endSegment();
1042
+ return segments;
1043
+ }
1044
+ function namesElevation(command, depth = 0) {
1045
+ for (const tokens of quoteAwareSegments(command)) {
1046
+ let i = 0;
1047
+ while (i < tokens.length) {
1048
+ const token = tokens[i];
1049
+ const base = token.slice(token.lastIndexOf("/") + 1);
1050
+ if (base === "sudo" || base === "doas")
1051
+ return true;
1052
+ const isSkippable = ENV_ASSIGNMENT_RE.test(token) || token.startsWith("-") || token.startsWith(">") || token.startsWith("<") || RUNNER_OPERAND_RE.test(token) || COMMAND_PREFIX_TOKENS.has(base);
1053
+ if (!isSkippable)
1054
+ break;
1055
+ if (EVAL_FLAG_RE.test(token) && depth < 3 && i + 1 < tokens.length) {
1056
+ if (namesElevation(tokens[i + 1], depth + 1))
1057
+ return true;
1058
+ }
1059
+ i++;
1060
+ }
1061
+ }
1062
+ return false;
1063
+ }
993
1064
  function blockSudo(ctx) {
994
1065
  if (ctx.toolName !== "Bash")
995
1066
  return allow();
996
1067
  const cmd = getCommand(ctx).trimStart();
997
- if (SUDO_RE.test(cmd) || cmd.startsWith("sudo ")) {
1068
+ if (namesElevation(cmd)) {
998
1069
  const allowPatterns = ctx.params?.allowPatterns ?? [];
999
1070
  if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
1000
1071
  return allow();
@@ -1754,7 +1825,7 @@ function registerBuiltinPolicies(enabledNames) {
1754
1825
  }
1755
1826
  }
1756
1827
  }
1757
- 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, SEGMENT_SEPARATORS, COMMAND_PREFIX_TOKENS, SELF_BINARY_TOKEN_RE, CONFIG_SUBCOMMAND_RE, PAUSE_FLAG_RE, ENV_ASSIGNMENT_RE, RUNNER_OPERAND_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;
1828
+ 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, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, SEGMENT_SEPARATORS, COMMAND_PREFIX_TOKENS, SELF_BINARY_TOKEN_RE, CONFIG_SUBCOMMAND_RE, PAUSE_FLAG_RE, ENV_ASSIGNMENT_RE, RUNNER_OPERAND_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, EVAL_FLAG_RE, 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;
1758
1829
  var init_builtin_policies = __esm(() => {
1759
1830
  init_hook_logger();
1760
1831
  isClaudeInternalPath = isAgentInternalPath;
@@ -1791,7 +1862,6 @@ var init_builtin_policies = __esm(() => {
1791
1862
  CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
1792
1863
  ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
1793
1864
  ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
1794
- SUDO_RE = /(?:^|;|&&|\|\|)\s*sudo\s/;
1795
1865
  PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
1796
1866
  RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
1797
1867
  CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
@@ -1877,6 +1947,7 @@ var init_builtin_policies = __esm(() => {
1877
1947
  HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
1878
1948
  GH_PIPELINE_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gh\s+(?:workflow\s+(?:run|enable|disable)|run\s+(?:rerun|cancel)|pr\s+merge|release\s+(?:create|delete)|cache\s+delete|secret\s+(?:set|delete))\b/;
1879
1949
  gitBranchCache = new Map;
1950
+ EVAL_FLAG_RE = /^-{1,2}c$/;
1880
1951
  HOME_PREFIX_RE = /^(?:~[A-Za-z0-9_.-]*|\$HOME|\$\{HOME\})(?=$|\/)/;
1881
1952
  RM_CMD_RE = /^(?:\/\S*\/)?rm$/;
1882
1953
  FIND_CMD_RE = /^(?:\/\S*\/)?find$/;
@@ -2434,7 +2505,7 @@ var init_builtin_policies = __esm(() => {
2434
2505
  });
2435
2506
 
2436
2507
  // package.json
2437
- var version = "1.0.0-beta.7";
2508
+ var version = "1.0.0-beta.9";
2438
2509
  var init_package = () => {};
2439
2510
 
2440
2511
  // src/posthog-key.ts
@@ -9993,7 +10064,7 @@ function isWorkerHookRequest(msg) {
9993
10064
  if (!msg || typeof msg !== "object")
9994
10065
  return false;
9995
10066
  const m = msg;
9996
- return m.type === "hook" && typeof m.hookEvent === "string" && typeof m.cli === "string" && typeof m.stdin === "string" && (m.cwd === undefined || typeof m.cwd === "string");
10067
+ return m.type === "hook" && typeof m.hookEvent === "string" && typeof m.cli === "string" && typeof m.stdin === "string" && (m.cwd === undefined || m.cwd === null || typeof m.cwd === "string");
9997
10068
  }
9998
10069
  function encodeFrame(value) {
9999
10070
  const body = Buffer.from(JSON.stringify(value), "utf8");
@@ -10052,7 +10123,7 @@ function handleConnection(socket) {
10052
10123
  try {
10053
10124
  const result = await evaluateHookEvent(request.hookEvent, request.cli, request.stdin, {
10054
10125
  awaitTelemetryFlush: false,
10055
- fallbackCwd: request.cwd
10126
+ fallbackCwd: request.cwd ?? undefined
10056
10127
  });
10057
10128
  socket.write(encodeFrame({
10058
10129
  type: "hookResult",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.0-beta.7",
3
+ "version": "1.0.0-beta.9",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -114,9 +114,9 @@
114
114
  "sharp": "0.35.0"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@failproofai/failproofaid-linux-x64": "1.0.0-beta.7",
118
- "@failproofai/failproofaid-linux-arm64": "1.0.0-beta.7",
119
- "@failproofai/failproofaid-darwin-x64": "1.0.0-beta.7",
120
- "@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.7"
117
+ "@failproofai/failproofaid-linux-x64": "1.0.0-beta.9",
118
+ "@failproofai/failproofaid-linux-arm64": "1.0.0-beta.9",
119
+ "@failproofai/failproofaid-darwin-x64": "1.0.0-beta.9",
120
+ "@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.9"
121
121
  }
122
122
  }
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env bash
2
+ # ─────────────────────────────────────────────────────────────────────────────
3
+ # Reproduce a REAL user install, in the shape that actually breaks.
4
+ #
5
+ # sudo npm i -g failproofai (global prefix owned by root)
6
+ # then run the CLI as a NORMAL user
7
+ #
8
+ # That split is the whole point. A single-user laptop where npm's prefix is
9
+ # owned by the person running the hooks hides an entire class of bug — it is
10
+ # how the root-owned policy-shim fail-open shipped: every hook run by a non-root
11
+ # user hit EACCES, the policy never loaded, and the hook exited 0 ALLOWING the
12
+ # call, while builtins kept firing so the machine looked protected.
13
+ #
14
+ # systemd is real here (jrei/systemd-ubuntu), so `failproofai config` installs
15
+ # and starts the actual service rather than skipping that half.
16
+ #
17
+ # Usage:
18
+ # scripts/repro-npm-install.sh # build, pack, drop into a shell
19
+ # scripts/repro-npm-install.sh --published # install the last RELEASE instead
20
+ # ─────────────────────────────────────────────────────────────────────────────
21
+ set -uo pipefail
22
+
23
+ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
24
+ cd "$REPO"
25
+
26
+ IMAGE=jrei/systemd-ubuntu:24.04
27
+ NAME=fpai-repro
28
+ USER_IN_BOX=dev
29
+ PUBLISHED=0
30
+ [ "${1:-}" = "--published" ] && PUBLISHED=1
31
+
32
+ VERSION=$(node -p "require('$REPO/package.json').version")
33
+ say() { printf '\n\033[1m── %s\033[0m\n' "$1"; }
34
+
35
+ # ── 1. Build what a tarball would carry ──────────────────────────────────────
36
+ if [ "$PUBLISHED" -eq 0 ]; then
37
+ say "Building dist + daemon"
38
+ [ -f dist/cli.mjs ] || bun run build
39
+ [ -f target/release/failproofaid ] || cargo build --release --bin failproofaid
40
+
41
+ # `dist/cli.mjs` INLINES the version at build time. `npm pack --ignore-scripts`
42
+ # skips the `prepare` script that would rebuild it, so bumping package.json
43
+ # without rebuilding packs a bundle that reports the OLD version — and you
44
+ # spend an afternoon testing code you did not write. A real publish never hits
45
+ # this (publish.yml builds, and `prepare` runs on a normal pack); this is a
46
+ # trap of the fast path only, which is exactly why it is checked here.
47
+ if ! grep -q "$VERSION" dist/cli.mjs 2>/dev/null; then
48
+ say "dist is stale (built for $(grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+-beta\.[0-9]\+' dist/cli.mjs | head -1)) — rebuilding"
49
+ bun run build
50
+ fi
51
+
52
+ say "Packing $VERSION"
53
+ rm -f failproofai-*.tgz
54
+ npm pack --ignore-scripts >/dev/null 2>&1
55
+ TARBALL="$REPO/failproofai-$VERSION.tgz"
56
+ [ -f "$TARBALL" ] || { echo "pack failed"; exit 1; }
57
+ ls -la "$TARBALL" | awk '{printf " %s bytes %s\n", $5, $9}'
58
+
59
+ # Belt and braces: assert what is actually IN the tarball, not what we meant
60
+ # to put there.
61
+ PACKED=$(tar -xzOf "$TARBALL" package/dist/cli.mjs 2>/dev/null \
62
+ | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+-beta\.[0-9]\+' | head -1)
63
+ if [ "$PACKED" != "$VERSION" ]; then
64
+ echo " REFUSING: tarball reports '$PACKED' but package.json says '$VERSION'"
65
+ exit 1
66
+ fi
67
+ echo " bundled version verified: $PACKED"
68
+ fi
69
+
70
+ # ── 2. A container with a real init ──────────────────────────────────────────
71
+ say "Starting $IMAGE (real systemd)"
72
+ docker rm -f "$NAME" >/dev/null 2>&1
73
+ # --cgroupns=host + the two tmpfs mounts are REQUIRED on a cgroup v2 host
74
+ # (anything modern). Without them systemd cannot set up its own hierarchy and
75
+ # the container exits 255 immediately, with an empty `docker logs` — which is
76
+ # a genuinely awful thing to debug, hence the comment.
77
+ docker run -d --name "$NAME" --privileged --cgroupns=host \
78
+ --network=host \
79
+ -v /sys/fs/cgroup:/sys/fs/cgroup:rw --tmpfs /run --tmpfs /run/lock \
80
+ ${TARBALL:+-v "$TARBALL":/pkg.tgz:ro} \
81
+ -v "$REPO/target/release/failproofaid":/opt/failproofaid:ro \
82
+ "$IMAGE" >/dev/null
83
+ # Wait for systemd to finish booting, or `systemctl` answers nothing useful.
84
+ until docker exec "$NAME" systemctl is-system-running 2>/dev/null | grep -qE 'running|degraded'; do
85
+ if ! docker ps --filter "name=$NAME" --format '{{.Names}}' | grep -q "$NAME"; then
86
+ echo " container died during boot — cgroup v2 flags wrong for this host?"
87
+ docker logs "$NAME" 2>&1 | tail -5
88
+ exit 1
89
+ fi
90
+ sleep 1
91
+ done
92
+ echo " systemd: $(docker exec "$NAME" systemctl is-system-running 2>/dev/null)"
93
+
94
+ # ── 3. Node, and a NON-ROOT user to run as ───────────────────────────────────
95
+ say "Installing Node + creating an unprivileged user"
96
+ docker exec "$NAME" bash -lc '
97
+ apt-get update -qq >/dev/null 2>&1
98
+ apt-get install -y -qq curl ca-certificates sudo >/dev/null 2>&1
99
+ curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/n.sh 2>/dev/null && bash /tmp/n.sh >/dev/null 2>&1
100
+ apt-get install -y -qq nodejs >/dev/null 2>&1
101
+ node --version
102
+ ' 2>&1 | sed 's/^/ node /'
103
+
104
+ docker exec "$NAME" bash -lc "
105
+ id $USER_IN_BOX >/dev/null 2>&1 || useradd -m -s /bin/bash $USER_IN_BOX
106
+ # Passwordless sudo: the daemon install needs root, and we are testing the
107
+ # INSTALL, not whether a human can type a password.
108
+ echo '$USER_IN_BOX ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/$USER_IN_BOX
109
+ chmod 440 /etc/sudoers.d/$USER_IN_BOX
110
+ "
111
+
112
+ # ── 4. Install globally AS ROOT — the shape that exposes ownership bugs ──────
113
+ say "npm install -g (as root)"
114
+ if [ "$PUBLISHED" -eq 1 ]; then
115
+ docker exec "$NAME" bash -lc "npm install -g failproofai@$VERSION 2>&1 | tail -4"
116
+ else
117
+ docker exec "$NAME" bash -lc "npm install -g /pkg.tgz 2>&1 | tail -4"
118
+ fi
119
+
120
+ say "Who owns the global install?"
121
+ docker exec "$NAME" bash -lc '
122
+ P=$(npm root -g)/failproofai
123
+ stat -c " %U:%G %a %n" "$P" 2>/dev/null
124
+ stat -c " %U:%G %a %n" "$P/dist" 2>/dev/null
125
+ '
126
+
127
+ # ── 5. Hand over, running as the unprivileged user ───────────────────────────
128
+ say "Ready"
129
+ cat <<EOF
130
+
131
+ The box is up as container "$NAME". Everything below runs as the
132
+ UNPRIVILEGED user, against a root-owned global install — the real shape.
133
+
134
+ Shell in:
135
+ docker exec -it -u $USER_IN_BOX $NAME bash -l
136
+
137
+ Then, inside:
138
+ failproofai --version
139
+ failproofai config # real systemd service install
140
+ systemctl status failproofaid@$USER_IN_BOX
141
+
142
+ The daemon binary is mounted at /opt/failproofaid. There is no GitHub
143
+ release for $VERSION, so point the CLI at it rather than letting it try
144
+ to download one:
145
+ export FAILPROOFAI_DAEMON_BINARY=/opt/failproofaid
146
+
147
+ Fire a hook exactly as an agent CLI would:
148
+ printf '{"session_id":"s","cwd":"'"\$PWD"'","hook_event_name":"PreToolUse",
149
+ "tool_name":"Bash","tool_input":{"command":"sudo rm -rf /"}}' \\
150
+ | failproofai --hook PreToolUse --cli claude
151
+
152
+ Tear down:
153
+ docker rm -f $NAME
154
+
155
+ EOF
@@ -164,7 +164,6 @@ const ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
164
164
  const ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
165
165
 
166
166
  // blockSudo
167
- const SUDO_RE = /(?:^|;|&&|\|\|)\s*sudo\s/;
168
167
  const PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
169
168
  const RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
170
169
 
@@ -749,10 +748,163 @@ function blockEnvFiles(ctx: PolicyContext): PolicyResult {
749
748
  return allow();
750
749
  }
751
750
 
751
+ /**
752
+ * True when a segment runs an elevation binary IN COMMAND POSITION.
753
+ *
754
+ * Anchored structurally rather than by token, for the reason `namesSelfPause`
755
+ * is: `SUDO_RE` required the literal token `sudo` at a command boundary, so
756
+ * **`/usr/bin/sudo rm -rf /` was ALLOWED** — a direct invocation, no
757
+ * obfuscation, one absolute path away from root on a `defaultEnabled` guard.
758
+ * Every other form the shell resolves before settling on the binary — prefix
759
+ * assignments, redirections, runners and their flags — is walked off first, and
760
+ * the comparison is on the BASENAME so a path cannot hide the name.
761
+ *
762
+ * `doas` is included because it is the same capability under a different name;
763
+ * a machine with `doas` installed and only `sudo` blocked is not blocked.
764
+ *
765
+ * This raises the bar; it does not close the class. `bash -c "sudo …"`, a
766
+ * variable (`S=sudo; $S …`) and base64-through-a-pipe all still reach root,
767
+ * because static inspection of one command string cannot follow them. Closing
768
+ * that properly needs enforcement below the shell, not a better regex.
769
+ */
770
+ /**
771
+ * Split a command into per-segment TOKEN LISTS the way a shell reads it.
772
+ *
773
+ * Distinct from `shellSegments` below, which returns raw segment STRINGS and is
774
+ * what the deletion policies want. Named apart deliberately: defining a second
775
+ * `shellSegments` here shadowed that one, silently broke every `rm -rf` check,
776
+ * and disabled this matcher too — caught only because `tsc` reports duplicate
777
+ * implementations.
778
+ *
779
+ * Segmentation happens on the RAW string and skips separators inside quotes,
780
+ * which is the whole point: stripping quoting FIRST turns `\|` — a literal pipe
781
+ * in a grep alternation — into a real separator, so `grep "a\|sudo b"` parses
782
+ * as a command called `sudo` and gets denied. That is a common enough pattern
783
+ * that over-blocking it is worse than the evasion it would catch.
784
+ *
785
+ * Each token is unquoted individually afterwards, which still defeats `\sudo`
786
+ * and `"sudo"` without ever letting quote removal change where the boundaries
787
+ * are. Quoted tokens are returned separately so a caller can decide whether
788
+ * their CONTENTS deserve a second look — see `EVAL_FLAG_RE`.
789
+ */
790
+ function quoteAwareSegments(command: string): Array<string[]> {
791
+ const segments: Array<string[]> = [];
792
+ let tokens: string[] = [];
793
+ let token = "";
794
+ let quote: '"' | "'" | null = null;
795
+ const endToken = () => {
796
+ if (token) tokens.push(token);
797
+ token = "";
798
+ };
799
+ const endSegment = () => {
800
+ endToken();
801
+ if (tokens.length) segments.push(tokens);
802
+ tokens = [];
803
+ };
804
+ for (let i = 0; i < command.length; i++) {
805
+ const c = command[i];
806
+ if (quote) {
807
+ // Inside single quotes a backslash is literal; inside double quotes it escapes.
808
+ if (c === "\\" && quote === '"' && i + 1 < command.length) {
809
+ token += command[++i];
810
+ continue;
811
+ }
812
+ if (c === quote) {
813
+ quote = null;
814
+ continue;
815
+ }
816
+ token += c;
817
+ continue;
818
+ }
819
+ if (c === "\\" && i + 1 < command.length) {
820
+ // An escaped character is literal — `\sudo` is still sudo, and `\|` is a
821
+ // pipe CHARACTER rather than a separator.
822
+ token += command[++i];
823
+ continue;
824
+ }
825
+ if (c === '"' || c === "'") {
826
+ quote = c;
827
+ continue;
828
+ }
829
+ if (/\s/.test(c)) {
830
+ endToken();
831
+ continue;
832
+ }
833
+ if (/[;&|\n\r(){}`]/.test(c)) {
834
+ endSegment();
835
+ continue;
836
+ }
837
+ token += c;
838
+ }
839
+ endSegment();
840
+ return segments;
841
+ }
842
+
843
+ /** `-c` and friends: the flags whose ARGUMENT is a command, not data. */
844
+ const EVAL_FLAG_RE = /^-{1,2}c$/;
845
+
846
+ /**
847
+ * True when a command runs an elevation binary IN COMMAND POSITION.
848
+ *
849
+ * Anchored structurally rather than by token, for the reason `namesSelfPause`
850
+ * is: `SUDO_RE` required the literal token `sudo` at a command boundary, so
851
+ * **`/usr/bin/sudo rm -rf /` was ALLOWED** — a direct invocation, no
852
+ * obfuscation, one absolute path away from root on a `defaultEnabled` guard.
853
+ * Every other form the shell resolves before settling on the binary — prefix
854
+ * assignments, redirections, runners and their flags — is walked off first, and
855
+ * the comparison is on the BASENAME so a path cannot hide the name.
856
+ *
857
+ * `doas` is included because it is the same capability under a different name;
858
+ * a machine with `doas` installed and only `sudo` blocked is not blocked.
859
+ *
860
+ * A quoted argument is re-examined ONLY when the command that owns it is a
861
+ * shell runner invoked with an eval flag (`bash -c "sudo …"`). That distinction
862
+ * is load-bearing: `bash -c "sudo x"` and `grep "a|sudo x"` are identical from
863
+ * the outside, and the only thing separating an execution from a search string
864
+ * is whether the receiving binary evaluates its argument.
865
+ *
866
+ * This raises the bar; it does not close the class. A variable (`S=sudo; $S …`),
867
+ * base64 through a pipe, and a wrapper script on disk all still reach root,
868
+ * because static inspection of one command string cannot follow them. Closing
869
+ * that properly needs enforcement below the shell, not a better matcher.
870
+ */
871
+ function namesElevation(command: string, depth = 0): boolean {
872
+ for (const tokens of quoteAwareSegments(command)) {
873
+ let i = 0;
874
+ while (i < tokens.length) {
875
+ const token = tokens[i];
876
+ const base = token.slice(token.lastIndexOf("/") + 1);
877
+ // `sudo`/`doas` are themselves in COMMAND_PREFIX_TOKENS (they legitimately
878
+ // stand in front of another binary), so they must be tested BEFORE the
879
+ // skip list or the walk would step straight over the thing being looked for.
880
+ if (base === "sudo" || base === "doas") return true;
881
+ const isSkippable =
882
+ ENV_ASSIGNMENT_RE.test(token) ||
883
+ token.startsWith("-") ||
884
+ token.startsWith(">") ||
885
+ token.startsWith("<") ||
886
+ RUNNER_OPERAND_RE.test(token) ||
887
+ COMMAND_PREFIX_TOKENS.has(base);
888
+ if (!isSkippable) break;
889
+ // `sh -c "…"` / `bash -c "…"`: the next token is a COMMAND. Bounded
890
+ // recursion so a nested `bash -c "bash -c …"` cannot spin.
891
+ if (EVAL_FLAG_RE.test(token) && depth < 3 && i + 1 < tokens.length) {
892
+ if (namesElevation(tokens[i + 1], depth + 1)) return true;
893
+ }
894
+ i++;
895
+ }
896
+ }
897
+ return false;
898
+ }
899
+
752
900
  function blockSudo(ctx: PolicyContext): PolicyResult {
753
901
  if (ctx.toolName !== "Bash") return allow();
754
902
  const cmd = getCommand(ctx).trimStart();
755
- if (SUDO_RE.test(cmd) || cmd.startsWith("sudo ")) {
903
+ // ONE pass. `shellSegments` unquotes each token individually, so `\sudo` and
904
+ // `"sudo"` are still caught — without a whole-string strip, which is what
905
+ // turned an escaped pipe in `grep "a\|sudo b"` into a segment separator and
906
+ // denied an ordinary search.
907
+ if (namesElevation(cmd)) {
756
908
  // Check allowPatterns — match against parsed tokens, not raw string
757
909
  const allowPatterns = ((ctx.params?.allowPatterns ?? []) as string[]);
758
910
  if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p))) return allow();
@@ -31,8 +31,24 @@ import { readHooksConfig, writeHooksConfig } from "./hooks-config";
31
31
  import { credentialsFile, failproofaiHome as layoutHome } from "./fp-home";
32
32
  import { readCredentials, writeCredentials, updateConfig } from "./fp-config";
33
33
 
34
- /** The hosted ingest endpoint — a COMPLETE endpoint, not a base to join onto. */
35
- export const DEFAULT_INGEST_URL = "https://server.befailproof.ai/v1/events";
34
+ /**
35
+ * The hosted ingest endpoint — a COMPLETE endpoint, not a base to join onto.
36
+ *
37
+ * The DASHBOARD hostname, not the API server's. On the hosted deployment a
38
+ * reverse proxy routes `/v1/*` and `/enforcement/v1/*` to the server and
39
+ * everything else to the Next.js app, so this path already reaches ingest —
40
+ * the server hostname is not needed to get there and does not need to be a
41
+ * public name for the product to work.
42
+ *
43
+ * One hostname is also what makes `--connect <origin>` mean one thing. Ingest,
44
+ * key introspection (`/v1/auth/introspect`) and cloud-managed policy
45
+ * (`/enforcement/v1/*`) all resolve against the same origin someone pastes,
46
+ * which is the origin they already have in their browser.
47
+ *
48
+ * MUST stay byte-identical to `DEFAULT_INGEST_URL` in
49
+ * `crates/fpai-collect/src/config.rs` — see that copy for why.
50
+ */
51
+ export const DEFAULT_INGEST_URL = "https://app.befailproof.ai/v1/events";
36
52
 
37
53
  export interface IngestCredential {
38
54
  url: string;
@@ -72,6 +72,7 @@ import {
72
72
  daemonVersionSkew,
73
73
  primeElevation,
74
74
  setDaemonConfigured,
75
+ probeDaemon,
75
76
  probeDaemonEndToEnd,
76
77
  uninstallDaemonService,
77
78
  } from "./daemon-service";
@@ -1020,39 +1021,60 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1020
1021
  }
1021
1022
 
1022
1023
  if (url === null) {
1023
- const entered = await promptText({
1024
- message: "Failproof Cloud URL",
1025
- defaultValue: cloudBaseFor(DEFAULT_INGEST_URL),
1026
- hint: "Enter for the hosted endpoint",
1027
- // The SAME guard `--connect` uses, not a looser lookalike. This was
1028
- // `/^https?:\/\//`, which accepts `http://cloud.example.com` — and
1029
- // the wizard is the documented primary path, so the interactive flow
1030
- // put the machine's bearer token on the wire in clear while the flag
1031
- // nobody uses refused to. `validateCloudUrl` permits http for
1032
- // loopback only, which is what the local walkthrough needs.
1033
- validate: (v) => {
1034
- const check = validateCloudUrl(cloudBaseFor(v));
1035
- return check.ok ? null : check.reason;
1036
- },
1037
- stdin,
1038
- stdout,
1039
- });
1040
- if (entered === null) return cancel();
1041
- // Asked for as a base and normalised, so this and `--connect` take the
1042
- // same thing. Someone pasting the older `/events` endpoint still works.
1043
- const validated = validateCloudUrl(cloudBaseFor(entered));
1044
- if (!validated.ok) {
1045
- // Unreachable while `validate` above rejects the same input; kept so
1046
- // the two can never drift into disagreeing silently.
1047
- stdout.write(`\n${validated.reason}\n`);
1048
- return cancel();
1024
+ // NOT asked for. There is exactly one right answer for everybody using
1025
+ // the hosted product, and asking made it look like a decision — which
1026
+ // is how a key ends up pasted into the URL field, and how someone
1027
+ // reasonably types the dashboard's own address and gets a 404 from a
1028
+ // web app that is not the ingest endpoint. Both are real, both happened
1029
+ // within ten minutes of each other, and neither is a mistake the person
1030
+ // making it can be expected to avoid: "Failproof Cloud URL" has no
1031
+ // knowable answer other than the default it was already showing.
1032
+ //
1033
+ // The two audiences that genuinely need a different endpoint keep an
1034
+ // explicit way to say so, and neither is an interactive prompt:
1035
+ //
1036
+ // • local development / self-hosting → FAILPROOFAI_CLOUD_URL, the
1037
+ // same variable the DAEMON already reads for cloud-managed policy
1038
+ // (crates/failproofaid/src/cloud_client.rs), so one export points
1039
+ // the whole machine at one place instead of the wizard and the
1040
+ // daemon disagreeing.
1041
+ // • scripted installs → `failproofai config --connect <url> --token`,
1042
+ // unchanged.
1043
+ //
1044
+ // The env value goes through the SAME `validateCloudUrl` a typed one
1045
+ // did — it is not a trusted back door. http stays loopback-only, so a
1046
+ // bearer token still cannot be exported onto the wire in clear by
1047
+ // setting a variable.
1048
+ const override = process.env.FAILPROOFAI_CLOUD_URL?.trim();
1049
+ if (override) {
1050
+ const validated = validateCloudUrl(cloudBaseFor(override));
1051
+ if (!validated.ok) {
1052
+ // Loud, not silent-fallback-to-hosted: someone who exported this
1053
+ // wants THAT endpoint, and quietly reporting a machine to the
1054
+ // hosted service instead is the one outcome they did not ask for.
1055
+ stdout.write(
1056
+ `\nFAILPROOFAI_CLOUD_URL is set to "${override}", which cannot be used: ` +
1057
+ `${validated.reason}\n`,
1058
+ );
1059
+ return cancel();
1060
+ }
1061
+ url = validated.url;
1062
+ // Named on screen, because an env var is invisible at the moment it
1063
+ // matters and a machine reporting somewhere unexpected is exactly the
1064
+ // thing nobody notices until they go looking for data that is not
1065
+ // there.
1066
+ stdout.write(`\nUsing ${url} (from FAILPROOFAI_CLOUD_URL).\n`);
1067
+ } else {
1068
+ url = cloudBaseFor(DEFAULT_INGEST_URL);
1049
1069
  }
1050
- url = validated.url;
1051
1070
  }
1052
1071
 
1053
1072
  if (token === null) {
1054
1073
  token = await promptText({
1055
- message: "API key",
1074
+ // The destination is in the question now that it is no longer a
1075
+ // question of its own. It is the only remaining place a person can
1076
+ // notice they are about to send a key somewhere they did not mean.
1077
+ message: `API key for ${new URL(url).host}`,
1056
1078
  hint: "needs events:add · policies:pull enables managed policy too",
1057
1079
  // Masked: setup is routinely run while screen-sharing, and a pasted
1058
1080
  // key would otherwise sit in the scrollback of every recording.
@@ -1189,16 +1211,25 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
1189
1211
  // Checked HERE rather than inside `installDaemonService`, which can only
1190
1212
  // honestly report on the service: keeping them apart is also what lets the
1191
1213
  // install mechanics be tested against a stub binary.
1192
- if (!(await probeDaemonEndToEnd())) {
1193
- hookLogWarn("failproofaid was installed and running but did not answer a policy evaluation");
1214
+ const probe = await probeDaemon();
1215
+ if (!probe.ok) {
1216
+ hookLogWarn(
1217
+ `failproofaid was installed and running but did not answer a policy evaluation (${probe.reason})`,
1218
+ );
1219
+ // Two different faults, two different things to go and look at. Saying
1220
+ // "your worker will not start" at a machine whose socket simply never
1221
+ // came up sends someone to inspect a process that is fine.
1194
1222
  stdout.write(
1195
- "\nfailproofaid started but cannot evaluate policies — its worker process\n" +
1196
- "could not be run. Setup stopped before changing anything, because a machine\n" +
1197
- "configured to require a daemon that cannot answer denies every tool call.\n\n" +
1223
+ (probe.reason === "worker"
1224
+ ? "\nfailproofaid started but cannot evaluate policies — it is listening, and\n" +
1225
+ "its worker process could not be run.\n"
1226
+ : "\nfailproofaid started but could not be reached on its socket.\n") +
1227
+ "Setup stopped before changing anything, because a machine configured to\n" +
1228
+ "require a daemon that cannot answer denies every tool call.\n\n" +
1198
1229
  ` Check it with: ${daemonStatusCommand() ?? "systemctl status failproofaid"}\n` +
1199
1230
  " Then re-run: failproofai config\n\n",
1200
1231
  );
1201
- void emit("configure_aborted", { reason: "daemon_not_answering" });
1232
+ void emit("configure_aborted", { reason: `daemon_not_answering_${probe.reason}` });
1202
1233
  outro("Nothing was changed.", { ok: false }, stdout);
1203
1234
  return { applied: false, abort: "daemon_failed" };
1204
1235
  }