frely-cli 0.23.0 → 0.23.2
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/README.md +31 -137
- package/README.zh-CN.md +34 -135
- package/dist/agent-help.d.ts +48 -0
- package/dist/agent-help.js +7 -0
- package/dist/agent-help.js.map +1 -1
- package/dist/cloud.d.ts +28 -2
- package/dist/cloud.js +32 -3
- package/dist/cloud.js.map +1 -1
- package/dist/computer/command.js +9 -2
- package/dist/computer/command.js.map +1 -1
- package/dist/computer/ocu.d.ts +7 -0
- package/dist/computer/ocu.js +16 -0
- package/dist/computer/ocu.js.map +1 -1
- package/dist/computer/server.js +2 -2
- package/dist/computer/server.js.map +1 -1
- package/dist/computer/toolset.d.ts +9 -1
- package/dist/computer/toolset.js +19 -1
- package/dist/computer/toolset.js.map +1 -1
- package/dist/device/protocol.d.ts +2 -0
- package/dist/device/protocol.js +3 -1
- package/dist/device/protocol.js.map +1 -1
- package/dist/device/relay-client.js +8 -13
- package/dist/device/relay-client.js.map +1 -1
- package/dist/device/stream-coalesce.d.ts +18 -0
- package/dist/device/stream-coalesce.js +94 -0
- package/dist/device/stream-coalesce.js.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-command.js +6 -1
- package/dist/mcp-command.js.map +1 -1
- package/dist/runtime/git-config-guard.d.ts +51 -0
- package/dist/runtime/git-config-guard.js +257 -0
- package/dist/runtime/git-config-guard.js.map +1 -0
- package/dist/runtime/mcp.d.ts +2 -2
- package/dist/runtime/mcp.js +2 -2
- package/dist/runtime/mcp.js.map +1 -1
- package/dist/runtime/process-manager.js +3 -0
- package/dist/runtime/process-manager.js.map +1 -1
- package/dist/runtime/process-tree.d.ts +7 -1
- package/dist/runtime/process-tree.js +20 -1
- package/dist/runtime/process-tree.js.map +1 -1
- package/dist/runtime/sandbox-proxy.d.ts +39 -0
- package/dist/runtime/sandbox-proxy.js +134 -0
- package/dist/runtime/sandbox-proxy.js.map +1 -0
- package/dist/runtime/sandbox.d.ts +48 -4
- package/dist/runtime/sandbox.js +165 -17
- package/dist/runtime/sandbox.js.map +1 -1
- package/dist/runtime/ssh-proxy.d.ts +1 -0
- package/dist/runtime/ssh-proxy.js +12 -3
- package/dist/runtime/ssh-proxy.js.map +1 -1
- package/dist/runtime/tunnel.d.ts +30 -0
- package/dist/runtime/tunnel.js +56 -0
- package/dist/runtime/tunnel.js.map +1 -0
- package/dist/runtime/workspace.js +25 -6
- package/dist/runtime/workspace.js.map +1 -1
- package/dist/update/auto-update.d.ts +52 -0
- package/dist/update/auto-update.js +133 -0
- package/dist/update/auto-update.js.map +1 -0
- package/dist/update/update.d.ts +2 -2
- package/dist/update/update.js +2 -2
- package/dist/update/update.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-tree.js","sourceRoot":"","sources":["../../src/runtime/process-tree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAElC,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAEtC,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,KAAmB;IAC5D,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;IACtB,IAAI,CAAC,GAAG,IAAI,WAAW,CAAC,KAAK,CAAC;QAAE,OAAO;IACvC,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,EAAE,wBAAwB,CAAC,CAAC;IAClE,IAAI,SAAwB,CAAC;IAC7B,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,aAAa,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;QACjG,SAAS,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CACjD,QAAQ,EACR,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EACjC,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,wBAAwB,EAAE,EACxD,GAAG,EAAE,CAAC,OAAO,EAAE,CAChB,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,IAAI,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAAC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACpD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;gBAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACjH,CAAC;QACD,SAAS,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IACD,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,6GAA6G;AAC7G,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAElC,6GAA6G;AAC7G,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACtB;IAAkC;IAAyB;IAAhF,YAAqB,QAAuB,EAAW,MAAc,EAAW,MAAc,EAAE,IAAa;QAC3G,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,MAAM,IAAI,CAAC,MAAM,GAAG,oBAAoB,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrK,KAAK,CAAC,kCAAkC,QAAQ,IAAI,SAAS,IAAI,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAF3H,aAAQ,GAAR,QAAQ,CAAe;QAAW,WAAM,GAAN,MAAM,CAAQ;QAAW,WAAM,GAAN,MAAM,CAAQ;IAG9F,CAAC;CACF;
|
|
1
|
+
{"version":3,"file":"process-tree.js","sourceRoot":"","sources":["../../src/runtime/process-tree.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user's own HTTP(S) proxy (Clash and the like) as the upstream of the command sandbox's proxy.
|
|
3
|
+
*
|
|
4
|
+
* srt gives sandboxed commands its own proxy and dials the network from the CLI process. A user who reaches the internet only through
|
|
5
|
+
* a local proxy has that setting in their shell, which a background service never sees, so downloads inside the sandbox fail. The
|
|
6
|
+
* address is taken, first match wins, from: the value saved with `frely sandbox proxy <url>`, the proxy variables of this process,
|
|
7
|
+
* and on macOS the system proxy (`scutil --proxy`). The user's own proxy settings are only read, never changed.
|
|
8
|
+
*
|
|
9
|
+
* srt 0.0.77 (`network.parentProxy`) takes `{ http, https, noProxy }`, accepts http:// and https:// proxy URLs only (a bare host:port
|
|
10
|
+
* counts as http), always bypasses loopback destinations, and reads the value once, when the sandbox starts.
|
|
11
|
+
*/
|
|
12
|
+
export interface UpstreamProxy {
|
|
13
|
+
http?: string;
|
|
14
|
+
https?: string;
|
|
15
|
+
noProxy?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function sandboxSettingsPath(env?: NodeJS.ProcessEnv): string;
|
|
18
|
+
/** A proxy URL srt can use: http(s) scheme, or no scheme at all. Credentials in the URL are kept (srt sends them to the proxy). */
|
|
19
|
+
export declare function usableProxyUrl(value: string | undefined): string | undefined;
|
|
20
|
+
/** The value `frely sandbox proxy` saved, or undefined. */
|
|
21
|
+
export declare function readSavedProxy(path?: string): UpstreamProxy | undefined;
|
|
22
|
+
/** Save (or with `undefined` remove) the proxy to use for the sandbox. Takes effect when the MCP service restarts. */
|
|
23
|
+
export declare function saveProxy(proxy: {
|
|
24
|
+
url: string;
|
|
25
|
+
noProxy?: string;
|
|
26
|
+
} | undefined, path?: string): Promise<void>;
|
|
27
|
+
/** The proxy variables of a process, including ALL_PROXY when it is an http(s) proxy (srt reads only HTTP_PROXY / HTTPS_PROXY itself). */
|
|
28
|
+
export declare function proxyFromEnv(env: NodeJS.ProcessEnv): UpstreamProxy | undefined;
|
|
29
|
+
/** The HTTP/HTTPS proxy of the macOS network settings, from the text of `scutil --proxy`. SOCKS and auto-config (PAC) entries are ignored. */
|
|
30
|
+
export declare function parseScutilProxy(text: string): UpstreamProxy | undefined;
|
|
31
|
+
/** The upstream proxy for the sandbox: saved value, then this process's environment, then (macOS) the system proxy. */
|
|
32
|
+
export declare function resolveUpstreamProxy(options?: {
|
|
33
|
+
saved?: UpstreamProxy | undefined;
|
|
34
|
+
env?: NodeJS.ProcessEnv;
|
|
35
|
+
platform?: string;
|
|
36
|
+
system?: () => UpstreamProxy | undefined;
|
|
37
|
+
}): UpstreamProxy | undefined;
|
|
38
|
+
/** `frely sandbox proxy [<url> [--no-proxy <list>] | --clear]`: show, save or remove the proxy used by the command sandbox. */
|
|
39
|
+
export declare function runSandboxProxyCommand(args: readonly string[], write: (text: string) => void): Promise<void>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
const SETTINGS_FILE = "sandbox.json";
|
|
7
|
+
export function sandboxSettingsPath(env = process.env) {
|
|
8
|
+
return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "frely", SETTINGS_FILE);
|
|
9
|
+
}
|
|
10
|
+
/** A proxy URL srt can use: http(s) scheme, or no scheme at all. Credentials in the URL are kept (srt sends them to the proxy). */
|
|
11
|
+
export function usableProxyUrl(value) {
|
|
12
|
+
const text = value?.trim();
|
|
13
|
+
if (!text)
|
|
14
|
+
return undefined;
|
|
15
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//iu.test(text) ? text : `http://${text}`;
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(withScheme);
|
|
18
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.hostname ? text : undefined;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The value `frely sandbox proxy` saved, or undefined. */
|
|
25
|
+
export function readSavedProxy(path = sandboxSettingsPath()) {
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(readFileSync(path, "utf8")).proxy;
|
|
28
|
+
const url = typeof value?.url === "string" ? usableProxyUrl(value.url) : undefined;
|
|
29
|
+
if (!url)
|
|
30
|
+
return undefined;
|
|
31
|
+
return { http: url, https: url, ...(typeof value?.noProxy === "string" && value.noProxy.trim() ? { noProxy: value.noProxy.trim() } : {}) };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Save (or with `undefined` remove) the proxy to use for the sandbox. Takes effect when the MCP service restarts. */
|
|
38
|
+
export async function saveProxy(proxy, path = sandboxSettingsPath()) {
|
|
39
|
+
if (!proxy) {
|
|
40
|
+
await rm(path, { force: true });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const url = usableProxyUrl(proxy.url);
|
|
44
|
+
if (!url)
|
|
45
|
+
throw new Error("The proxy must be an http:// or https:// URL (or host:port). SOCKS proxies are not supported by the sandbox.");
|
|
46
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
47
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
48
|
+
await writeFile(temporary, `${JSON.stringify({ proxy: { url, ...(proxy.noProxy?.trim() ? { noProxy: proxy.noProxy.trim() } : {}) } }, null, 2)}\n`, { mode: 0o600 });
|
|
49
|
+
await rename(temporary, path);
|
|
50
|
+
}
|
|
51
|
+
/** The proxy variables of a process, including ALL_PROXY when it is an http(s) proxy (srt reads only HTTP_PROXY / HTTPS_PROXY itself). */
|
|
52
|
+
export function proxyFromEnv(env) {
|
|
53
|
+
const pick = (...names) => names.map((name) => usableProxyUrl(env[name])).find((value) => value !== undefined);
|
|
54
|
+
const all = pick("ALL_PROXY", "all_proxy");
|
|
55
|
+
const http = pick("HTTP_PROXY", "http_proxy") ?? all;
|
|
56
|
+
const https = pick("HTTPS_PROXY", "https_proxy") ?? all ?? http;
|
|
57
|
+
if (!http && !https)
|
|
58
|
+
return undefined;
|
|
59
|
+
const noProxy = (env.NO_PROXY ?? env.no_proxy ?? "").trim();
|
|
60
|
+
return { ...(http ? { http } : {}), ...(https ? { https } : {}), ...(noProxy ? { noProxy } : {}) };
|
|
61
|
+
}
|
|
62
|
+
/** The HTTP/HTTPS proxy of the macOS network settings, from the text of `scutil --proxy`. SOCKS and auto-config (PAC) entries are ignored. */
|
|
63
|
+
export function parseScutilProxy(text) {
|
|
64
|
+
const field = (name) => new RegExp(`^\\s*${name}\\s*:\\s*(.+?)\\s*$`, "mu").exec(text)?.[1];
|
|
65
|
+
const endpoint = (prefix) => {
|
|
66
|
+
const host = field(`${prefix}Proxy`);
|
|
67
|
+
const port = field(`${prefix}Port`);
|
|
68
|
+
return field(`${prefix}Enable`) === "1" && host && port ? usableProxyUrl(`http://${host.includes(":") ? `[${host}]` : host}:${port}`) : undefined;
|
|
69
|
+
};
|
|
70
|
+
const http = endpoint("HTTP");
|
|
71
|
+
const https = endpoint("HTTPS");
|
|
72
|
+
if (!http && !https)
|
|
73
|
+
return undefined;
|
|
74
|
+
const exceptions = /ExceptionsList\s*:\s*<array>\s*\{([^}]*)\}/u.exec(text)?.[1];
|
|
75
|
+
const noProxy = exceptions ? [...exceptions.matchAll(/^\s*\d+\s*:\s*(.+?)\s*$/gmu)].map((match) => match[1]).join(",") : "";
|
|
76
|
+
return { ...(http ? { http } : {}), ...(https ? { https } : {}), ...(noProxy ? { noProxy } : {}) };
|
|
77
|
+
}
|
|
78
|
+
function readSystemProxy() {
|
|
79
|
+
try {
|
|
80
|
+
return parseScutilProxy(execFileSync("scutil", ["--proxy"], { encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"] }));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The upstream proxy for the sandbox: saved value, then this process's environment, then (macOS) the system proxy. */
|
|
87
|
+
export function resolveUpstreamProxy(options = {}) {
|
|
88
|
+
const saved = "saved" in options ? options.saved : readSavedProxy();
|
|
89
|
+
if (saved)
|
|
90
|
+
return saved;
|
|
91
|
+
const fromEnv = proxyFromEnv(options.env ?? process.env);
|
|
92
|
+
if (fromEnv)
|
|
93
|
+
return fromEnv;
|
|
94
|
+
return (options.platform ?? process.platform) === "darwin" ? (options.system ?? readSystemProxy)() : undefined;
|
|
95
|
+
}
|
|
96
|
+
/** `frely sandbox proxy [<url> [--no-proxy <list>] | --clear]`: show, save or remove the proxy used by the command sandbox. */
|
|
97
|
+
export async function runSandboxProxyCommand(args, write) {
|
|
98
|
+
const noProxyIndex = args.indexOf("--no-proxy");
|
|
99
|
+
const noProxy = noProxyIndex === -1 ? undefined : args[noProxyIndex + 1];
|
|
100
|
+
if (noProxyIndex !== -1 && !noProxy)
|
|
101
|
+
throw new Error("--no-proxy needs a comma-separated list.");
|
|
102
|
+
const rest = noProxyIndex === -1 ? [...args] : args.filter((_, index) => index !== noProxyIndex && index !== noProxyIndex + 1);
|
|
103
|
+
if (rest.includes("--clear")) {
|
|
104
|
+
await saveProxy(undefined);
|
|
105
|
+
write("Saved sandbox proxy removed. Restart the MCP service (frely mcp stop, then frely mcp start) for it to take effect.\n");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (rest.length === 0) {
|
|
109
|
+
const saved = readSavedProxy();
|
|
110
|
+
const effective = resolveUpstreamProxy();
|
|
111
|
+
write(saved ? `Saved sandbox proxy: ${redact(saved.http)}${saved.noProxy ? ` (no proxy for ${saved.noProxy})` : ""}\n` : "No sandbox proxy saved.\n");
|
|
112
|
+
write(effective ? `In use when the sandbox starts: ${redact(effective.https ?? effective.http)}${saved ? "" : " (from this environment or the system settings)"}\n` : "In use when the sandbox starts: none (direct connections).\n");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (rest.length !== 1)
|
|
116
|
+
throw new Error("Usage: frely sandbox proxy [<http://host:port> [--no-proxy <list>] | --clear]");
|
|
117
|
+
await saveProxy({ url: rest[0], ...(noProxy ? { noProxy } : {}) });
|
|
118
|
+
write(`Sandbox proxy saved: ${redact(rest[0])}. Restart the MCP service (frely mcp stop, then frely mcp start) for it to take effect.\n`);
|
|
119
|
+
}
|
|
120
|
+
/** The proxy URL without its password, for display. */
|
|
121
|
+
function redact(value) {
|
|
122
|
+
if (!value)
|
|
123
|
+
return "none";
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//iu.test(value) ? value : `http://${value}`);
|
|
126
|
+
if (url.password)
|
|
127
|
+
url.password = "***";
|
|
128
|
+
return url.toString().replace(/\/$/u, "");
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return "(invalid)";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=sandbox-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-proxy.js","sourceRoot":"","sources":["../../src/runtime/sandbox-proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
|
|
2
|
+
import { type UpstreamProxy } from "./sandbox-proxy.js";
|
|
2
3
|
/**
|
|
3
4
|
* OS-level sandboxing for `run_command` / `start_process`.
|
|
4
5
|
*
|
|
@@ -85,9 +86,43 @@ export declare const SSH_PROXY_PREFIX: string;
|
|
|
85
86
|
* relay that reads them from the environment. Other platforms keep srt's own setting (Linux uses socat with proxyauth).
|
|
86
87
|
*/
|
|
87
88
|
export declare function sshProxyPrefix(platform?: string, helper?: readonly string[]): string;
|
|
88
|
-
export
|
|
89
|
+
export interface SandboxConfigOptions {
|
|
90
|
+
/** run_command only: the command may write the workspace `.git/config`; the caller then runs the guard in git-config-guard.ts. */
|
|
91
|
+
allowGitConfig?: boolean;
|
|
92
|
+
/** The user's own proxy that srt forwards to. Read once when the sandbox starts. */
|
|
93
|
+
parentProxy?: UpstreamProxy | undefined;
|
|
94
|
+
}
|
|
95
|
+
export declare function buildSandboxConfig(workspaceRoot: string, allowedGroups?: readonly string[], options?: SandboxConfigOptions): SandboxRuntimeConfig;
|
|
89
96
|
/** Plain-words refusal shown to the caller when the sandbox cannot run on this device. */
|
|
90
97
|
export declare function sandboxUnavailableMessage(reason: string): string;
|
|
98
|
+
/** What the sandbox needs to wrap one command: the parts of srt that `wrapSerialized` uses, so tests can stand in for it. */
|
|
99
|
+
export interface WrapManager {
|
|
100
|
+
updateConfig(config: SandboxRuntimeConfig): void;
|
|
101
|
+
wrapWithSandbox(command: string, binShell?: string, customConfig?: Partial<SandboxRuntimeConfig>, abortSignal?: AbortSignal, options?: {
|
|
102
|
+
commandId?: string;
|
|
103
|
+
commandText?: string;
|
|
104
|
+
}): Promise<string>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Wrap one command. Every wrap goes through this queue, one at a time, because a command that may change `.git/config` needs srt's
|
|
108
|
+
* global `filesystem.allowGitConfig` switched on while it is wrapped. srt bakes that switch into the command's own sandbox profile
|
|
109
|
+
* (the macOS seatbelt profile string, the Linux bubblewrap arguments) inside `wrapWithSandbox`, and reads it nowhere afterwards, so
|
|
110
|
+
* switching it back right after the wrap leaves that command's sandbox as it was made and later commands without it. Only the wrap
|
|
111
|
+
* (milliseconds) is queued; the commands themselves run concurrently.
|
|
112
|
+
*/
|
|
113
|
+
export declare function wrapSerialized(manager: WrapManager, base: SandboxRuntimeConfig, wrap: () => Promise<string>, allowGitConfig: boolean): Promise<string>;
|
|
114
|
+
export interface SandboxedCommand {
|
|
115
|
+
/** The command to spawn with `shell: true`. */
|
|
116
|
+
command: string;
|
|
117
|
+
/** The key under which srt records this command's violations; undefined when the command is not sandboxed. */
|
|
118
|
+
commandId?: string;
|
|
119
|
+
/** The command may change `.git/config`: snapshot it before the spawn and run the guard after the exit. */
|
|
120
|
+
gitConfig: boolean;
|
|
121
|
+
}
|
|
122
|
+
export interface SandboxCommandOptions {
|
|
123
|
+
/** run_command only: allow writing the workspace `.git/config` (the caller must run the guard). start_process never sets this. */
|
|
124
|
+
allowGitConfig?: boolean;
|
|
125
|
+
}
|
|
91
126
|
/**
|
|
92
127
|
* Wrap a shell command with sandbox restrictions before it is spawned with
|
|
93
128
|
* `shell: true`. Fails closed: when the sandbox cannot run (unsupported
|
|
@@ -96,7 +131,10 @@ export declare function sandboxUnavailableMessage(reason: string): string;
|
|
|
96
131
|
* is returned unchanged only when the owner turned the sandbox off
|
|
97
132
|
* (`FRELY_SANDBOX=off`) or approved the `unsandboxed` grant for this call.
|
|
98
133
|
*/
|
|
134
|
+
export declare function sandboxCommandDetailed(command: string, workspaceRoot: string, allowedGroups?: readonly string[], options?: SandboxCommandOptions): Promise<SandboxedCommand>;
|
|
99
135
|
export declare function sandboxCommand(command: string, workspaceRoot: string, allowedGroups?: readonly string[]): Promise<string>;
|
|
136
|
+
/** The violations srt recorded for one wrapped command (proxy denials always; file and socket denials where srt's log monitor runs). */
|
|
137
|
+
export declare function sandboxViolationLines(commandId: string | undefined): string[];
|
|
100
138
|
/** The group whose unapproved credential path a command names, or `null` for a path no grant can open. */
|
|
101
139
|
type DeniedRead = {
|
|
102
140
|
path: string;
|
|
@@ -108,11 +146,17 @@ type DeniedRead = {
|
|
|
108
146
|
* Text matching only: a script that reads the path itself is still confined by the sandbox, just without this explanation.
|
|
109
147
|
*/
|
|
110
148
|
export declare function deniedReadReference(command: string, allowedGroups?: readonly string[]): DeniedRead | undefined;
|
|
149
|
+
/** What the caller knows about a failed command beyond its stderr: srt's recorded violations and the workspace it ran in. */
|
|
150
|
+
export interface DenialContext {
|
|
151
|
+
violations?: readonly string[];
|
|
152
|
+
workspaceRoot?: string;
|
|
153
|
+
}
|
|
111
154
|
/**
|
|
112
|
-
* Hint for a failed sandboxed command whose
|
|
113
|
-
* (request_permission) instead of giving up on a bare "Operation not permitted"
|
|
155
|
+
* Hint for a failed sandboxed command whose output looks like a sandbox denial, so the caller asks for the narrowest approval
|
|
156
|
+
* (request_permission) instead of giving up on a bare "Operation not permitted" or reaching for `unsandboxed`. It reads srt's
|
|
157
|
+
* recorded violations, then stderr and the command text. Nothing is guessed beyond that: a denial it cannot place gets the generic hint.
|
|
114
158
|
*/
|
|
115
|
-
export declare function sandboxDenialHint(stderr: string, command?: string, exitCode?: number | null, sshConfigReadable?: boolean): string | undefined;
|
|
159
|
+
export declare function sandboxDenialHint(stderr: string, command?: string, exitCode?: number | null, sshConfigReadable?: boolean, context?: DenialContext): string | undefined;
|
|
116
160
|
/** Whether ~/.ssh/config can be read by the CLI process itself (outside the sandbox). */
|
|
117
161
|
export declare function sshConfigReadable(): boolean;
|
|
118
162
|
/** Exposed for tests: resets the module-level init state and the underlying manager. */
|
package/dist/runtime/sandbox.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
3
|
import { homedir, tmpdir } from "node:os";
|
|
3
|
-
import { isAbsolute, join, normalize, parse, sep } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, normalize, parse, sep } from "node:path";
|
|
4
5
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
5
6
|
import { cliLaunchArguments } from "../cli-launch.js";
|
|
7
|
+
import { gitConfigGuardAvailable } from "./git-config-guard.js";
|
|
8
|
+
import { resolveUpstreamProxy } from "./sandbox-proxy.js";
|
|
6
9
|
/**
|
|
7
10
|
* OS-level sandboxing for `run_command` / `start_process`.
|
|
8
11
|
*
|
|
@@ -218,7 +221,27 @@ function helperEntry() {
|
|
|
218
221
|
return process.argv[1] ?? "";
|
|
219
222
|
}
|
|
220
223
|
}
|
|
221
|
-
|
|
224
|
+
/** A directory `path` inside the workspace, or false when it is missing or only a link (a worktree's `.git` file). */
|
|
225
|
+
const isDirectory = (path) => { try {
|
|
226
|
+
return statSync(path).isDirectory();
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return false;
|
|
230
|
+
} };
|
|
231
|
+
/**
|
|
232
|
+
* `.git` paths of the workspace root that srt does not (or no longer) protect on its own. srt derives its `.git/hooks` and `.git/config`
|
|
233
|
+
* denials from the CLI process's current directory, not from the workspace a command runs in, so they are named here as well.
|
|
234
|
+
* `.git/hooks` is always denied (hooks run programs outside the sandbox). `.git/config` is denied unless the command may change it
|
|
235
|
+
* (run_command with the guard in git-config-guard.ts).
|
|
236
|
+
*/
|
|
237
|
+
function gitWriteDenials(workspaceRoot, allowGitConfig) {
|
|
238
|
+
const dotGit = join(workspaceRoot, ".git");
|
|
239
|
+
if (!isDirectory(dotGit))
|
|
240
|
+
return [];
|
|
241
|
+
return [...(isDirectory(join(dotGit, "hooks")) ? [join(dotGit, "hooks")] : []), ...(allowGitConfig ? [] : [join(dotGit, "config")])];
|
|
242
|
+
}
|
|
243
|
+
export function buildSandboxConfig(workspaceRoot, allowedGroups = [], options = {}) {
|
|
244
|
+
const allowGitConfig = options.allowGitConfig === true;
|
|
222
245
|
return {
|
|
223
246
|
network: {
|
|
224
247
|
// Left open by default: an empty allowlist would break ordinary
|
|
@@ -230,11 +253,13 @@ export function buildSandboxConfig(workspaceRoot, allowedGroups = []) {
|
|
|
230
253
|
deniedDomains: [],
|
|
231
254
|
// Local dev servers and tests need to listen on and reach loopback ports. srt reads this only when it initializes, so it cannot vary per call.
|
|
232
255
|
allowLocalBinding: true,
|
|
256
|
+
...(options.parentProxy ? { parentProxy: options.parentProxy } : {}),
|
|
233
257
|
},
|
|
234
258
|
filesystem: {
|
|
235
259
|
denyRead: sensitiveReadPaths(allowedGroups),
|
|
236
260
|
allowWrite: [workspaceRoot, tmpdir(), ...writablePaths(allowedGroups)],
|
|
237
|
-
denyWrite: protectedWriteDenials(allowedGroups),
|
|
261
|
+
denyWrite: [...protectedWriteDenials(allowedGroups), ...gitWriteDenials(workspaceRoot, allowGitConfig)],
|
|
262
|
+
...(allowGitConfig ? { allowGitConfig: true } : {}),
|
|
238
263
|
},
|
|
239
264
|
};
|
|
240
265
|
}
|
|
@@ -253,6 +278,8 @@ export function sandboxUnavailableMessage(reason) {
|
|
|
253
278
|
+ "or set FRELY_SANDBOX=off on the device.";
|
|
254
279
|
}
|
|
255
280
|
let initialized;
|
|
281
|
+
/** The configuration srt was initialized with: what every command runs under, and what is put back after a git-config wrap. */
|
|
282
|
+
let baseConfig;
|
|
256
283
|
async function ensureInitialized(workspaceRoot) {
|
|
257
284
|
initialized ??= (async () => {
|
|
258
285
|
if (!SandboxManager.isSupportedPlatform()) {
|
|
@@ -269,7 +296,9 @@ async function ensureInitialized(workspaceRoot) {
|
|
|
269
296
|
for (const warning of dependencies.warnings)
|
|
270
297
|
warnOnce(warning);
|
|
271
298
|
try {
|
|
272
|
-
|
|
299
|
+
// The proxy servers keep the upstream proxy they start with, so a changed proxy needs a restart of the service.
|
|
300
|
+
baseConfig = buildSandboxConfig(workspaceRoot, [], { parentProxy: resolveUpstreamProxy() });
|
|
301
|
+
await SandboxManager.initialize(structuredClone(baseConfig));
|
|
273
302
|
return { ready: true };
|
|
274
303
|
}
|
|
275
304
|
catch (error) {
|
|
@@ -280,6 +309,30 @@ async function ensureInitialized(workspaceRoot) {
|
|
|
280
309
|
})();
|
|
281
310
|
return initialized;
|
|
282
311
|
}
|
|
312
|
+
let wrapQueue = Promise.resolve();
|
|
313
|
+
/**
|
|
314
|
+
* Wrap one command. Every wrap goes through this queue, one at a time, because a command that may change `.git/config` needs srt's
|
|
315
|
+
* global `filesystem.allowGitConfig` switched on while it is wrapped. srt bakes that switch into the command's own sandbox profile
|
|
316
|
+
* (the macOS seatbelt profile string, the Linux bubblewrap arguments) inside `wrapWithSandbox`, and reads it nowhere afterwards, so
|
|
317
|
+
* switching it back right after the wrap leaves that command's sandbox as it was made and later commands without it. Only the wrap
|
|
318
|
+
* (milliseconds) is queued; the commands themselves run concurrently.
|
|
319
|
+
*/
|
|
320
|
+
export function wrapSerialized(manager, base, wrap, allowGitConfig) {
|
|
321
|
+
const task = async () => {
|
|
322
|
+
if (!allowGitConfig)
|
|
323
|
+
return wrap();
|
|
324
|
+
manager.updateConfig({ ...base, filesystem: { ...base.filesystem, allowGitConfig: true } });
|
|
325
|
+
try {
|
|
326
|
+
return await wrap();
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
manager.updateConfig(base);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const run = wrapQueue.then(task, task);
|
|
333
|
+
wrapQueue = run.catch(() => undefined);
|
|
334
|
+
return run;
|
|
335
|
+
}
|
|
283
336
|
/**
|
|
284
337
|
* Wrap a shell command with sandbox restrictions before it is spawned with
|
|
285
338
|
* `shell: true`. Fails closed: when the sandbox cannot run (unsupported
|
|
@@ -288,26 +341,45 @@ async function ensureInitialized(workspaceRoot) {
|
|
|
288
341
|
* is returned unchanged only when the owner turned the sandbox off
|
|
289
342
|
* (`FRELY_SANDBOX=off`) or approved the `unsandboxed` grant for this call.
|
|
290
343
|
*/
|
|
291
|
-
export async function
|
|
344
|
+
export async function sandboxCommandDetailed(command, workspaceRoot, allowedGroups = [], options = {}) {
|
|
292
345
|
if (isSandboxDisabled())
|
|
293
|
-
return command;
|
|
346
|
+
return { command, gitConfig: false };
|
|
294
347
|
// The owner opened raw outbound connections for this call, which the sandbox cannot offer without losing its other limits.
|
|
295
348
|
if (allowedGroups.includes(UNSANDBOXED_GROUP))
|
|
296
|
-
return command;
|
|
349
|
+
return { command, gitConfig: false };
|
|
297
350
|
const denied = deniedReadReference(command, allowedGroups);
|
|
298
351
|
if (denied)
|
|
299
352
|
throw new Error(deniedReadMessage(denied));
|
|
300
353
|
const init = await ensureInitialized(workspaceRoot);
|
|
301
|
-
if (!init.ready)
|
|
302
|
-
throw new Error(sandboxUnavailableMessage(init.reason));
|
|
354
|
+
if (!init.ready || !baseConfig)
|
|
355
|
+
throw new Error(sandboxUnavailableMessage(init.ready ? "sandbox configuration missing" : init.reason));
|
|
356
|
+
// Without git the guard cannot check the file afterwards, so `.git/config` then stays read-only.
|
|
357
|
+
const gitConfig = options.allowGitConfig === true && await gitConfigGuardAvailable();
|
|
358
|
+
const commandId = randomUUID();
|
|
359
|
+
// initialize() fixed allowWrite to the first workspace that ran a command; give every call its own workspace.
|
|
360
|
+
const { allowGitConfig: _unused, ...filesystem } = buildSandboxConfig(workspaceRoot, allowedGroups, { allowGitConfig: gitConfig }).filesystem ?? {};
|
|
303
361
|
try {
|
|
304
|
-
|
|
305
|
-
return
|
|
362
|
+
const wrapped = await wrapSerialized(SandboxManager, baseConfig, () => SandboxManager.wrapWithSandbox(sshProxyPrefix() + command, undefined, { filesystem }, undefined, { commandId, commandText: command }), gitConfig);
|
|
363
|
+
return { command: wrapped, commandId, gitConfig };
|
|
306
364
|
}
|
|
307
365
|
catch (error) {
|
|
308
366
|
throw new Error(sandboxUnavailableMessage(`wrapping the command failed: ${errorText(error)}`));
|
|
309
367
|
}
|
|
310
368
|
}
|
|
369
|
+
export async function sandboxCommand(command, workspaceRoot, allowedGroups = []) {
|
|
370
|
+
return (await sandboxCommandDetailed(command, workspaceRoot, allowedGroups)).command;
|
|
371
|
+
}
|
|
372
|
+
/** The violations srt recorded for one wrapped command (proxy denials always; file and socket denials where srt's log monitor runs). */
|
|
373
|
+
export function sandboxViolationLines(commandId) {
|
|
374
|
+
if (!commandId)
|
|
375
|
+
return [];
|
|
376
|
+
try {
|
|
377
|
+
return SandboxManager.getSandboxViolationStore().getViolationsForCommand(commandId).map((violation) => violation.line);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
311
383
|
/**
|
|
312
384
|
* A credential path the command names in its text without an approved grant. The sandbox would only show such a folder as empty
|
|
313
385
|
* (Linux) or fail with a bare "Operation not permitted" (macOS), and a remote client would read that as "the file does not exist".
|
|
@@ -335,18 +407,92 @@ function deniedReadMessage({ path, group }) {
|
|
|
335
407
|
return `The command was not run: no permission to read ${path}. Frely's command sandbox hides credential folders until the device owner approves access. `
|
|
336
408
|
+ `Call request_permission with resource "${group}", the reason and this exact command; it returns a link where the owner approves it with a passkey or authenticator code. Then run the same command again.`;
|
|
337
409
|
}
|
|
410
|
+
const DENIAL_TEXT = /operation not permitted|permission denied|read-only file system|sandbox/iu;
|
|
411
|
+
const DOCKER_SOCKET = /docker\.sock|cannot connect to the docker daemon|docker daemon socket/iu;
|
|
412
|
+
const RAW_TCP_CLIENT = /(?:^|[\s;&|(])(?:psql|pg_dump|pg_restore|mysql|mysqldump|mariadb|redis-cli|mongosh?|nc|ncat|netcat|telnet|sqlcmd|cqlsh|clickhouse-client)(?=\s|$)/u;
|
|
413
|
+
const CONNECTION_FAILURE = /connection refused|econnrefused|network is unreachable|no route to host|could not connect|can't connect|connection timed out|operation not permitted/iu;
|
|
414
|
+
const XPC_DENIAL = /mach-lookup|\bxpc\b|connection invalid|bootstrap_look_up/iu;
|
|
415
|
+
const SEATBELT_NETWORK_DENIAL = /deny(?:\(\d+\))? network-outbound/u;
|
|
416
|
+
/** `~/x` for paths below the home directory, the plain path otherwise. */
|
|
417
|
+
const shortPath = (path) => path === homedir() ? "~" : path.startsWith(homedir() + sep) ? `~/${path.slice(homedir().length + 1)}` : path;
|
|
418
|
+
/** Absolute paths named on the lines of `text` that look like a refused operation. */
|
|
419
|
+
function deniedPaths(lines) {
|
|
420
|
+
const found = [];
|
|
421
|
+
for (const line of lines) {
|
|
422
|
+
if (!/operation not permitted|permission denied|read-only file system|deny(?:\(\d+\))? file-write/iu.test(line))
|
|
423
|
+
continue;
|
|
424
|
+
for (const match of line.matchAll(/(?<![\w.~-])(\/[^\s'"`:;,)<>|]+)/gu))
|
|
425
|
+
found.push(match[1]);
|
|
426
|
+
}
|
|
427
|
+
return [...new Set(found)];
|
|
428
|
+
}
|
|
429
|
+
/** The `path:` resource that would open the write `path` for a command, or undefined when no grant can. */
|
|
430
|
+
function writeGrantFor(path, workspaceRoot) {
|
|
431
|
+
const target = normalize(path);
|
|
432
|
+
if (workspaceRoot && within(target, normalize(workspaceRoot)))
|
|
433
|
+
return undefined;
|
|
434
|
+
if (within(target, normalize(tmpdir())) || /^\/(?:dev|proc|sys)(?:\/|$)/u.test(target))
|
|
435
|
+
return undefined;
|
|
436
|
+
// A protected path (a shell start-up file, a credential folder) can only be opened by naming it exactly.
|
|
437
|
+
const protectedPath = PROTECTED_PATHS.map(expanded).find((item) => within(target, item));
|
|
438
|
+
const dir = protectedPath ?? dirname(target);
|
|
439
|
+
// Grant names always use forward slashes, whatever the platform separator is.
|
|
440
|
+
const grant = `path:${shortPath(dir)}`.replace(/\\/gu, "/");
|
|
441
|
+
return customWritePath(grant) === undefined ? undefined : grant;
|
|
442
|
+
}
|
|
338
443
|
/**
|
|
339
|
-
* Hint for a failed sandboxed command whose
|
|
340
|
-
* (request_permission) instead of giving up on a bare "Operation not permitted"
|
|
444
|
+
* Hint for a failed sandboxed command whose output looks like a sandbox denial, so the caller asks for the narrowest approval
|
|
445
|
+
* (request_permission) instead of giving up on a bare "Operation not permitted" or reaching for `unsandboxed`. It reads srt's
|
|
446
|
+
* recorded violations, then stderr and the command text. Nothing is guessed beyond that: a denial it cannot place gets the generic hint.
|
|
341
447
|
*/
|
|
342
|
-
export function sandboxDenialHint(stderr, command, exitCode, sshConfigReadable) {
|
|
448
|
+
export function sandboxDenialHint(stderr, command, exitCode, sshConfigReadable, context = {}) {
|
|
343
449
|
const ssh = command !== undefined ? sshHint(stderr, command, exitCode, sshConfigReadable) : undefined;
|
|
344
450
|
if (ssh)
|
|
345
451
|
return ssh;
|
|
346
|
-
|
|
452
|
+
const violations = context.violations ?? [];
|
|
453
|
+
const lines = [...stderr.split(/\r?\n/u), ...violations];
|
|
454
|
+
const text = lines.join("\n");
|
|
455
|
+
const rawTcp = command !== undefined && RAW_TCP_CLIENT.test(command) && CONNECTION_FAILURE.test(stderr) || violations.some((line) => SEATBELT_NETWORK_DENIAL.test(line));
|
|
456
|
+
if (!(DENIAL_TEXT.test(text) || DOCKER_SOCKET.test(text) || XPC_DENIAL.test(text) || rawTcp || violations.length > 0))
|
|
457
|
+
return undefined;
|
|
458
|
+
const UNSANDBOXED_ASK = `Call request_permission with resource "${UNSANDBOXED_GROUP}", the reason and this exact command; the owner approves it once by default, for just this command. Then run the same command again.`;
|
|
459
|
+
// The Docker socket is the whole story: nothing narrower opens it, and the config.json / socket directory that show up in the same
|
|
460
|
+
// output are not something a path: grant can or should open (~/.docker is never opened; /var/run is far too broad).
|
|
461
|
+
if (DOCKER_SOCKET.test(text)) {
|
|
462
|
+
return `The Docker socket cannot be opened from the Frely command sandbox, and nothing narrower than ${UNSANDBOXED_GROUP} reaches it (a container can mount the whole home directory). ${UNSANDBOXED_ASK}`;
|
|
463
|
+
}
|
|
464
|
+
const hints = [];
|
|
465
|
+
if (/\.git[\\/]hooks\b/u.test(text)) {
|
|
466
|
+
hints.push("Writing to .git/hooks stays blocked in the Frely command sandbox: hooks run programs outside it, so no approval opens it and request_permission will not help. Ask the user to add or change the hook themselves.");
|
|
467
|
+
}
|
|
468
|
+
if (/\.git[\\/]config\b/u.test(text) && DENIAL_TEXT.test(text)) {
|
|
469
|
+
hints.push("Writing .git/config is allowed in run_command for a .git directory inside the workspace, and never in start_process (run it with run_command instead).");
|
|
470
|
+
}
|
|
471
|
+
const groups = [...Object.entries(SENSITIVE_PATH_GROUPS), ["friday-admin", ADMIN_SESSION_PATHS]]
|
|
472
|
+
.filter(([, paths]) => paths.some((path) => commandNamesPath(text, path))).map(([group]) => group);
|
|
473
|
+
for (const group of groups) {
|
|
474
|
+
hints.push(`The Frely command sandbox hides these credential folders until the device owner approves them. Call request_permission with resource "${group}", the reason and this exact command; the owner approves it on the web. Then run the same command again.`);
|
|
475
|
+
}
|
|
476
|
+
if (groups.length === 0) {
|
|
477
|
+
const grants = [...new Set(deniedPaths(lines).flatMap((path) => writeGrantFor(path, context.workspaceRoot) ?? []))];
|
|
478
|
+
if (grants.length > 0 && !/\.git[\\/]hooks\b/u.test(text)) {
|
|
479
|
+
hints.push(`The command tried to write outside the workspace and temp directory, which the Frely command sandbox blocks. Call request_permission with ${grants.map((grant) => `resource "${grant}"`).join(" and ")}, the reason and this exact command; the owner approves it on the web. Then run the same command again.`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (rawTcp) {
|
|
483
|
+
hints.push("The Frely command sandbox only routes HTTP(S) and SOCKS traffic, so a direct TCP connection (database and redis clients, nc) is blocked. "
|
|
484
|
+
+ "Stay in the sandbox: start `frely tunnel <host>:<port>` in the same command and point the client at 127.0.0.1:<port>, for example `frely tunnel db.example.com:5432 & sleep 1; psql -h 127.0.0.1 -p 5432 ...`. "
|
|
485
|
+
+ `If a tunnel cannot work for this client, ${UNSANDBOXED_ASK.replace(/^Call/u, "call")}`);
|
|
486
|
+
}
|
|
487
|
+
else if (XPC_DENIAL.test(text)) {
|
|
488
|
+
hints.push(`The command needs a macOS system service (XPC) that the Frely command sandbox does not open, and only ${UNSANDBOXED_GROUP} reaches it. ${UNSANDBOXED_ASK}`);
|
|
489
|
+
}
|
|
490
|
+
if (hints.length > 0)
|
|
491
|
+
return hints.join(" ");
|
|
492
|
+
if (!DENIAL_TEXT.test(text))
|
|
347
493
|
return undefined;
|
|
348
494
|
return "This command may have been blocked by the Frely command sandbox (writes only to the workspace and temp directory; credential folders unreadable; only HTTP(S) and ssh reach the network). "
|
|
349
|
-
+
|
|
495
|
+
+ `Name what was blocked and ask for the narrowest approval: call request_permission with the reason and the exact command (a credential group, cache directories or a specific path:<dir> first, ${UNSANDBOXED_GROUP} only when nothing narrower fits); the owner approves it on the web with a passkey or authenticator code. Then run the same command again.`;
|
|
350
496
|
}
|
|
351
497
|
const SSH_COMMAND = /(?:^|[\s;&|(])(?:ssh|scp|sftp|git)\s/u;
|
|
352
498
|
const SSH_FAILURE = /proxy refused the tunnel|could not resolve hostname|kex_exchange_identification/iu;
|
|
@@ -373,6 +519,8 @@ export function sshConfigReadable() {
|
|
|
373
519
|
/** Exposed for tests: resets the module-level init state and the underlying manager. */
|
|
374
520
|
export async function resetSandboxForTests() {
|
|
375
521
|
initialized = undefined;
|
|
522
|
+
baseConfig = undefined;
|
|
523
|
+
wrapQueue = Promise.resolve();
|
|
376
524
|
warned = false;
|
|
377
525
|
await SandboxManager.reset().catch(() => undefined);
|
|
378
526
|
}
|