failproofai 1.0.0-beta.7 → 1.0.0-beta.8
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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +3 -3
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ommekk._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0c_qcgp._.js → [root-of-the-server]__1tkyag1._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tyf0tc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1v185mo._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_04mekdy._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_194yh9u._.js → _0i4l3_6._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0r-dsre._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0se484m._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0uz7o9y._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1fahg9e._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1w8sola._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1w_6p9g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
- package/.next/standalone/.next/static/chunks/{2aej0vi1fm87m.js → 02vvywmww22zz.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3t1wr_kx6brwc.js → 0_-focvo4cuiv.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2bxld6-rym3ah.js → 1fwh-2un_l-eo.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2p2kz4aj1tooj.js → 1zrdsxe91qq6i.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0m-m7wgtxdoav.js → 2047vah_v4emu.js} +1 -1
- package/.next/standalone/.next/static/chunks/23hl78dzffun4.js +6 -0
- package/.next/standalone/.next/static/chunks/{11l6oh9cyb35a.js → 2bw0stk-s-1dd.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0gr76ps8_zm26.js → 2r_j5wrkbu5wm.js} +1 -1
- package/.next/standalone/.next/static/chunks/3m8onpqrjxzu5.js +1 -0
- package/.next/standalone/.next/static/chunks/{36pc3_8fpb6ic.js → 3yn1y6ckypqwp.js} +1 -1
- package/.next/standalone/package.json +5 -5
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +118 -1
- package/dist/cli.mjs +1166 -101
- package/dist/worker.mjs +77 -6
- package/package.json +5 -5
- package/scripts/repro-npm-install.sh +155 -0
- package/src/hooks/builtin-policies.ts +154 -2
- package/src/hooks/configure-wizard.ts +16 -6
- package/src/hooks/daemon-client.ts +35 -0
- package/src/hooks/daemon-service.ts +227 -22
- package/src/hooks/first-run-gate.ts +9 -1
- package/src/hooks/fp-reset.ts +25 -0
- package/src/hooks/uninstall-cli.ts +345 -0
- package/src/hooks/worker-server.ts +12 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kbc9bs._.js +0 -3
- package/.next/standalone/.next/static/chunks/0945ybz3fsvba.js +0 -1
- package/.next/standalone/.next/static/chunks/166f8qnh03jqc.js +0 -6
- /package/.next/standalone/.next/static/{LUaLkKg40nSkHrIajexIk → gOR8i03AeQm1VnIayDHVV}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{LUaLkKg40nSkHrIajexIk → gOR8i03AeQm1VnIayDHVV}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{LUaLkKg40nSkHrIajexIk → gOR8i03AeQm1VnIayDHVV}/_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 (
|
|
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,
|
|
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.
|
|
2508
|
+
var version = "1.0.0-beta.8";
|
|
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.
|
|
3
|
+
"version": "1.0.0-beta.8",
|
|
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.
|
|
118
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.0-beta.
|
|
119
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.0-beta.
|
|
120
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.
|
|
117
|
+
"@failproofai/failproofaid-linux-x64": "1.0.0-beta.8",
|
|
118
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.0-beta.8",
|
|
119
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.0-beta.8",
|
|
120
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.8"
|
|
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
|
-
|
|
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();
|
|
@@ -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";
|
|
@@ -1189,16 +1190,25 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1189
1190
|
// Checked HERE rather than inside `installDaemonService`, which can only
|
|
1190
1191
|
// honestly report on the service: keeping them apart is also what lets the
|
|
1191
1192
|
// install mechanics be tested against a stub binary.
|
|
1192
|
-
|
|
1193
|
-
|
|
1193
|
+
const probe = await probeDaemon();
|
|
1194
|
+
if (!probe.ok) {
|
|
1195
|
+
hookLogWarn(
|
|
1196
|
+
`failproofaid was installed and running but did not answer a policy evaluation (${probe.reason})`,
|
|
1197
|
+
);
|
|
1198
|
+
// Two different faults, two different things to go and look at. Saying
|
|
1199
|
+
// "your worker will not start" at a machine whose socket simply never
|
|
1200
|
+
// came up sends someone to inspect a process that is fine.
|
|
1194
1201
|
stdout.write(
|
|
1195
|
-
|
|
1196
|
-
"
|
|
1197
|
-
|
|
1202
|
+
(probe.reason === "worker"
|
|
1203
|
+
? "\nfailproofaid started but cannot evaluate policies — it is listening, and\n" +
|
|
1204
|
+
"its worker process could not be run.\n"
|
|
1205
|
+
: "\nfailproofaid started but could not be reached on its socket.\n") +
|
|
1206
|
+
"Setup stopped before changing anything, because a machine configured to\n" +
|
|
1207
|
+
"require a daemon that cannot answer denies every tool call.\n\n" +
|
|
1198
1208
|
` Check it with: ${daemonStatusCommand() ?? "systemctl status failproofaid"}\n` +
|
|
1199
1209
|
" Then re-run: failproofai config\n\n",
|
|
1200
1210
|
);
|
|
1201
|
-
void emit("configure_aborted", { reason:
|
|
1211
|
+
void emit("configure_aborted", { reason: `daemon_not_answering_${probe.reason}` });
|
|
1202
1212
|
outro("Nothing was changed.", { ok: false }, stdout);
|
|
1203
1213
|
return { applied: false, abort: "daemon_failed" };
|
|
1204
1214
|
}
|
|
@@ -107,6 +107,41 @@ export function daemonSocketPresent(): boolean {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Whether the daemon socket accepts a connection *right now*.
|
|
112
|
+
*
|
|
113
|
+
* Narrower than `daemonSocketPresent()` (a stale socket file outlives the
|
|
114
|
+
* process that made it) and narrower than `attemptDaemonHook`, which reports
|
|
115
|
+
* `"unreachable"` for BOTH a refused connection and a request that was accepted
|
|
116
|
+
* and then never answered. Those two have different causes and different
|
|
117
|
+
* remedies — a socket that is not up yet versus a worker that cannot run — and
|
|
118
|
+
* telling them apart is the whole point of this function.
|
|
119
|
+
*
|
|
120
|
+
* Connect budget only; nothing is sent. Used by the health probe, never on the
|
|
121
|
+
* hook path.
|
|
122
|
+
*/
|
|
123
|
+
export async function daemonAcceptsConnections(
|
|
124
|
+
timeoutMs = DAEMON_CONNECT_TIMEOUT_MS,
|
|
125
|
+
): Promise<boolean> {
|
|
126
|
+
if (process.platform === "win32") return false;
|
|
127
|
+
return new Promise<boolean>((resolve) => {
|
|
128
|
+
let settled = false;
|
|
129
|
+
const done = (v: boolean) => {
|
|
130
|
+
if (settled) return;
|
|
131
|
+
settled = true;
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
socket.removeAllListeners();
|
|
134
|
+
socket.destroy();
|
|
135
|
+
resolve(v);
|
|
136
|
+
};
|
|
137
|
+
const socket = createConnection({ path: socketPath() });
|
|
138
|
+
const timer = setTimeout(() => done(false), timeoutMs);
|
|
139
|
+
timer.unref?.();
|
|
140
|
+
socket.on("connect", () => done(true));
|
|
141
|
+
socket.on("error", () => done(false));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
110
145
|
function socketPath(): string {
|
|
111
146
|
if (process.env.FAILPROOFAI_DAEMON_SOCKET) return process.env.FAILPROOFAI_DAEMON_SOCKET;
|
|
112
147
|
return daemonSocketPath();
|