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.
- 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]__0c_qcgp._.js → [root-of-the-server]__0abann8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f9901b._.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]__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/_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/{_194yh9u._.js → _1t3vvw3._.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/{2p2kz4aj1tooj.js → 0jibhdbjojuq5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{36pc3_8fpb6ic.js → 0qubxfmqjdk8-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0m-m7wgtxdoav.js → 159mxiauw9a8_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{11l6oh9cyb35a.js → 1xxzquhtvg72x.js} +1 -1
- package/.next/standalone/.next/static/chunks/270jpx60b2kuj.js +1 -0
- package/.next/standalone/.next/static/chunks/{2aej0vi1fm87m.js → 2f295j8d9ta-h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2bxld6-rym3ah.js → 2g7p545cy5458.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0gr76ps8_zm26.js → 2nsm1u-n43hfa.js} +1 -1
- package/.next/standalone/.next/static/chunks/3940dugfmx1ai.js +6 -0
- package/.next/standalone/.next/static/chunks/{3t1wr_kx6brwc.js → 3dpc4c0l_hf32.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 +1199 -98
- 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/collector-config.ts +18 -2
- package/src/hooks/configure-wizard.ts +65 -34
- 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-home.ts +23 -3
- package/src/hooks/fp-reset.ts +137 -5
- 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 → biwmgAwO6vVswC0z-soDU}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{LUaLkKg40nSkHrIajexIk → biwmgAwO6vVswC0z-soDU}/_clientMiddlewareManifest.js +0 -0
- /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 (
|
|
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.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.
|
|
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.
|
|
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.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
|
-
|
|
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
|
-
/**
|
|
35
|
-
|
|
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
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1196
|
-
"
|
|
1197
|
-
|
|
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:
|
|
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
|
}
|