@yagni-app/code-staging 1.1.0-staging.1325.1 → 1.1.0-staging.1327.1
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/dist/extension/sandbox/bash.d.ts +36 -2
- package/dist/extension/sandbox/bash.js +107 -5
- package/dist/extension/sandbox/config.d.ts +9 -0
- package/dist/extension/sandbox/config.js +85 -1
- package/dist/extension/sandbox/manager.d.ts +7 -0
- package/dist/extension/sandbox/manager.js +10 -0
- package/dist/extension/sandbox/panel.d.ts +34 -3
- package/dist/extension/sandbox/panel.js +161 -2
- package/dist/extension/sandbox/session.d.ts +14 -1
- package/dist/extension/sandbox/session.js +204 -10
- package/package.json +2 -2
|
@@ -69,6 +69,29 @@ export declare function preWrappedCommand(manager: YagniSandboxManager, command:
|
|
|
69
69
|
* offending path when found (pi-sandbox's regex, credited MIT).
|
|
70
70
|
*/
|
|
71
71
|
export declare function extractBlockedWritePath(output: string): string | null;
|
|
72
|
+
/** The three network-posture denial classes this classifier knows (closed
|
|
73
|
+
* set — rides the sink line as a low-cardinality field). */
|
|
74
|
+
export type NetworkDenialClass = "ipc-listen" | "tls-trustd" | "loopback";
|
|
75
|
+
/**
|
|
76
|
+
* Classify a sandbox network-posture denial from command output. Pure;
|
|
77
|
+
* advisory-only (never widens anything — the fix is a knob the USER flips,
|
|
78
|
+
* never an automatic grant). Matches the signatures observed live and in
|
|
79
|
+
* the session review:
|
|
80
|
+
* - ipc-listen: `listen EPERM` (node:events EPERM with syscall listen —
|
|
81
|
+
* a named unix-socket or TCP bind the profile denies;
|
|
82
|
+
* tsx/vitest test-runner IPC is the canonical case)
|
|
83
|
+
* - tls-trustd: `x509` + `OSStatus -26276` (Go TLS verification denied
|
|
84
|
+
* the trustd.agent mach lookup — gh/gcloud/terraform)
|
|
85
|
+
* - loopback: EPERM/"not permitted" on a connect to 127.0.0.1/
|
|
86
|
+
* localhost/::1 (loopback bypasses the proxy via
|
|
87
|
+
* no_proxy, so the allowlist can never admit it)
|
|
88
|
+
* Returns null when the output carries no network-posture signature — a
|
|
89
|
+
* plain file-write EPERM must NOT get a network hint.
|
|
90
|
+
*/
|
|
91
|
+
export declare function networkDenialHint(output: string): {
|
|
92
|
+
cls: NetworkDenialClass;
|
|
93
|
+
hint: string;
|
|
94
|
+
} | null;
|
|
72
95
|
/**
|
|
73
96
|
* Wrap decision for user_bash (! commands): same rules as the tool, minus
|
|
74
97
|
* the dangerouslyDisableSandbox parameter (users running ! commands are
|
|
@@ -78,9 +101,20 @@ export declare function shouldUseSandboxForUserCommand(command: string, manager:
|
|
|
78
101
|
/**
|
|
79
102
|
* Post-execution violation annotation: annotate a command's combined output
|
|
80
103
|
* with sandbox violation details when the sandbox is active. Returns the
|
|
81
|
-
* annotated output (or the original when nothing to annotate)
|
|
104
|
+
* annotated output (or the original when nothing to annotate) plus the
|
|
105
|
+
* network-posture classification (null when the output carried no network
|
|
106
|
+
* signature) so callers can log the sink event without a second regex pass.
|
|
107
|
+
* Composes the hint (networkDenialHint) after the violation detail —
|
|
108
|
+
* advisory only, never a grant. Uninitialized manager ⇒ passthrough, no
|
|
109
|
+
* hint (no annotation context, and the sink is not wired yet).
|
|
82
110
|
*/
|
|
83
|
-
export declare function annotateCommandOutput(manager: YagniSandboxManager, command: string, output: string):
|
|
111
|
+
export declare function annotateCommandOutput(manager: YagniSandboxManager, command: string, output: string): {
|
|
112
|
+
text: string;
|
|
113
|
+
net: {
|
|
114
|
+
cls: NetworkDenialClass;
|
|
115
|
+
hint: string;
|
|
116
|
+
} | null;
|
|
117
|
+
};
|
|
84
118
|
/** cwd must exist before spawn (guard borrowed from pi's local ops). */
|
|
85
119
|
export declare function assertSpawnableCwd(cwd: string): void;
|
|
86
120
|
/**
|
|
@@ -131,6 +131,97 @@ export function extractBlockedWritePath(output) {
|
|
|
131
131
|
const match = output.match(/(?:\/bin\/bash|bash|sh): (?:line \d+: )?(\/[^\s:]+): Operation not permitted/);
|
|
132
132
|
return match ? match[1] : null;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Classify a sandbox network-posture denial from command output. Pure;
|
|
136
|
+
* advisory-only (never widens anything — the fix is a knob the USER flips,
|
|
137
|
+
* never an automatic grant). Matches the signatures observed live and in
|
|
138
|
+
* the session review:
|
|
139
|
+
* - ipc-listen: `listen EPERM` (node:events EPERM with syscall listen —
|
|
140
|
+
* a named unix-socket or TCP bind the profile denies;
|
|
141
|
+
* tsx/vitest test-runner IPC is the canonical case)
|
|
142
|
+
* - tls-trustd: `x509` + `OSStatus -26276` (Go TLS verification denied
|
|
143
|
+
* the trustd.agent mach lookup — gh/gcloud/terraform)
|
|
144
|
+
* - loopback: EPERM/"not permitted" on a connect to 127.0.0.1/
|
|
145
|
+
* localhost/::1 (loopback bypasses the proxy via
|
|
146
|
+
* no_proxy, so the allowlist can never admit it)
|
|
147
|
+
* Returns null when the output carries no network-posture signature — a
|
|
148
|
+
* plain file-write EPERM must NOT get a network hint.
|
|
149
|
+
*/
|
|
150
|
+
export function networkDenialHint(output) {
|
|
151
|
+
// ipc-listen: EPERM on a socket/TCP bind — node prints "listen EPERM: …",
|
|
152
|
+
// other tools print "EPERM … listen". Both orders are the same OS
|
|
153
|
+
// denial, but the second marker must be a socket/TCP-bind signal, not the
|
|
154
|
+
// bare word "listen" — a file-write EPERM whose text merely contains
|
|
155
|
+
// "listen" (e.g. `bash: ./listen.sh: Operation not permitted`) must stay a
|
|
156
|
+
// filesystem denial (the pinned invariant). Qualification is strictly
|
|
157
|
+
// SAME-LINE: socket markers, host:port addresses, and the inline
|
|
158
|
+
// syscall:'listen' token must all sit on the denial line itself (a stray
|
|
159
|
+
// IP:port or detail line elsewhere in the output is not a bind denial;
|
|
160
|
+
// node's two-line shape qualifies because its EPERM line carries the
|
|
161
|
+
// .pipe/.sock path or host:port address on its own line).
|
|
162
|
+
const sysListen = /syscall: ['"]listen['"]/i;
|
|
163
|
+
// The listen+EPERM denial shape. Two spellings are deliberately NOT a
|
|
164
|
+
// denial: the `syscall: 'listen'` value (stripped before the test — it
|
|
165
|
+
// is a detail-line token, not a denial verb, and a stray detail line
|
|
166
|
+
// anywhere in the output must not self-qualify) and filename-like
|
|
167
|
+
// tokens (listen.sh, listen:) — the bare word "listen" in a path is
|
|
168
|
+
// not a bind attempt (the listen.sh false-positive class). A line
|
|
169
|
+
// carrying its own syscall token still counts when it is otherwise a
|
|
170
|
+
// denial shape (an inline-token denial line, not a stray).
|
|
171
|
+
const denialShape = (line) => {
|
|
172
|
+
const stripped = line
|
|
173
|
+
.replace(/syscall: ['"]listen['"]/gi, "")
|
|
174
|
+
.replace(/\blisten[.:_-]\w*/gi, "");
|
|
175
|
+
return /\blisten\b/.test(stripped) && /\bEPERM\b|Operation not permitted/i.test(stripped);
|
|
176
|
+
};
|
|
177
|
+
const perLineMarker = /(\.sock\b|\.pipe\b|unix[- ]socket|\bipc\b|socket-domain|AF_UNIX|\bsocket\b|\bbind\b|\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+\b|\blocalhost:\d+\b)/i;
|
|
178
|
+
// A line CARRYING the syscall token but no other bind evidence is a
|
|
179
|
+
// node error-object DETAIL line (` code: 'EPERM', … syscall: 'listen'`)
|
|
180
|
+
// — a continuation, never a denial line in its own right. Only an
|
|
181
|
+
// otherwise-denial line with its own INLINE syscall token (the one-line
|
|
182
|
+
// spelling) qualifies; a detail line riding on a neighbor never does.
|
|
183
|
+
// Socket markers and host:port addresses keep the same-line discipline.
|
|
184
|
+
if (/\blisten\b/.test(output)) {
|
|
185
|
+
const lines = output.split("\n");
|
|
186
|
+
// A real bind denial: a single line that is BOTH a denial shape and
|
|
187
|
+
// carries its own bind evidence (a socket/address marker, or the
|
|
188
|
+
// inline syscall token). No cross-line qualification: every observed
|
|
189
|
+
// node denial line carries its own evidence (the .pipe/.sock path or
|
|
190
|
+
// the host:port address), and letting a detail line ride on its
|
|
191
|
+
// neighbor is exactly how stray `syscall: 'listen'` lines from
|
|
192
|
+
// unrelated output false-positived. The detail line never qualifies
|
|
193
|
+
// in its own right.
|
|
194
|
+
const denialLine = lines.some((line) => denialShape(line) && (perLineMarker.test(line) || sysListen.test(line)));
|
|
195
|
+
if (denialLine) {
|
|
196
|
+
return {
|
|
197
|
+
cls: "ipc-listen",
|
|
198
|
+
hint: "This looks like a sandbox network-posture denial (a socket bind the sandbox profile denies — test-runner IPC is the usual case). " +
|
|
199
|
+
"The user can fix it in the /sandbox panel (Network tab: the Engineering preset or the unix-sockets toggle); retrying with dangerouslyDisableSandbox is NOT needed once the knob is on.",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (/x509[^\n]*OSStatus -26276|OSStatus -26276/.test(output)) {
|
|
204
|
+
return {
|
|
205
|
+
cls: "tls-trustd",
|
|
206
|
+
hint: "This looks like a sandbox TLS-verification denial (the trustd.agent mach lookup is denied — Go CLIs like gh verify certs through it even on allowlisted domains). " +
|
|
207
|
+
"The user can fix it in the /sandbox panel (Network tab: the Engineering preset or the trustd toggle); retrying with dangerouslyDisableSandbox is NOT needed once the knob is on.",
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// Loopback connect denials: EPERM/not-permitted text with a loopback
|
|
211
|
+
// address nearby. Node prints `Error: listen EPERM ... (code: 'EPERM',
|
|
212
|
+
// syscall: 'connect', address: '127.0.0.1')`; curl prints the socket
|
|
213
|
+
// error line; generic CLIs print `connect() to 127.0.0.1 failed`.
|
|
214
|
+
if (/(EPERM|Operation not permitted|not permitted)/i.test(output) &&
|
|
215
|
+
/(127\.0\.0\.1|localhost|\[::1\]|"::1")/.test(output) &&
|
|
216
|
+
/(connect|curl|psql|wget|fetch|Failed to connect|Couldn't connect)/i.test(output)) {
|
|
217
|
+
return {
|
|
218
|
+
cls: "loopback",
|
|
219
|
+
hint: "This looks like a sandbox loopback denial (loopback bypasses the network allowlist, so localhost services are blocked by default). " +
|
|
220
|
+
"The user can fix it in the /sandbox panel (Network tab: the Engineering preset or the local-binding toggle); retrying with dangerouslyDisableSandbox is NOT needed once the knob is on.",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
134
225
|
/**
|
|
135
226
|
* Wrap decision for user_bash (! commands): same rules as the tool, minus
|
|
136
227
|
* the dangerouslyDisableSandbox parameter (users running ! commands are
|
|
@@ -146,14 +237,25 @@ export function shouldUseSandboxForUserCommand(command, manager, settings) {
|
|
|
146
237
|
/**
|
|
147
238
|
* Post-execution violation annotation: annotate a command's combined output
|
|
148
239
|
* with sandbox violation details when the sandbox is active. Returns the
|
|
149
|
-
* annotated output (or the original when nothing to annotate)
|
|
240
|
+
* annotated output (or the original when nothing to annotate) plus the
|
|
241
|
+
* network-posture classification (null when the output carried no network
|
|
242
|
+
* signature) so callers can log the sink event without a second regex pass.
|
|
243
|
+
* Composes the hint (networkDenialHint) after the violation detail —
|
|
244
|
+
* advisory only, never a grant. Uninitialized manager ⇒ passthrough, no
|
|
245
|
+
* hint (no annotation context, and the sink is not wired yet).
|
|
150
246
|
*/
|
|
151
247
|
export function annotateCommandOutput(manager, command, output) {
|
|
152
248
|
if (!manager.initialized)
|
|
153
|
-
return output;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
249
|
+
return { text: output, net: null };
|
|
250
|
+
const net = networkDenialHint(output);
|
|
251
|
+
if (!output.includes("Operation not permitted") && !net)
|
|
252
|
+
return { text: output, net: null };
|
|
253
|
+
const annotated = output.includes("Operation not permitted")
|
|
254
|
+
? manager.annotateStderrWithSandboxFailures(command, output)
|
|
255
|
+
: output;
|
|
256
|
+
if (!net)
|
|
257
|
+
return { text: annotated, net: null };
|
|
258
|
+
return { text: `${annotated}\n\n[sandbox] ${net.hint}`, net };
|
|
157
259
|
}
|
|
158
260
|
/** cwd must exist before spawn (guard borrowed from pi's local ops). */
|
|
159
261
|
export function assertSpawnableCwd(cwd) {
|
|
@@ -76,6 +76,12 @@ export declare function loadSandboxSettings(opts?: {
|
|
|
76
76
|
env?: NodeJS.ProcessEnv;
|
|
77
77
|
userHome?: string;
|
|
78
78
|
stateHomeOverride?: string | null;
|
|
79
|
+
/** When false, the project tier's network-widening knobs are dropped
|
|
80
|
+
* before the merge (a hostile repo's committed config cannot grant
|
|
81
|
+
* all-sockets / loopback / trustd). Defaults to true (trusted) — the
|
|
82
|
+
* callers that read the trust snapshot pass it explicitly; read-only
|
|
83
|
+
* surfaces (panel display, tests) can leave it unset. */
|
|
84
|
+
trusted?: boolean;
|
|
79
85
|
}): LoadedSandboxConfig;
|
|
80
86
|
export interface SandboxRuntimePaths {
|
|
81
87
|
cwd: string;
|
|
@@ -84,6 +90,9 @@ export interface SandboxRuntimePaths {
|
|
|
84
90
|
/** Project root; null outside a repo. */
|
|
85
91
|
projectRoot: string | null;
|
|
86
92
|
homeDir?: string;
|
|
93
|
+
/** Session env (drives the srt-forced-child tmpdir half of $TMPDIR
|
|
94
|
+
* expansion); defaults to process.env when absent. */
|
|
95
|
+
env?: NodeJS.ProcessEnv;
|
|
87
96
|
}
|
|
88
97
|
export interface SandboxRuntimeMerge {
|
|
89
98
|
network: SandboxNetworkSettings & {
|
|
@@ -23,6 +23,42 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
23
23
|
import { tmpdir } from "node:os";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { codeStateHome } from "../stateHome.js";
|
|
26
|
+
/** Network-widening keys that must never come from a non-trusted repo's
|
|
27
|
+
* committed config (the project tier). All four open real surface —
|
|
28
|
+
* unix-socket path entries admit local services, allowAllUnixSockets admits
|
|
29
|
+
* docker.sock (host access), allowLocalBinding opens every loopback service,
|
|
30
|
+
* enableWeakerNetworkIsolation opens the trustd exfiltration vector. The
|
|
31
|
+
* LOCAL tier is deliberately NOT gated: a local write already needs local
|
|
32
|
+
* write access, and Claude Code does not trust-gate settings.local.json
|
|
33
|
+
* either (documented in sandboxing.md). */
|
|
34
|
+
const NETWORK_WIDENING_KEYS = [
|
|
35
|
+
"allowUnixSockets",
|
|
36
|
+
"allowAllUnixSockets",
|
|
37
|
+
"allowLocalBinding",
|
|
38
|
+
];
|
|
39
|
+
/** Drop a tier's network-widening knobs (never call for the local tier). */
|
|
40
|
+
function stripNetworkWidening(settings) {
|
|
41
|
+
if (!settings)
|
|
42
|
+
return settings;
|
|
43
|
+
const network = settings.network ? { ...settings.network } : undefined;
|
|
44
|
+
let touched = false;
|
|
45
|
+
if (network) {
|
|
46
|
+
for (const k of NETWORK_WIDENING_KEYS) {
|
|
47
|
+
if (network[k] !== undefined) {
|
|
48
|
+
delete network[k];
|
|
49
|
+
touched = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const top = { ...settings };
|
|
54
|
+
if (settings.enableWeakerNetworkIsolation !== undefined) {
|
|
55
|
+
delete top.enableWeakerNetworkIsolation;
|
|
56
|
+
touched = true;
|
|
57
|
+
}
|
|
58
|
+
if (touched && network)
|
|
59
|
+
top.network = network;
|
|
60
|
+
return touched ? top : settings;
|
|
61
|
+
}
|
|
26
62
|
const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
27
63
|
"enabled",
|
|
28
64
|
"autoAllowBashIfSandboxed",
|
|
@@ -223,8 +259,17 @@ export function loadSandboxSettings(opts = {}) {
|
|
|
223
259
|
const projectPath = join(cwd, ".yagni-code", "config.json");
|
|
224
260
|
const localPath = join(cwd, ".yagni-code", "config.local.json");
|
|
225
261
|
const userSettings = readSandboxSettingsFromFile(userPath, warnings, unknownKeys);
|
|
226
|
-
const
|
|
262
|
+
const rawProjectSettings = readSandboxSettingsFromFile(projectPath, warnings, unknownKeys);
|
|
227
263
|
const localSettings = readSandboxSettingsFromFile(localPath, warnings, unknownKeys);
|
|
264
|
+
// Trust gate: a non-trusted repo's committed project config can set the
|
|
265
|
+
// network-widening knobs, which would silently open docker.sock /
|
|
266
|
+
// loopback / the trustd vector. Same posture as the permission-rule
|
|
267
|
+
// filtering: the local tier is personal (never gated), the project tier
|
|
268
|
+
// is only honored when the repo is trusted.
|
|
269
|
+
const projectSettings = opts.trusted === false ? stripNetworkWidening(rawProjectSettings) : rawProjectSettings;
|
|
270
|
+
if (opts.trusted === false && projectSettings !== rawProjectSettings) {
|
|
271
|
+
warnings.push("sandbox: network-widening keys ignored from project config (repo not trusted)");
|
|
272
|
+
}
|
|
228
273
|
const settings = {
|
|
229
274
|
enabled: localSettings?.enabled ?? projectSettings?.enabled ?? userSettings?.enabled ?? false,
|
|
230
275
|
autoAllowBashIfSandboxed: localSettings?.autoAllowBashIfSandboxed ??
|
|
@@ -279,6 +324,31 @@ function resolveSandboxFsPath(pattern, base) {
|
|
|
279
324
|
return pattern;
|
|
280
325
|
return expandHome(pattern, homeDir);
|
|
281
326
|
}
|
|
327
|
+
/** The temp dir srt FORCES onto every sandboxed child (sandbox-utils.js
|
|
328
|
+
* generateProxyEnvVars): CLAUDE_CODE_TMPDIR || CLAUDE_TMPDIR ||
|
|
329
|
+
* "/tmp/claude". The child's TMPDIR is NOT the extension host's tmpdir —
|
|
330
|
+
* tsx/node/test-runner children bind their IPC sockets HERE, so the
|
|
331
|
+
* socket allow must cover it (and the host dir, for tooling that reads
|
|
332
|
+
* the ambient TMPDIR). */
|
|
333
|
+
function srtForcedChildTmpdir(env) {
|
|
334
|
+
return env.CLAUDE_CODE_TMPDIR || env.CLAUDE_TMPDIR || "/tmp/claude";
|
|
335
|
+
}
|
|
336
|
+
/** allowUnixSockets entry resolution: the literal "$TMPDIR" marker expands
|
|
337
|
+
* to the TWO temp dirs a sandboxed session can have — the extension
|
|
338
|
+
* host's os.tmpdir() (env-honoring) AND the dir srt forces onto children
|
|
339
|
+
* (see srtForcedChildTmpdir). "$TMPDIR/..." prefixes expand against the
|
|
340
|
+
* HOST dir only (subpaths of the forced dir are an odd ask; keep them
|
|
341
|
+
* literal-precise). Anything else passes through verbatim (srt takes
|
|
342
|
+
* literal absolute paths). srt realpaths each entry into the SBPL subpath
|
|
343
|
+
* rule, so a /tmp symlink spelling composes at the OS level. */
|
|
344
|
+
function expandTmpDirEntries(entry, env) {
|
|
345
|
+
if (entry === "$TMPDIR") {
|
|
346
|
+
return [...new Set([tmpdir(), srtForcedChildTmpdir(env)])];
|
|
347
|
+
}
|
|
348
|
+
if (entry.startsWith("$TMPDIR/"))
|
|
349
|
+
return [join(tmpdir(), entry.slice("$TMPDIR/".length))];
|
|
350
|
+
return [entry];
|
|
351
|
+
}
|
|
282
352
|
let homeDirDefault = () => homeDirImpl();
|
|
283
353
|
import { homedir as homeDirImpl } from "node:os";
|
|
284
354
|
function homeDirDefaultFn() { return homeDirImpl(); }
|
|
@@ -360,6 +430,19 @@ export function mergeRulesIntoSandbox(settings, rules, base, worktreeGit) {
|
|
|
360
430
|
denyRead.add(resolveSandboxFsPath(p, base));
|
|
361
431
|
for (const p of settings.filesystem?.allowRead ?? [])
|
|
362
432
|
allowRead.add(resolveSandboxFsPath(p, base));
|
|
433
|
+
// allowUnixSockets: the portable "$TMPDIR" entry (what the Network tab's
|
|
434
|
+
// Engineering preset persists) expands to BOTH temp dirs a sandboxed session can have (the host's os.tmpdir() AND the dir srt forces onto children — they differ; see expandTmpDirEntries) —
|
|
435
|
+
// srt takes literal paths and emits one SBPL subpath rule per entry, so
|
|
436
|
+
// the expansion admits every random-named IPC socket a test runner binds
|
|
437
|
+
// there. Literal paths and ~/. entries pass through untouched (union
|
|
438
|
+
// semantics: a project/user entry plus the local "$TMPDIR" entry yield
|
|
439
|
+
// two paths). macOS-only surface (srt ignores the list on Linux, where
|
|
440
|
+
// seccomp cannot path-filter — the Linux preset uses the broader
|
|
441
|
+
// allowAllUnixSockets instead).
|
|
442
|
+
const expandEnv = base.env ?? process.env;
|
|
443
|
+
const allowUnixSockets = [
|
|
444
|
+
...new Set((settings.network?.allowUnixSockets ?? []).flatMap((e) => expandTmpDirEntries(e, expandEnv))),
|
|
445
|
+
];
|
|
363
446
|
// Linked-worktree git access: allow writes to the shared common git dir
|
|
364
447
|
// only (routine worktree git — index.lock, refs, objects — never writes
|
|
365
448
|
// outside it), and pin hooks + config read-only within the newly allowed
|
|
@@ -392,6 +475,7 @@ export function mergeRulesIntoSandbox(settings, rules, base, worktreeGit) {
|
|
|
392
475
|
...settings.network,
|
|
393
476
|
allowedDomains: [...allowedDomains],
|
|
394
477
|
deniedDomains: [...deniedDomains],
|
|
478
|
+
allowUnixSockets,
|
|
395
479
|
},
|
|
396
480
|
filesystem: {
|
|
397
481
|
allowRead: [...allowRead],
|
|
@@ -32,6 +32,9 @@ export interface SandboxSessionOptions {
|
|
|
32
32
|
rgPath?: string;
|
|
33
33
|
askHandler?: NetworkAskHandler | null;
|
|
34
34
|
events?: SandboxManagerEvents;
|
|
35
|
+
/** Trust snapshot for the network-widening config gate (project tier is
|
|
36
|
+
* dropped when false). session.ts sets it per trust-probe capture. */
|
|
37
|
+
trusted?: boolean;
|
|
35
38
|
}
|
|
36
39
|
export interface SandboxSessionState {
|
|
37
40
|
/** srt initialized and commands will be wrapped. */
|
|
@@ -65,6 +68,10 @@ export declare class YagniSandboxManager {
|
|
|
65
68
|
get settings(): SandboxSettings | null;
|
|
66
69
|
get dependencies(): SandboxDependencyStatus | null;
|
|
67
70
|
setAskHandler(handler: NetworkAskHandler | null): void;
|
|
71
|
+
/** Trust snapshot setter (session.ts's ctx probes run after
|
|
72
|
+
* construction). Read at merge time, so a trust flip applies to the next
|
|
73
|
+
* initialize/refresh without a reset. */
|
|
74
|
+
setTrusted(trusted: boolean): void;
|
|
68
75
|
/** The failure-trail wrapper handed to srt — exported for tests so the
|
|
69
76
|
* catch-and-log contract (fail closed + sink line) is observable without
|
|
70
77
|
* driving a real wrapped network connection. */
|
|
@@ -58,6 +58,12 @@ export class YagniSandboxManager {
|
|
|
58
58
|
setAskHandler(handler) {
|
|
59
59
|
this.askHandler = handler;
|
|
60
60
|
}
|
|
61
|
+
/** Trust snapshot setter (session.ts's ctx probes run after
|
|
62
|
+
* construction). Read at merge time, so a trust flip applies to the next
|
|
63
|
+
* initialize/refresh without a reset. */
|
|
64
|
+
setTrusted(trusted) {
|
|
65
|
+
this.opts.trusted = trusted;
|
|
66
|
+
}
|
|
61
67
|
/** The failure-trail wrapper handed to srt — exported for tests so the
|
|
62
68
|
* catch-and-log contract (fail closed + sink line) is observable without
|
|
63
69
|
* driving a real wrapped network connection. */
|
|
@@ -106,6 +112,7 @@ export class YagniSandboxManager {
|
|
|
106
112
|
env: this.opts.env,
|
|
107
113
|
userHome: this.opts.userHome,
|
|
108
114
|
stateHomeOverride: this.opts.stateHomeOverride,
|
|
115
|
+
trusted: this.opts.trusted,
|
|
109
116
|
});
|
|
110
117
|
this.surfaceConfigDiagnostics(diagnostics);
|
|
111
118
|
const worktreeGit = this.state.worktreeGit !== undefined
|
|
@@ -116,6 +123,7 @@ export class YagniSandboxManager {
|
|
|
116
123
|
userStateHome: this.opts.stateHomeOverride ?? codeStateHome(null, this.opts.env, this.opts.userHome),
|
|
117
124
|
projectRoot: this.opts.projectRoot ?? null,
|
|
118
125
|
homeDir: this.opts.userHome,
|
|
126
|
+
env: this.opts.env ?? process.env,
|
|
119
127
|
}, worktreeGit);
|
|
120
128
|
}
|
|
121
129
|
runtimeConfig(merge, settings) {
|
|
@@ -166,6 +174,7 @@ export class YagniSandboxManager {
|
|
|
166
174
|
env: this.opts.env,
|
|
167
175
|
userHome: this.opts.userHome,
|
|
168
176
|
stateHomeOverride: this.opts.stateHomeOverride,
|
|
177
|
+
trusted: this.opts.trusted,
|
|
169
178
|
});
|
|
170
179
|
// Same warning surfacing as buildRuntimeMerge — initialize() skips
|
|
171
180
|
// buildRuntimeMerge on the disabled path, so its own load reports too.
|
|
@@ -218,6 +227,7 @@ export class YagniSandboxManager {
|
|
|
218
227
|
env: this.opts.env,
|
|
219
228
|
userHome: this.opts.userHome,
|
|
220
229
|
stateHomeOverride: this.opts.stateHomeOverride,
|
|
230
|
+
trusted: this.opts.trusted,
|
|
221
231
|
}).settings));
|
|
222
232
|
}
|
|
223
233
|
/**
|
|
@@ -22,9 +22,12 @@ import { Container, type SelectItem } from "@earendil-works/pi-tui";
|
|
|
22
22
|
import { type SandboxRuntimeMerge, type SandboxRuntimePaths, type SandboxSettings } from "./config.js";
|
|
23
23
|
import type { RenderTheme } from "../subagentRender.js";
|
|
24
24
|
import type { PermissionRule } from "../permissionRules/loadConfig.js";
|
|
25
|
-
export type SandboxPanelTab = "mode" | "overrides" | "config" | "dependencies";
|
|
25
|
+
export type SandboxPanelTab = "mode" | "overrides" | "network" | "config" | "dependencies";
|
|
26
26
|
export type SandboxModeChoice = "auto-allow" | "regular" | "disabled";
|
|
27
27
|
export type SandboxOverrideChoice = "open" | "closed";
|
|
28
|
+
/** A Network-tab selection: the one-click Engineering preset (platform-
|
|
29
|
+
* shaped composition) or an individual knob toggle. */
|
|
30
|
+
export type SandboxNetworkChoice = "engineering-preset" | "engineering-preset-off" | "trustd" | "trustd-off" | "unix-sockets" | "unix-sockets-off" | "local-binding" | "local-binding-off";
|
|
28
31
|
/** Everything the panel needs, resolved by the caller (session.ts). */
|
|
29
32
|
export interface SandboxPanelState {
|
|
30
33
|
settings: SandboxSettings;
|
|
@@ -44,12 +47,16 @@ export interface SandboxPanelActions {
|
|
|
44
47
|
onModeSelect: (choice: SandboxModeChoice) => Promise<SandboxPanelOutcome>;
|
|
45
48
|
/** Persist an override choice. Same contract. */
|
|
46
49
|
onOverrideSelect: (choice: SandboxOverrideChoice) => Promise<SandboxPanelOutcome>;
|
|
50
|
+
/** Persist a network-posture choice (preset or single knob). Same
|
|
51
|
+
* contract; the knob state derives from the settings snapshot the panel
|
|
52
|
+
* was built with. */
|
|
53
|
+
onNetworkSelect: (choice: SandboxNetworkChoice) => Promise<SandboxPanelOutcome>;
|
|
47
54
|
}
|
|
48
55
|
/** What the panel hands back when it closes: the confirmation (or error)
|
|
49
56
|
* the session surfaces via notify once custom() resolves. */
|
|
50
57
|
export interface SandboxPanelOutcome {
|
|
51
58
|
text: string;
|
|
52
|
-
level: "info" | "error";
|
|
59
|
+
level: "info" | "error" | "warning";
|
|
53
60
|
}
|
|
54
61
|
/** Current mode derived from resolved settings (Claude's currentMode). */
|
|
55
62
|
export declare function deriveCurrentMode(settings: SandboxSettings, sessionToggledOff: boolean): SandboxModeChoice;
|
|
@@ -59,6 +66,28 @@ export declare function overrideOptions(current: SandboxOverrideChoice): SelectI
|
|
|
59
66
|
/** Per-mode explanation (Claude's copy, adapted to our surfaces). */
|
|
60
67
|
export declare function modeExplanation(mode: SandboxModeChoice): string;
|
|
61
68
|
export declare function overrideExplanation(choice: SandboxOverrideChoice): string;
|
|
69
|
+
/** The three knobs' resolved state, derived from settings (undefined =
|
|
70
|
+
* unset, which resolves OFF at the runtime). */
|
|
71
|
+
export interface NetworkKnobState {
|
|
72
|
+
trustd: boolean;
|
|
73
|
+
/** macOS: the $TMPDIR entry is present in allowUnixSockets; Linux:
|
|
74
|
+
* allowAllUnixSockets (path-scoped lists are macOS-only — seccomp cannot
|
|
75
|
+
* path-filter). */
|
|
76
|
+
unixSockets: boolean;
|
|
77
|
+
localBinding: boolean;
|
|
78
|
+
}
|
|
79
|
+
export declare function deriveNetworkKnobs(settings: SandboxSettings, platform: NodeJS.Platform): NetworkKnobState;
|
|
80
|
+
/** Does the platform-shape of the preset match the knobs already resolved
|
|
81
|
+
* on? Drives the preset row's "(applied)" label. Platform-shaped: on
|
|
82
|
+
* Linux the preset never sets trustd (macOS-only), so "applied" is
|
|
83
|
+
* sockets+loopback there; on darwin it is all three. */
|
|
84
|
+
export declare function presetApplied(knobs: NetworkKnobState, platform: NodeJS.Platform): boolean;
|
|
85
|
+
/** The literal settings block the preset persists per platform (the local
|
|
86
|
+
* tier; session.ts owns the write). Exported so session + tests share one
|
|
87
|
+
* composition — the panel never builds it ad hoc. */
|
|
88
|
+
export declare function engineeringPresetBlock(platform: NodeJS.Platform): SandboxSettings;
|
|
89
|
+
export declare function networkOptions(knobs: NetworkKnobState, platform: NodeJS.Platform): SelectItem[];
|
|
90
|
+
export declare function networkExplanation(item: SelectItem, platform: NodeJS.Platform): string;
|
|
62
91
|
/**
|
|
63
92
|
* Config tab sections, conditional like Claude's SandboxConfigTab: empty
|
|
64
93
|
* sections are omitted entirely (Excluded Commands — Claude's one always-on
|
|
@@ -70,7 +99,8 @@ export interface ConfigSection {
|
|
|
70
99
|
detail: string[];
|
|
71
100
|
}
|
|
72
101
|
export declare function configSections(state: SandboxPanelState): ConfigSection[];
|
|
73
|
-
/** Tabs offered, mirroring Claude
|
|
102
|
+
/** Tabs offered, mirroring Claude plus our Network tab (the deliberate
|
|
103
|
+
* deviation): Dependencies appears only on errors. */
|
|
74
104
|
export declare function panelTabs(dependencyErrors: string[]): SandboxPanelTab[];
|
|
75
105
|
/**
|
|
76
106
|
* The /sandbox panel. Structure: top border · tab strip · blank · tab
|
|
@@ -95,6 +125,7 @@ export declare class SandboxPanel extends Container {
|
|
|
95
125
|
private buildBody;
|
|
96
126
|
private buildTabContent;
|
|
97
127
|
private buildModeTab;
|
|
128
|
+
private buildNetworkTab;
|
|
98
129
|
private buildOverridesTab;
|
|
99
130
|
private buildConfigTab;
|
|
100
131
|
private buildDependenciesTab;
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { Box, Container, HStack, Key, SelectList, Text, matchesKey, } from "@earendil-works/pi-tui";
|
|
22
22
|
import { mergeRulesIntoSandbox } from "./config.js";
|
|
23
23
|
import { resolveWorktreeGitAccess } from "./worktreeGit.js";
|
|
24
|
+
import { logEvent } from "../errorSink.js";
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// Pure derivation helpers (unit-tested; no TUI dependency)
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
@@ -62,6 +63,106 @@ export function overrideExplanation(choice) {
|
|
|
62
63
|
? "When a command fails due to sandbox restrictions, the agent can retry with dangerouslyDisableSandbox to run outside the sandbox (falling back to default permissions)."
|
|
63
64
|
: "All bash commands invoked by the model must run in the sandbox unless they are explicitly listed in excludedCommands.";
|
|
64
65
|
}
|
|
66
|
+
export function deriveNetworkKnobs(settings, platform) {
|
|
67
|
+
const sockets = platform === "darwin"
|
|
68
|
+
? (settings.network?.allowUnixSockets ?? []).some((e) => e === "$TMPDIR" || e === "$TMPDIR/")
|
|
69
|
+
: settings.network?.allowAllUnixSockets === true;
|
|
70
|
+
return {
|
|
71
|
+
trustd: settings.enableWeakerNetworkIsolation === true,
|
|
72
|
+
unixSockets: sockets,
|
|
73
|
+
localBinding: settings.network?.allowLocalBinding === true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Does the platform-shape of the preset match the knobs already resolved
|
|
77
|
+
* on? Drives the preset row's "(applied)" label. Platform-shaped: on
|
|
78
|
+
* Linux the preset never sets trustd (macOS-only), so "applied" is
|
|
79
|
+
* sockets+loopback there; on darwin it is all three. */
|
|
80
|
+
export function presetApplied(knobs, platform) {
|
|
81
|
+
if (platform === "darwin")
|
|
82
|
+
return knobs.trustd && knobs.unixSockets && knobs.localBinding;
|
|
83
|
+
return knobs.unixSockets && knobs.localBinding;
|
|
84
|
+
}
|
|
85
|
+
/** The literal settings block the preset persists per platform (the local
|
|
86
|
+
* tier; session.ts owns the write). Exported so session + tests share one
|
|
87
|
+
* composition — the panel never builds it ad hoc. */
|
|
88
|
+
export function engineeringPresetBlock(platform) {
|
|
89
|
+
if (platform === "darwin") {
|
|
90
|
+
return {
|
|
91
|
+
enableWeakerNetworkIsolation: true,
|
|
92
|
+
network: { allowUnixSockets: ["$TMPDIR"], allowLocalBinding: true },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
network: { allowAllUnixSockets: true, allowLocalBinding: true },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function networkOptions(knobs, platform) {
|
|
100
|
+
const isMac = platform === "darwin";
|
|
101
|
+
const items = [
|
|
102
|
+
{
|
|
103
|
+
value: "engineering-preset",
|
|
104
|
+
label: `Engineering preset: tests + gh + localhost in one click${presetApplied(knobs, platform) ? " (applied)" : ""}`,
|
|
105
|
+
description: "Unix sockets in the temp dir (test-runner IPC), macOS TLS chain verification, and loopback connections — the routine engineering-work knobs",
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
if (isMac) {
|
|
109
|
+
items.push({
|
|
110
|
+
value: "trustd",
|
|
111
|
+
label: `macOS TLS verification via trustd: ${knobs.trustd ? "on" : "off"}`,
|
|
112
|
+
description: "Go CLIs (gh, gcloud, terraform) verify TLS through com.apple.trustd.agent; allowing it opens a potential data exfiltration vector through the trustd service",
|
|
113
|
+
});
|
|
114
|
+
items.push({
|
|
115
|
+
value: "unix-sockets",
|
|
116
|
+
label: `Unix sockets in $TMPDIR: ${knobs.unixSockets ? "on" : "off"}`,
|
|
117
|
+
description: "Admits test-runner IPC binds (tsx, vitest, node workers) inside the sandbox temp dir only",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
items.push({
|
|
122
|
+
value: "unix-sockets",
|
|
123
|
+
label: `All unix sockets: ${knobs.unixSockets ? "on" : "off"}`,
|
|
124
|
+
description: "seccomp cannot path-filter on Linux, so this is all-or-nothing — docker.sock included. Test-runner IPC needs it",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
items.push({
|
|
128
|
+
value: "local-binding",
|
|
129
|
+
label: `Localhost connections + binding: ${knobs.localBinding ? "on" : "off"}`,
|
|
130
|
+
description: "Loopback bypasses the domain allowlist (localhost services and dev servers/DBs become reachable without prompts)",
|
|
131
|
+
});
|
|
132
|
+
// Remove-entries ride the same list as -off variants: SelectItem values
|
|
133
|
+
// are matched literally by the onSelect handler.
|
|
134
|
+
if (knobs.trustd || knobs.unixSockets || knobs.localBinding) {
|
|
135
|
+
items.push({
|
|
136
|
+
value: "engineering-preset-off",
|
|
137
|
+
label: "Remove the preset knobs (set off in this project's local settings)",
|
|
138
|
+
description: "Writes explicit off values to .yagni-code/config.local.json; project/user entries stay as they are",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return items;
|
|
142
|
+
}
|
|
143
|
+
export function networkExplanation(item, platform) {
|
|
144
|
+
switch (item.value) {
|
|
145
|
+
case "engineering-preset":
|
|
146
|
+
return platform === "darwin"
|
|
147
|
+
? "Applies: unix sockets in $TMPDIR + trustd TLS verification + localhost connections. Held work (pnpm/tsx test runs, gh, psql to localhost) runs sandboxed instead of escaping. The trustd knob opens a potential data exfiltration vector through the trustd service — that tradeoff is yours to accept."
|
|
148
|
+
: "Applies: all unix sockets (no path filtering on Linux — docker.sock included) + localhost connections. Held work (test runs, local DB clients) runs sandboxed instead of escaping.";
|
|
149
|
+
case "trustd":
|
|
150
|
+
case "trustd-off":
|
|
151
|
+
return "Needed for Go TLS verification (gh, gcloud, terraform). Enabling opens a potential data exfiltration vector through the trustd service — off by default.";
|
|
152
|
+
case "unix-sockets":
|
|
153
|
+
case "unix-sockets-off":
|
|
154
|
+
return platform === "darwin"
|
|
155
|
+
? "Allows binding unix sockets under $TMPDIR only — every tsx/vitest/node test-runner IPC socket lives there. Writes to $TMPDIR are already sandbox-allowed, so this stays low-risk."
|
|
156
|
+
: "All-or-nothing on Linux: seccomp cannot filter socket paths, so allowing test-runner IPC also allows docker.sock. Off by default; excludedCommands can keep specific commands out of the sandbox instead.";
|
|
157
|
+
case "local-binding":
|
|
158
|
+
case "local-binding-off":
|
|
159
|
+
return "psql/redis to localhost, dev servers on :4567, and any 127.0.0.1 client. Loopback bypasses the per-domain allowlist — this is all-or-nothing for localhost.";
|
|
160
|
+
case "engineering-preset-off":
|
|
161
|
+
return "Writes explicit OFF values to this project's local settings — local wins over project/user on scalars, so a knob set higher up stops applying here. The $TMPDIR socket entry is removed from the local list. Entries committed to project or user config are NOT changed — edit those files directly to turn a knob back on there.";
|
|
162
|
+
default:
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
65
166
|
export function configSections(state) {
|
|
66
167
|
const { settings, merge } = state;
|
|
67
168
|
const sections = [];
|
|
@@ -93,14 +194,24 @@ export function configSections(state) {
|
|
|
93
194
|
const unixSockets = settings.network?.allowUnixSockets ?? [];
|
|
94
195
|
if (unixSockets.length > 0)
|
|
95
196
|
sections.push({ title: "Allowed Unix Sockets", detail: [unixSockets.join(", ")] });
|
|
197
|
+
if (merge.network.allowLocalBinding) {
|
|
198
|
+
sections.push({ title: "Local Binding", detail: ["loopback + private ranges allowed (localhost clients, dev servers, DB tunnels)"] });
|
|
199
|
+
}
|
|
200
|
+
if (settings.network?.allowAllUnixSockets) {
|
|
201
|
+
sections.push({ title: "All Unix Sockets", detail: ["allowed (Linux posture — includes docker.sock)"] });
|
|
202
|
+
}
|
|
203
|
+
if (settings.enableWeakerNetworkIsolation) {
|
|
204
|
+
sections.push({ title: "Weaker Network Isolation", detail: ["trustd.agent allowed (Go TLS verification; opens a trustd exfiltration vector)"] });
|
|
205
|
+
}
|
|
96
206
|
if (state.sessionGrants.length > 0) {
|
|
97
207
|
sections.push({ title: "Session Grants (memory-only)", detail: [state.sessionGrants.join(", ")] });
|
|
98
208
|
}
|
|
99
209
|
return sections;
|
|
100
210
|
}
|
|
101
|
-
/** Tabs offered, mirroring Claude
|
|
211
|
+
/** Tabs offered, mirroring Claude plus our Network tab (the deliberate
|
|
212
|
+
* deviation): Dependencies appears only on errors. */
|
|
102
213
|
export function panelTabs(dependencyErrors) {
|
|
103
|
-
const tabs = ["mode", "overrides", "config"];
|
|
214
|
+
const tabs = ["mode", "overrides", "network", "config"];
|
|
104
215
|
if (dependencyErrors.length > 0)
|
|
105
216
|
tabs.push("dependencies");
|
|
106
217
|
return tabs;
|
|
@@ -111,6 +222,7 @@ export function panelTabs(dependencyErrors) {
|
|
|
111
222
|
const TAB_LABELS = {
|
|
112
223
|
mode: "Mode",
|
|
113
224
|
overrides: "Overrides",
|
|
225
|
+
network: "Network",
|
|
114
226
|
config: "Config",
|
|
115
227
|
dependencies: "Dependencies",
|
|
116
228
|
};
|
|
@@ -201,6 +313,8 @@ export class SandboxPanel extends Container {
|
|
|
201
313
|
return this.buildModeTab();
|
|
202
314
|
case "overrides":
|
|
203
315
|
return this.buildOverridesTab();
|
|
316
|
+
case "network":
|
|
317
|
+
return this.buildNetworkTab();
|
|
204
318
|
case "config":
|
|
205
319
|
return this.buildConfigTab();
|
|
206
320
|
case "dependencies":
|
|
@@ -240,6 +354,51 @@ export class SandboxPanel extends Container {
|
|
|
240
354
|
wrap.addChild(explanation);
|
|
241
355
|
return wrap;
|
|
242
356
|
}
|
|
357
|
+
buildNetworkTab() {
|
|
358
|
+
const t = this.theme;
|
|
359
|
+
if (!this.state.settings.enabled) {
|
|
360
|
+
this.activeList = null;
|
|
361
|
+
return new Text(t.fg("muted", "Sandbox is not enabled. Enable sandbox to configure network posture."), 0, 0);
|
|
362
|
+
}
|
|
363
|
+
const knobs = deriveNetworkKnobs(this.state.settings, process.platform);
|
|
364
|
+
const options = networkOptions(knobs, process.platform);
|
|
365
|
+
const list = new SelectList(options, 10, selectListTheme(t));
|
|
366
|
+
const explanation = new Text(t.fg("muted", networkExplanation(options[0], process.platform)), 0, 1);
|
|
367
|
+
list.onSelectionChange = (item) => {
|
|
368
|
+
explanation.setText(t.fg("muted", networkExplanation(item, process.platform)));
|
|
369
|
+
this.invalidate();
|
|
370
|
+
};
|
|
371
|
+
list.onSelect = (item) => {
|
|
372
|
+
void this.actions
|
|
373
|
+
.onNetworkSelect(item.value)
|
|
374
|
+
.then((outcome) => this.done(outcome))
|
|
375
|
+
// A rejection AFTER a successful persist (e.g. load() or
|
|
376
|
+
// refreshConfig blew up) must not close the panel silently — the
|
|
377
|
+
// write may have landed while the live refresh did not, so the
|
|
378
|
+
// user gets the honest message AND the trail gets the reason
|
|
379
|
+
// (error class only; the thrown message is never scrubbed into
|
|
380
|
+
// the sink — same posture as sandbox_persist_failed).
|
|
381
|
+
.catch((err) => {
|
|
382
|
+
logEvent({
|
|
383
|
+
source: "sandbox",
|
|
384
|
+
level: "warn",
|
|
385
|
+
event: "sandbox_refresh_failed",
|
|
386
|
+
fields: { error: err instanceof Error ? err.constructor.name : typeof err },
|
|
387
|
+
});
|
|
388
|
+
this.done({
|
|
389
|
+
text: "Network setting saved, but applying it to the live session failed — restart the session (or /new) to make it take effect.",
|
|
390
|
+
level: "warning",
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
list.onCancel = () => this.done();
|
|
395
|
+
this.activeList = list;
|
|
396
|
+
const wrap = new Container();
|
|
397
|
+
wrap.addChild(new Text(t.bold("Network posture for engineering work:"), 0, 0));
|
|
398
|
+
wrap.addChild(list);
|
|
399
|
+
wrap.addChild(explanation);
|
|
400
|
+
return wrap;
|
|
401
|
+
}
|
|
243
402
|
buildOverridesTab() {
|
|
244
403
|
const t = this.theme;
|
|
245
404
|
if (!this.state.settings.enabled) {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { type SandboxSettings } from "./config.js";
|
|
16
16
|
import { YagniSandboxManager } from "./manager.js";
|
|
17
|
+
import { type SandboxNetworkChoice } from "./panel.js";
|
|
17
18
|
import type { PermissionRule } from "../permissionRules/loadConfig.js";
|
|
18
19
|
/**
|
|
19
20
|
* Build the sandbox bash composition: given ANY stock bash definition (pi's
|
|
@@ -63,7 +64,7 @@ export interface RegisterSandboxOptions {
|
|
|
63
64
|
stateHomeOverride?: string | null;
|
|
64
65
|
projectRoot?: string | null;
|
|
65
66
|
rgPath?: string;
|
|
66
|
-
}) => Pick<YagniSandboxManager, "initialized" | "initialize" | "reset" | "refreshConfig" | "setAskHandler" | "wrapWithSandbox" | "cleanupAfterCommand" | "annotateStderrWithSandboxFailures"> & YagniSandboxManager;
|
|
67
|
+
}) => Pick<YagniSandboxManager, "initialized" | "initialize" | "reset" | "refreshConfig" | "setAskHandler" | "setTrusted" | "wrapWithSandbox" | "cleanupAfterCommand" | "annotateStderrWithSandboxFailures"> & YagniSandboxManager;
|
|
67
68
|
/** Whether interactive prompts are possible (TUI/RPC). */
|
|
68
69
|
hasUI: (ctx: ExtensionContext) => boolean;
|
|
69
70
|
/**
|
|
@@ -82,4 +83,16 @@ export interface RegisterSandboxOptions {
|
|
|
82
83
|
* sandbox (no-op registration, stock bash stays).
|
|
83
84
|
*/
|
|
84
85
|
export declare function registerSandbox(pi: ExtensionAPI, opts: RegisterSandboxOptions): SandboxSessionHandle | null;
|
|
86
|
+
/** Persist a Network-tab choice (the Engineering preset or a single knob
|
|
87
|
+
* toggle) to the PROJECT-LOCAL config — same destination + honesty contract
|
|
88
|
+
* as the other panel writes. The preset composes the platform-shaped block
|
|
89
|
+
* from panel.ts (one source of truth for the composition) and NORMALIZES
|
|
90
|
+
* the other platform's knobs off (a stale broader door — or a stale trustd
|
|
91
|
+
* value on Linux — never survives a preset apply silently); knob toggles
|
|
92
|
+
* write explicit off values so local precedence overrides any project/user
|
|
93
|
+
* on-state. The $TMPDIR socket entry is REMOVED from the local array (not
|
|
94
|
+
* falsed — arrays have no false), so un-applying the preset leaves the
|
|
95
|
+
* local tier clean; project/user entries keep applying by union. Exported
|
|
96
|
+
* for tests (the per-platform branches are pinned directly). */
|
|
97
|
+
export declare function persistNetworkChoice(choice: SandboxNetworkChoice, cwd: string, platform?: NodeJS.Platform): void;
|
|
85
98
|
//# sourceMappingURL=session.d.ts.map
|
|
@@ -23,9 +23,9 @@ import { isDebug } from "../diagnostics.js";
|
|
|
23
23
|
import { mutateConfigJson, mutateLocalConfig } from "../settingsFiles.js";
|
|
24
24
|
import { loadSandboxSettings } from "./config.js";
|
|
25
25
|
import { resolveWorktreeGitAccess } from "./worktreeGit.js";
|
|
26
|
-
import { annotateCommandOutput, makeSandboxSpawnHook, preWrappedCommand, shouldUseSandbox, shouldUseSandboxForUserCommand, } from "./bash.js";
|
|
26
|
+
import { annotateCommandOutput, makeSandboxSpawnHook, networkDenialHint, preWrappedCommand, shouldUseSandbox, shouldUseSandboxForUserCommand, } from "./bash.js";
|
|
27
27
|
import { YagniSandboxManager } from "./manager.js";
|
|
28
|
-
import { SandboxPanel, buildPanelState } from "./panel.js";
|
|
28
|
+
import { SandboxPanel, buildPanelState, engineeringPresetBlock } from "./panel.js";
|
|
29
29
|
import { effectiveRules } from "../permissionRules/loadConfig.js";
|
|
30
30
|
/**
|
|
31
31
|
* Build the sandbox bash composition: given ANY stock bash definition (pi's
|
|
@@ -68,7 +68,20 @@ export function makeBashComposition(manager, settings, cwd) {
|
|
|
68
68
|
restrictions.push(allowed.length > 0
|
|
69
69
|
? `Network: only these domains (wildcards ok): ${allowed.join(", ")}`
|
|
70
70
|
: "Network: no domains pre-allowed — the first contact to each host prompts the user");
|
|
71
|
-
restrictions.push(`Filesystem writes: working directory,
|
|
71
|
+
restrictions.push(`Filesystem writes: working directory, the session temp dir ($TMPDIR), and paths granted by Edit(...) allow rules — /tmp itself is NOT writable; only the per-user temp dir is`);
|
|
72
|
+
if (s.network?.allowLocalBinding) {
|
|
73
|
+
restrictions.push("Localhost: connections and binds to 127.0.0.1/localhost are allowed (local DBs, dev servers, tunnels) — loopback bypasses the domain allowlist by design");
|
|
74
|
+
}
|
|
75
|
+
const hasTmpSockets = (s.network?.allowUnixSockets ?? []).some((e) => e === "$TMPDIR" || e === "$TMPDIR/");
|
|
76
|
+
if (s.network?.allowAllUnixSockets) {
|
|
77
|
+
restrictions.push("Unix sockets: ALL allowed (Linux posture — includes docker.sock; test runners and IPC clients work)");
|
|
78
|
+
}
|
|
79
|
+
else if (hasTmpSockets) {
|
|
80
|
+
restrictions.push("Unix sockets in the temp dir ($TMPDIR): allowed — test runners (tsx, vitest, node --test with process isolation) and other IPC that binds there work sandboxed");
|
|
81
|
+
}
|
|
82
|
+
if (s.enableWeakerNetworkIsolation) {
|
|
83
|
+
restrictions.push("macOS TLS verification (trustd): allowed — Go CLIs (gh, gcloud, terraform) can verify certificates against allowlisted domains");
|
|
84
|
+
}
|
|
72
85
|
// A linked-worktree session additionally allows the shared common git
|
|
73
86
|
// dir (git operations work sandboxed there); the description must say
|
|
74
87
|
// so or the model under-reports what it can do.
|
|
@@ -80,9 +93,14 @@ export function makeBashComposition(manager, settings, cwd) {
|
|
|
80
93
|
const strictNote = s.allowUnsandboxedCommands === false
|
|
81
94
|
? "All commands MUST run sandboxed — dangerouslyDisableSandbox is disabled by policy."
|
|
82
95
|
: "Default to running commands in the sandbox. Set dangerouslyDisableSandbox: true ONLY with evidence of a sandbox-caused failure (\"Operation not permitted\", denied path/host, unix-socket errors) — the retry still goes through the normal permission flow. The user can adjust restrictions with /sandbox.";
|
|
96
|
+
// Heredoc caveat (verified live): macOS bash 3.2 writes heredoc temp
|
|
97
|
+
// files relative to the CURRENT DIRECTORY, not TMPDIR — so heredocs
|
|
98
|
+
// fail once cwd drifts outside the writable roots. Stated once, after
|
|
99
|
+
// the restrictions, so the model doesn't reach for it blindly.
|
|
100
|
+
const heredocNote = "Caveat: heredocs (<<EOF) write temp files relative to the current directory on macOS bash 3.2 — from an unwritable cwd they fail; prefer printf or redirect from a file in the working directory.";
|
|
83
101
|
const wrapped = {
|
|
84
102
|
...def,
|
|
85
|
-
description: `${def.description}\n\n## Command sandbox\nCommands run inside an OS sandbox: ${restrictions.join("; ")}. ${strictNote}`,
|
|
103
|
+
description: `${def.description}\n\n## Command sandbox\nCommands run inside an OS sandbox: ${restrictions.join("; ")}. ${strictNote}\n${heredocNote}`,
|
|
86
104
|
parameters: schema,
|
|
87
105
|
async execute(id, params, signal, onUpdate, ctx) {
|
|
88
106
|
const input = params;
|
|
@@ -103,9 +121,14 @@ export function makeBashComposition(manager, settings, cwd) {
|
|
|
103
121
|
result = await sandboxBash.execute(id, execParams, signal, onUpdate, ctx);
|
|
104
122
|
}
|
|
105
123
|
catch (err) {
|
|
106
|
-
if (err instanceof Error && err.message.includes("Operation not permitted")) {
|
|
124
|
+
if (err instanceof Error && (err.message.includes("Operation not permitted") || networkDenialHint(err.message))) {
|
|
125
|
+
// The thrown-message gate matches the result-text gate: a
|
|
126
|
+
// network-signature denial in EITHER surface gets the
|
|
127
|
+
// annotation + hint (node throws lowercase "operation not
|
|
128
|
+
// permitted", which the bare substring misses).
|
|
107
129
|
const annotated = annotateCommandOutput(manager, input.command, err.message);
|
|
108
|
-
|
|
130
|
+
logNetworkDenialHint(annotated.net);
|
|
131
|
+
throw new Error(annotated.text);
|
|
109
132
|
}
|
|
110
133
|
throw err;
|
|
111
134
|
}
|
|
@@ -114,9 +137,10 @@ export function makeBashComposition(manager, settings, cwd) {
|
|
|
114
137
|
}
|
|
115
138
|
if (result?.content) {
|
|
116
139
|
const text = result.content.map((c) => (c.type === "text" ? c.text : "")).join("\n");
|
|
117
|
-
if (text.includes("Operation not permitted")) {
|
|
140
|
+
if (text.includes("Operation not permitted") || networkDenialHint(text)) {
|
|
118
141
|
const annotated = annotateCommandOutput(manager, input.command, text);
|
|
119
|
-
|
|
142
|
+
logNetworkDenialHint(annotated.net);
|
|
143
|
+
result = { ...result, content: [{ type: "text", text: annotated.text }] };
|
|
120
144
|
}
|
|
121
145
|
}
|
|
122
146
|
return result;
|
|
@@ -198,6 +222,7 @@ export function registerSandbox(pi, opts) {
|
|
|
198
222
|
trustedSnapshot = ctx.isProjectTrusted();
|
|
199
223
|
}
|
|
200
224
|
catch (err) {
|
|
225
|
+
void err;
|
|
201
226
|
// Keep the prior snapshot (fail-open parity with the gate) but make
|
|
202
227
|
// the throw VISIBLE: warn (not debug) — readSessionTrail filters debug
|
|
203
228
|
// lines, so a debug event would never reach /feedback; a trust-probe
|
|
@@ -213,6 +238,13 @@ export function registerSandbox(pi, opts) {
|
|
|
213
238
|
};
|
|
214
239
|
/** Rules filtered to what the sandbox may enforce OS-wide (trust-aware). */
|
|
215
240
|
const effectiveRulesForSandbox = () => effectiveRules(rules, trustedSnapshot);
|
|
241
|
+
// The network-widening config gate (project-tier knobs dropped when the
|
|
242
|
+
// repo is untrusted) reads the SAME snapshot — applied to the manager on
|
|
243
|
+
// every ctx capture (session_start AND tool_call) so a mid-session trust
|
|
244
|
+
// flip reaches the config gate alongside the rule gate without a reset.
|
|
245
|
+
const applyTrustToManager = () => {
|
|
246
|
+
manager.setTrusted(trustedSnapshot);
|
|
247
|
+
};
|
|
216
248
|
const sessionDomainGrants = new Set();
|
|
217
249
|
const askHandler = async (host) => {
|
|
218
250
|
if (sessionDomainGrants.has(host))
|
|
@@ -335,8 +367,19 @@ export function registerSandbox(pi, opts) {
|
|
|
335
367
|
if (timeout !== undefined && timeout > 0) {
|
|
336
368
|
timeoutHandle = setTimeout(() => { timedOut = true; killGroup(); }, timeout * 1000);
|
|
337
369
|
}
|
|
338
|
-
|
|
339
|
-
child
|
|
370
|
+
// Tail buffer (bounded) so a network-posture denial can be
|
|
371
|
+
// annotated + hinted after the child exits — the tool-call path
|
|
372
|
+
// annotates via annotateCommandOutput; the !-command path owns its
|
|
373
|
+
// own composition here (the classifier + hint ride the last 8KB).
|
|
374
|
+
let tail = "";
|
|
375
|
+
const MAX_TAIL = 8 * 1024;
|
|
376
|
+
const onChunk = (d) => {
|
|
377
|
+
const text = d.toString();
|
|
378
|
+
tail = (tail + text).slice(-MAX_TAIL);
|
|
379
|
+
onData(d);
|
|
380
|
+
};
|
|
381
|
+
child.stdout?.on("data", onChunk);
|
|
382
|
+
child.stderr?.on("data", onChunk);
|
|
340
383
|
signal?.addEventListener("abort", killGroup, { once: true });
|
|
341
384
|
try {
|
|
342
385
|
const exitCode = await waitForChildProcess(child);
|
|
@@ -344,6 +387,13 @@ export function registerSandbox(pi, opts) {
|
|
|
344
387
|
throw new Error("aborted");
|
|
345
388
|
if (timedOut)
|
|
346
389
|
throw new Error(`timeout:${timeout}`);
|
|
390
|
+
// Post-run hint: same advisory as the tool path, appended after
|
|
391
|
+
// the child's own output so the user sees the fix pointer inline.
|
|
392
|
+
const net = networkDenialHint(tail);
|
|
393
|
+
if (net) {
|
|
394
|
+
logEvent({ source: "sandbox", level: "info", event: "network_denial_hint", fields: { class: net.cls } });
|
|
395
|
+
onData(Buffer.from(`\n[sandbox] ${net.hint}`));
|
|
396
|
+
}
|
|
347
397
|
return { exitCode };
|
|
348
398
|
}
|
|
349
399
|
finally {
|
|
@@ -362,11 +412,15 @@ export function registerSandbox(pi, opts) {
|
|
|
362
412
|
pi.on("tool_call", async (_event, ctx) => {
|
|
363
413
|
captureUi(ctx);
|
|
364
414
|
captureTrust(ctx);
|
|
415
|
+
// Mid-session trust flips must reach the manager's CONFIG gate too, not
|
|
416
|
+
// just the rule gate — applyTrustToManager is cheap (a boolean set).
|
|
417
|
+
applyTrustToManager();
|
|
365
418
|
return;
|
|
366
419
|
});
|
|
367
420
|
pi.on("session_start", async (_event, ctx) => {
|
|
368
421
|
captureUi(ctx);
|
|
369
422
|
captureTrust(ctx);
|
|
423
|
+
applyTrustToManager();
|
|
370
424
|
currentSettings = load();
|
|
371
425
|
// --no-sandbox skips init in EVERY mode — headless included. The notify
|
|
372
426
|
// is UI-gated, the SKIP never is (a UI-only skip would silently keep the
|
|
@@ -418,6 +472,15 @@ export function registerSandbox(pi, opts) {
|
|
|
418
472
|
lines.push(`deniedDomains: ${s.network.deniedDomains.join(", ")}`);
|
|
419
473
|
if (s.network?.allowLocalBinding)
|
|
420
474
|
lines.push("allowLocalBinding: yes (loopback + private ranges)");
|
|
475
|
+
const tmpSocket = (s.network?.allowUnixSockets ?? []).some((e) => e === "$TMPDIR" || e === "$TMPDIR/");
|
|
476
|
+
if (s.network?.allowAllUnixSockets)
|
|
477
|
+
lines.push("allowAllUnixSockets: yes (ALL unix sockets — includes docker.sock)");
|
|
478
|
+
else if (s.network?.allowUnixSockets?.length)
|
|
479
|
+
lines.push(`allowUnixSockets: ${s.network.allowUnixSockets.join(", ")}`);
|
|
480
|
+
if (tmpSocket)
|
|
481
|
+
lines.push(" (the $TMPDIR entry covers test-runner IPC sockets — the Engineering preset's sockets knob)");
|
|
482
|
+
if (s.enableWeakerNetworkIsolation)
|
|
483
|
+
lines.push("enableWeakerNetworkIsolation: yes (trustd allowed — Go TLS verification; opens a trustd exfiltration vector)");
|
|
421
484
|
if (s.filesystem?.allowWrite?.length)
|
|
422
485
|
lines.push(`allowWrite: ${s.filesystem.allowWrite.join(", ")}`);
|
|
423
486
|
if (s.filesystem?.denyWrite?.length)
|
|
@@ -608,6 +671,50 @@ export function registerSandbox(pi, opts) {
|
|
|
608
671
|
level: "info",
|
|
609
672
|
};
|
|
610
673
|
},
|
|
674
|
+
onNetworkSelect: async (choice) => {
|
|
675
|
+
// Knob changes apply LIVE (refreshConfig swaps the runtime config
|
|
676
|
+
// and srt reads the knobs at wrap time per command) — no restart.
|
|
677
|
+
// The bash DESCRIPTION stays stale until restart (load-time text);
|
|
678
|
+
// the denial hint is the mid-session discovery path. Stated in the
|
|
679
|
+
// outcome so the user knows the exact boundary.
|
|
680
|
+
try {
|
|
681
|
+
persistNetworkChoice(choice, opts.cwd);
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
// Same diagnosability posture as sandbox_persist_failed: the
|
|
685
|
+
// thrown message is mutateConfigJson's own path/reason text —
|
|
686
|
+
// surfaced, never silently swallowed.
|
|
687
|
+
logEvent({
|
|
688
|
+
source: "sandbox",
|
|
689
|
+
level: "warn",
|
|
690
|
+
event: "sandbox_persist_failed",
|
|
691
|
+
fields: {
|
|
692
|
+
kind: "network",
|
|
693
|
+
choice,
|
|
694
|
+
error: err instanceof Error ? err.message : String(err),
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
return {
|
|
698
|
+
text: `Could not write the network setting to .yagni-code/config.local.json — edit it directly (see /sandbox docs for the keys) and restart.`,
|
|
699
|
+
level: "error",
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
currentSettings = load();
|
|
703
|
+
if (manager.initialized)
|
|
704
|
+
manager.refreshConfig(effectiveRulesForSandbox());
|
|
705
|
+
const applied = choice === "engineering-preset";
|
|
706
|
+
const detail = choice === "engineering-preset"
|
|
707
|
+
? "tests, gh, and localhost clients now run sandboxed (live — no restart needed; the bash tool's description updates on the next session)"
|
|
708
|
+
: choice === "engineering-preset-off"
|
|
709
|
+
? "explicit off values written to this project's local settings (local wins over project/user, so higher-tier knob values stop applying); files at other tiers are unchanged"
|
|
710
|
+
: "saved to project-local settings and applied live";
|
|
711
|
+
return {
|
|
712
|
+
text: applied
|
|
713
|
+
? `✓ Engineering preset applied — ${detail}`
|
|
714
|
+
: `✓ Network setting — ${detail}`,
|
|
715
|
+
level: "info",
|
|
716
|
+
};
|
|
717
|
+
},
|
|
611
718
|
};
|
|
612
719
|
let outcome;
|
|
613
720
|
try {
|
|
@@ -674,6 +781,17 @@ export function registerSandbox(pi, opts) {
|
|
|
674
781
|
function isPlainRecord(v) {
|
|
675
782
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
676
783
|
}
|
|
784
|
+
/** Sink line when the network-posture classifier fires — the impact
|
|
785
|
+
* signal (paired with tool_execute_decision's useSandbox it makes the
|
|
786
|
+
* escape-rate before/after readable from the trail). Closed enum class,
|
|
787
|
+
* no command content. Takes the already-computed classification (the
|
|
788
|
+
* classifier runs ONCE per denial — at the annotation site) so the logged
|
|
789
|
+
* class can never disagree with the hint the user saw. */
|
|
790
|
+
function logNetworkDenialHint(net) {
|
|
791
|
+
if (!net)
|
|
792
|
+
return;
|
|
793
|
+
logEvent({ source: "sandbox", level: "info", event: "network_denial_hint", fields: { class: net.cls } });
|
|
794
|
+
}
|
|
677
795
|
/** Sink line for a failed domain-grant persist — the thrown message is
|
|
678
796
|
* mutateConfigJson's own path/reason text (diagnosable, no file content),
|
|
679
797
|
* same posture as sandbox_persist_failed / rule_save_failed. */
|
|
@@ -751,6 +869,82 @@ function persistSandboxOverride(choice, cwd) {
|
|
|
751
869
|
config.sandbox = sandbox;
|
|
752
870
|
}, "sandbox");
|
|
753
871
|
}
|
|
872
|
+
/** Persist a Network-tab choice (the Engineering preset or a single knob
|
|
873
|
+
* toggle) to the PROJECT-LOCAL config — same destination + honesty contract
|
|
874
|
+
* as the other panel writes. The preset composes the platform-shaped block
|
|
875
|
+
* from panel.ts (one source of truth for the composition) and NORMALIZES
|
|
876
|
+
* the other platform's knobs off (a stale broader door — or a stale trustd
|
|
877
|
+
* value on Linux — never survives a preset apply silently); knob toggles
|
|
878
|
+
* write explicit off values so local precedence overrides any project/user
|
|
879
|
+
* on-state. The $TMPDIR socket entry is REMOVED from the local array (not
|
|
880
|
+
* falsed — arrays have no false), so un-applying the preset leaves the
|
|
881
|
+
* local tier clean; project/user entries keep applying by union. Exported
|
|
882
|
+
* for tests (the per-platform branches are pinned directly). */
|
|
883
|
+
export function persistNetworkChoice(choice, cwd, platform = process.platform) {
|
|
884
|
+
mutateLocalConfig(cwd, (config) => {
|
|
885
|
+
const sandbox = isPlainRecord(config.sandbox) ? { ...config.sandbox } : {};
|
|
886
|
+
const network = isPlainRecord(sandbox.network) ? { ...sandbox.network } : {};
|
|
887
|
+
const sockets = isPlainRecord(network) ? (Array.isArray(network.allowUnixSockets) ? [...network.allowUnixSockets] : []) : [];
|
|
888
|
+
/** Copy-on-write: set/remove the $TMPDIR entry in the local socket
|
|
889
|
+
* array (arrays have no false — removal, not a value write). */
|
|
890
|
+
const setTmpdirSockets = (on) => {
|
|
891
|
+
const withoutTmp = sockets.filter((e) => e !== "$TMPDIR" && e !== "$TMPDIR/");
|
|
892
|
+
network.allowUnixSockets = on ? [...withoutTmp, "$TMPDIR"] : withoutTmp;
|
|
893
|
+
};
|
|
894
|
+
switch (choice) {
|
|
895
|
+
case "engineering-preset": {
|
|
896
|
+
const block = engineeringPresetBlock(platform);
|
|
897
|
+
if (block.enableWeakerNetworkIsolation !== undefined) {
|
|
898
|
+
sandbox.enableWeakerNetworkIsolation = true;
|
|
899
|
+
}
|
|
900
|
+
else if (sandbox.enableWeakerNetworkIsolation !== undefined) {
|
|
901
|
+
// Linux: the preset never touches trustd — a pre-existing local
|
|
902
|
+
// trustd value is NORMALIZED OFF (never silently deleted), so the
|
|
903
|
+
// local file keeps saying what it means.
|
|
904
|
+
sandbox.enableWeakerNetworkIsolation = false;
|
|
905
|
+
}
|
|
906
|
+
if (block.network?.allowUnixSockets) {
|
|
907
|
+
setTmpdirSockets(true);
|
|
908
|
+
// Normalize a pre-existing broader door OFF: the macOS preset is
|
|
909
|
+
// the NARROW shape, and a stale allowAllUnixSockets:true would
|
|
910
|
+
// keep docker.sock open while the panel shows the narrow preset.
|
|
911
|
+
network.allowAllUnixSockets = false;
|
|
912
|
+
}
|
|
913
|
+
if (block.network?.allowAllUnixSockets)
|
|
914
|
+
network.allowAllUnixSockets = true;
|
|
915
|
+
network.allowLocalBinding = true;
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
case "engineering-preset-off":
|
|
919
|
+
sandbox.enableWeakerNetworkIsolation = false;
|
|
920
|
+
network.allowAllUnixSockets = false;
|
|
921
|
+
network.allowLocalBinding = false;
|
|
922
|
+
setTmpdirSockets(false);
|
|
923
|
+
break;
|
|
924
|
+
case "trustd":
|
|
925
|
+
case "trustd-off":
|
|
926
|
+
sandbox.enableWeakerNetworkIsolation = choice === "trustd";
|
|
927
|
+
break;
|
|
928
|
+
case "unix-sockets":
|
|
929
|
+
case "unix-sockets-off": {
|
|
930
|
+
const on = choice === "unix-sockets";
|
|
931
|
+
if (platform === "darwin") {
|
|
932
|
+
setTmpdirSockets(on);
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
network.allowAllUnixSockets = on;
|
|
936
|
+
}
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
case "local-binding":
|
|
940
|
+
case "local-binding-off":
|
|
941
|
+
network.allowLocalBinding = choice === "local-binding";
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
sandbox.network = network;
|
|
945
|
+
config.sandbox = sandbox;
|
|
946
|
+
}, "sandbox");
|
|
947
|
+
}
|
|
754
948
|
/**
|
|
755
949
|
* Wait for a spawned child's exit without hanging on inherited stdio —
|
|
756
950
|
* after exit, release the pipes after a short idle grace so a detached
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.0-staging.
|
|
3
|
+
"version": "1.1.0-staging.1327.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "82e201e640d0f201b2e2a28a14b4259921086436"
|
|
62
62
|
}
|