@yagni-app/code-staging 1.1.2-staging.1368.1 → 1.1.2-staging.1369.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.
|
@@ -127,13 +127,64 @@ export declare function shouldUseSandboxForUserCommand(command: string, manager:
|
|
|
127
127
|
* advisory only, never a grant. Uninitialized manager ⇒ passthrough, no
|
|
128
128
|
* hint (no annotation context, and the sink is not wired yet).
|
|
129
129
|
*/
|
|
130
|
-
export declare function annotateCommandOutput(manager: YagniSandboxManager, command: string, output: string
|
|
130
|
+
export declare function annotateCommandOutput(manager: YagniSandboxManager, command: string, output: string, fsScope?: {
|
|
131
|
+
cwd: string;
|
|
132
|
+
scope: FsDenialScope;
|
|
133
|
+
}): {
|
|
131
134
|
text: string;
|
|
132
135
|
net: {
|
|
133
136
|
cls: NetworkDenialClass;
|
|
134
137
|
hint: string;
|
|
135
138
|
} | null;
|
|
139
|
+
fs: {
|
|
140
|
+
cls: FsDenialClass;
|
|
141
|
+
hint: string;
|
|
142
|
+
} | null;
|
|
136
143
|
};
|
|
144
|
+
/** The filesystem denial classes this classifier knows (closed set — rides
|
|
145
|
+
* the sink line as a low-cardinality field, the same contract as
|
|
146
|
+
* NetworkDenialClass). write-scope-protected is a distinct class (not a
|
|
147
|
+
* sub-case) because its REMEDY differs: protected paths can never be
|
|
148
|
+
* admitted with an allow rule, so the hint must not point at the knob. */
|
|
149
|
+
export type FsDenialClass = "write-scope" | "write-scope-protected" | "heredoc-cwd";
|
|
150
|
+
/** The writable/denied root lists the fs classifier needs, passed in by the
|
|
151
|
+
* caller so the classifier stays pure (the established pattern — see
|
|
152
|
+
* networkDenialHint). allowWrite: the sandbox's writable roots (session cwd,
|
|
153
|
+
* the temp dirs, config allowWrite). denyWrite: the protected paths that are
|
|
154
|
+
* denied even when a wider allow would cover them (config files, state dirs). */
|
|
155
|
+
export interface FsDenialScope {
|
|
156
|
+
allowWrite: readonly string[];
|
|
157
|
+
denyWrite: readonly string[];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Classify a sandbox filesystem denial from command + output. Pure;
|
|
161
|
+
* advisory-only (the fs twin of networkDenialHint — never widens anything,
|
|
162
|
+
* the remedy is model-side or a user config edit, never a grant). Matches
|
|
163
|
+
* the signatures observed live and in the session review:
|
|
164
|
+
* - heredoc-cwd: `cannot create temp file for here document` /
|
|
165
|
+
* `/dev/fd/NN: Operation not permitted` — bash 3.2
|
|
166
|
+
* writes heredoc temp files relative to the CURRENT
|
|
167
|
+
* directory, so a cd outside the writable roots makes
|
|
168
|
+
* every heredoc fail even when the target is writable
|
|
169
|
+
* (reproduced live; the e2b /dev/fd/62 case is the
|
|
170
|
+
* process-substitution sibling of the same mechanism)
|
|
171
|
+
* - write-scope: an EPERM whose line names a write TARGET path
|
|
172
|
+
* outside the writable roots — the literal `/tmp`
|
|
173
|
+
* writes observed in the sessions (`cat > /tmp/x`,
|
|
174
|
+
* `> /tmp/after.txt`, python PermissionError on a
|
|
175
|
+
* non-root path)
|
|
176
|
+
* - write-scope-protected: same shape but the target is a PROTECTED path
|
|
177
|
+
* (denyWrite) — the config.json / state-dir denies.
|
|
178
|
+
* Protected paths can never be admitted with allow
|
|
179
|
+
* rules, so the hint copy differs.
|
|
180
|
+
* Returns null when the output carries no fs-denial signature — a network
|
|
181
|
+
* denial must NOT get an fs hint (the pinned negative in networkDenialHint
|
|
182
|
+
* is symmetric here).
|
|
183
|
+
*/
|
|
184
|
+
export declare function fsDenialHint(command: string, output: string, cwd: string, scope: FsDenialScope): {
|
|
185
|
+
cls: FsDenialClass;
|
|
186
|
+
hint: string;
|
|
187
|
+
} | null;
|
|
137
188
|
/** cwd must exist before spawn (guard borrowed from pi's local ops). */
|
|
138
189
|
export declare function assertSpawnableCwd(cwd: string): void;
|
|
139
190
|
/**
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* lessons (process-group kill, stdio release).
|
|
22
22
|
*/
|
|
23
23
|
import { existsSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
24
25
|
import { logEvent } from "../errorSink.js";
|
|
25
26
|
import { scrubSecrets } from "../pipeline/scrubSecrets.js";
|
|
26
27
|
function stripLeadingSafeEnvVars(command) {
|
|
@@ -347,18 +348,128 @@ export function shouldUseSandboxForUserCommand(command, manager, settings) {
|
|
|
347
348
|
* advisory only, never a grant. Uninitialized manager ⇒ passthrough, no
|
|
348
349
|
* hint (no annotation context, and the sink is not wired yet).
|
|
349
350
|
*/
|
|
350
|
-
export function annotateCommandOutput(manager, command, output) {
|
|
351
|
+
export function annotateCommandOutput(manager, command, output, fsScope) {
|
|
351
352
|
if (!manager.initialized)
|
|
352
|
-
return { text: output, net: null };
|
|
353
|
+
return { text: output, net: null, fs: null };
|
|
353
354
|
const net = networkDenialHint(output);
|
|
354
355
|
if (!output.includes("Operation not permitted") && !net)
|
|
355
|
-
return { text: output, net: null };
|
|
356
|
+
return { text: output, net: null, fs: null };
|
|
356
357
|
const annotated = output.includes("Operation not permitted")
|
|
357
358
|
? manager.annotateStderrWithSandboxFailures(command, output)
|
|
358
359
|
: output;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
360
|
+
// The fs classification: same regex surface, same composition position,
|
|
361
|
+
// but the NETWORK hint wins when both classify (network denials have a
|
|
362
|
+
// user-flippable knob; fs remedies are model-side). When net wins, fs
|
|
363
|
+
// returns null — the callers log BOTH returns unconditionally, so a
|
|
364
|
+
// non-null loser would emit a second census line for one denial.
|
|
365
|
+
const fs = net
|
|
366
|
+
? null
|
|
367
|
+
: fsScope
|
|
368
|
+
? fsDenialHint(command, output, fsScope.cwd, fsScope.scope)
|
|
369
|
+
: null;
|
|
370
|
+
const hint = net ?? fs;
|
|
371
|
+
if (!hint)
|
|
372
|
+
return { text: annotated, net: null, fs: null };
|
|
373
|
+
return { text: `${annotated}\n\n[sandbox] ${hint.hint}`, net, fs };
|
|
374
|
+
}
|
|
375
|
+
/** Is `p` at or under one of the roots? Prefix match on path-segment
|
|
376
|
+
* boundaries (`/var/folders/T` must not admit `/var/folders/T-evil`). */
|
|
377
|
+
function underRoot(p, roots) {
|
|
378
|
+
return roots.some((r) => {
|
|
379
|
+
if (p === r)
|
|
380
|
+
return true;
|
|
381
|
+
return p.startsWith(r.endsWith("/") ? r : r + "/");
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/** Does the command carry a heredoc marker or a process substitution, plus a
|
|
385
|
+
* cd to a directory outside the writable roots? The discriminator for the
|
|
386
|
+
* heredoc-cwd drift shape: the temp-file write follows the shell's CURRENT
|
|
387
|
+
* directory, so a heredoc from inside the roots never has this failure —
|
|
388
|
+
* only the cd-drifted form does. */
|
|
389
|
+
function hasHeredocCwdDrift(command, cwd, scope) {
|
|
390
|
+
if (!/<<|<\(/.test(command))
|
|
391
|
+
return false;
|
|
392
|
+
// find every cd target and require at least one OUTSIDE the writable roots
|
|
393
|
+
const cdRe = /(^|[\n;&|(\s])cd\s+([^\n;&|)\s]+)/g;
|
|
394
|
+
let m;
|
|
395
|
+
while ((m = cdRe.exec(command)) !== null) {
|
|
396
|
+
const target = m[2].replace(/^("|')+|("|')+$/g, "");
|
|
397
|
+
// Resolve the same way a shell would for the classification question:
|
|
398
|
+
// absolute as-is, ~ against HOME (the common drift target), relative
|
|
399
|
+
// against the session cwd (a relative cd stays inside the roots only if
|
|
400
|
+
// the resolved path is under them).
|
|
401
|
+
let resolved = null;
|
|
402
|
+
if (target.startsWith("/"))
|
|
403
|
+
resolved = target;
|
|
404
|
+
else if (target.startsWith("~"))
|
|
405
|
+
resolved = target.replace(/^~/, process.env.HOME ?? "~");
|
|
406
|
+
else if (target !== "-" && !target.startsWith("$"))
|
|
407
|
+
resolved = join(cwd, target);
|
|
408
|
+
if (resolved && !underRoot(resolved, scope.allowWrite))
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Classify a sandbox filesystem denial from command + output. Pure;
|
|
415
|
+
* advisory-only (the fs twin of networkDenialHint — never widens anything,
|
|
416
|
+
* the remedy is model-side or a user config edit, never a grant). Matches
|
|
417
|
+
* the signatures observed live and in the session review:
|
|
418
|
+
* - heredoc-cwd: `cannot create temp file for here document` /
|
|
419
|
+
* `/dev/fd/NN: Operation not permitted` — bash 3.2
|
|
420
|
+
* writes heredoc temp files relative to the CURRENT
|
|
421
|
+
* directory, so a cd outside the writable roots makes
|
|
422
|
+
* every heredoc fail even when the target is writable
|
|
423
|
+
* (reproduced live; the e2b /dev/fd/62 case is the
|
|
424
|
+
* process-substitution sibling of the same mechanism)
|
|
425
|
+
* - write-scope: an EPERM whose line names a write TARGET path
|
|
426
|
+
* outside the writable roots — the literal `/tmp`
|
|
427
|
+
* writes observed in the sessions (`cat > /tmp/x`,
|
|
428
|
+
* `> /tmp/after.txt`, python PermissionError on a
|
|
429
|
+
* non-root path)
|
|
430
|
+
* - write-scope-protected: same shape but the target is a PROTECTED path
|
|
431
|
+
* (denyWrite) — the config.json / state-dir denies.
|
|
432
|
+
* Protected paths can never be admitted with allow
|
|
433
|
+
* rules, so the hint copy differs.
|
|
434
|
+
* Returns null when the output carries no fs-denial signature — a network
|
|
435
|
+
* denial must NOT get an fs hint (the pinned negative in networkDenialHint
|
|
436
|
+
* is symmetric here).
|
|
437
|
+
*/
|
|
438
|
+
export function fsDenialHint(command, output, cwd, scope) {
|
|
439
|
+
// heredoc-cwd first (most specific): the temp-file EPERM signatures, gated
|
|
440
|
+
// on the command actually carrying the drift shape.
|
|
441
|
+
if (/(cannot create temp file for here document|\/dev\/fd\/\d+: Operation not permitted)/.test(output) &&
|
|
442
|
+
hasHeredocCwdDrift(command, cwd, scope)) {
|
|
443
|
+
return {
|
|
444
|
+
cls: "heredoc-cwd",
|
|
445
|
+
hint: "This looks like a heredoc temp-file denial — bash writes heredoc temp files relative to the current directory, and a cd outside the writable roots makes every heredoc fail even when the heredoc target is writable. " +
|
|
446
|
+
"Run the heredoc from the working directory (no cd first), or use printf or a file in the working directory instead. Retrying with dangerouslyDisableSandbox is NOT needed.",
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
// write-target EPERM: the bash line shape (reusing extractBlockedWritePath's
|
|
450
|
+
// regex family) plus the python PermissionError shape.
|
|
451
|
+
const blocked = extractBlockedWritePath(output) ??
|
|
452
|
+
(output.match(/PermissionError: \[Errno \d+\] Operation not permitted: '([^']+)'/)?.[1] ?? null);
|
|
453
|
+
if (!blocked)
|
|
454
|
+
return null;
|
|
455
|
+
// A target inside the writable roots is NOT a write-scope denial — the
|
|
456
|
+
// scratchpad lives under the temp dir and its writes never hit the fence
|
|
457
|
+
// (the observed non-bug; pinned by test).
|
|
458
|
+
if (underRoot(blocked, scope.allowWrite))
|
|
459
|
+
return null;
|
|
460
|
+
if (underRoot(blocked, scope.denyWrite)) {
|
|
461
|
+
return {
|
|
462
|
+
cls: "write-scope-protected",
|
|
463
|
+
hint: "This looks like a sandbox write to a protected path — this path is denied even with allow rules (it holds the agent's own configuration). " +
|
|
464
|
+
"Do not write here; ask the user or use a different location. Retrying with dangerouslyDisableSandbox will still ask for permission and is the wrong tool for this — the protection is deliberate.",
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
cls: "write-scope",
|
|
469
|
+
hint: "This looks like a sandbox write-scope denial — the target is outside the writable roots (the working directory and $TMPDIR). " +
|
|
470
|
+
"Use $TMPDIR or the project directory for temporary files. To write here anyway, add the path to sandbox.filesystem.allowWrite in your settings (the /sandbox panel's Config tab shows the current scope). " +
|
|
471
|
+
"Retrying with dangerouslyDisableSandbox is usually NOT needed — move the target inside the scope instead.",
|
|
472
|
+
};
|
|
362
473
|
}
|
|
363
474
|
/** cwd must exist before spawn (guard borrowed from pi's local ops). */
|
|
364
475
|
export function assertSpawnableCwd(cwd) {
|
|
@@ -28,7 +28,10 @@ import type { PermissionRule } from "../permissionRules/loadConfig.js";
|
|
|
28
28
|
* here would be overwritten and the sandbox would silently never reach
|
|
29
29
|
* model-driven tool calls.
|
|
30
30
|
*/
|
|
31
|
-
export declare function makeBashComposition(manager: YagniSandboxManager, settings: () => SandboxSettings, cwd: string, onShellResolutionRetry?: (outcome: "recovered" | "exhausted") => void
|
|
31
|
+
export declare function makeBashComposition(manager: YagniSandboxManager, settings: () => SandboxSettings, cwd: string, onShellResolutionRetry?: (outcome: "recovered" | "exhausted") => void, fsScope?: () => {
|
|
32
|
+
allowWrite: readonly string[];
|
|
33
|
+
denyWrite: readonly string[];
|
|
34
|
+
} | null): (def: ToolDefinition) => ToolDefinition;
|
|
32
35
|
export interface SandboxSessionHandle {
|
|
33
36
|
manager: YagniSandboxManager;
|
|
34
37
|
settings: () => SandboxSettings;
|
|
@@ -23,7 +23,7 @@ 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, networkDenialHint, preWrappedCommand, shouldUseSandbox, shouldUseSandboxForUserCommand, } from "./bash.js";
|
|
26
|
+
import { annotateCommandOutput, fsDenialHint, makeSandboxSpawnHook, networkDenialHint, preWrappedCommand, shouldUseSandbox, shouldUseSandboxForUserCommand, } from "./bash.js";
|
|
27
27
|
import { YagniSandboxManager } from "./manager.js";
|
|
28
28
|
import { SandboxPanel, buildPanelState, engineeringPresetBlock } from "./panel.js";
|
|
29
29
|
import { effectiveRules } from "../permissionRules/loadConfig.js";
|
|
@@ -39,7 +39,7 @@ import { effectiveRules } from "../permissionRules/loadConfig.js";
|
|
|
39
39
|
* here would be overwritten and the sandbox would silently never reach
|
|
40
40
|
* model-driven tool calls.
|
|
41
41
|
*/
|
|
42
|
-
export function makeBashComposition(manager, settings, cwd, onShellResolutionRetry) {
|
|
42
|
+
export function makeBashComposition(manager, settings, cwd, onShellResolutionRetry, fsScope) {
|
|
43
43
|
return (def) => {
|
|
44
44
|
if (!settings().enabled)
|
|
45
45
|
return def;
|
|
@@ -50,6 +50,12 @@ export function makeBashComposition(manager, settings, cwd, onShellResolutionRet
|
|
|
50
50
|
shellPath: userShellPath,
|
|
51
51
|
spawnHook,
|
|
52
52
|
});
|
|
53
|
+
// The fs-denial classification scope (advisory hint only): resolved
|
|
54
|
+
// lazily per annotation — a denial is rare, the merge is not free.
|
|
55
|
+
const fsScopeForAnnotate = () => {
|
|
56
|
+
const scope = fsScope?.();
|
|
57
|
+
return scope ? { cwd, scope } : undefined;
|
|
58
|
+
};
|
|
53
59
|
const schema = Type.Object({
|
|
54
60
|
command: Type.String({ description: "The bash command to execute" }),
|
|
55
61
|
timeout: Type.Optional(Type.Number({ description: "Optional timeout in seconds" })),
|
|
@@ -138,9 +144,11 @@ export function makeBashComposition(manager, settings, cwd, onShellResolutionRet
|
|
|
138
144
|
// The thrown-message gate matches the result-text gate: a
|
|
139
145
|
// network-signature denial in EITHER surface gets the
|
|
140
146
|
// annotation + hint (node throws lowercase "operation not
|
|
141
|
-
// permitted", which the bare substring misses).
|
|
142
|
-
|
|
147
|
+
// permitted", which the bare substring misses). The fs hint
|
|
148
|
+
// composes under the same gate (one diagnosis, network wins).
|
|
149
|
+
const annotated = annotateCommandOutput(manager, input.command, err.message, fsScopeForAnnotate());
|
|
143
150
|
logNetworkDenialHint(annotated.net);
|
|
151
|
+
logFsDenialHint(annotated.fs, "thrown");
|
|
144
152
|
throw new Error(annotated.text);
|
|
145
153
|
}
|
|
146
154
|
throw err;
|
|
@@ -151,8 +159,9 @@ export function makeBashComposition(manager, settings, cwd, onShellResolutionRet
|
|
|
151
159
|
if (result?.content) {
|
|
152
160
|
const text = result.content.map((c) => (c.type === "text" ? c.text : "")).join("\n");
|
|
153
161
|
if (text.includes("Operation not permitted") || networkDenialHint(text)) {
|
|
154
|
-
const annotated = annotateCommandOutput(manager, input.command, text);
|
|
162
|
+
const annotated = annotateCommandOutput(manager, input.command, text, fsScopeForAnnotate());
|
|
155
163
|
logNetworkDenialHint(annotated.net);
|
|
164
|
+
logFsDenialHint(annotated.fs, "result");
|
|
156
165
|
result = { ...result, content: [{ type: "text", text: annotated.text }] };
|
|
157
166
|
}
|
|
158
167
|
}
|
|
@@ -179,6 +188,33 @@ export function registerSandbox(pi, opts) {
|
|
|
179
188
|
}).settings;
|
|
180
189
|
let currentSettings = load();
|
|
181
190
|
let rules = [];
|
|
191
|
+
// The fs-denial classification scope for the advisory hint (the roots
|
|
192
|
+
// the runtime merge resolves: cwd + tmpdir + config allowWrite, denied:
|
|
193
|
+
// the protected paths). Lazy + memo-invalidated per call — a denial is
|
|
194
|
+
// rare and buildRuntimeMerge is not free; re-reading per annotation also
|
|
195
|
+
// picks up rules/settings changes without a cache to invalidate.
|
|
196
|
+
const fsScope = () => {
|
|
197
|
+
if (!manager.initialized)
|
|
198
|
+
return null;
|
|
199
|
+
try {
|
|
200
|
+
const merge = manager.buildRuntimeMerge(rules);
|
|
201
|
+
return { allowWrite: merge.filesystem.allowWrite, denyWrite: merge.filesystem.denyWrite };
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
// Never the thrown message (it can carry command content) — error
|
|
205
|
+
// class only, same posture as the gate's classify-error line. Without
|
|
206
|
+
// this trace a persistently-throwing merge would silently strip fs
|
|
207
|
+
// hints from all three surfaces and the only symptom would be "hints
|
|
208
|
+
// disappeared" from the census the measurement depends on.
|
|
209
|
+
logEvent({
|
|
210
|
+
source: "sandbox",
|
|
211
|
+
level: "warn",
|
|
212
|
+
event: "fs_scope_error",
|
|
213
|
+
fields: { error: err instanceof Error ? err.constructor.name : typeof err },
|
|
214
|
+
});
|
|
215
|
+
return null; // fail-soft: no scope, no hint (the network hint still rides)
|
|
216
|
+
}
|
|
217
|
+
};
|
|
182
218
|
// Optional-chained: some harnesses/mocks don't implement flag APIs; the
|
|
183
219
|
// flag simply reads as unset there. Real pi registers it normally.
|
|
184
220
|
pi.registerFlag?.("no-sandbox", {
|
|
@@ -326,7 +362,7 @@ export function registerSandbox(pi, opts) {
|
|
|
326
362
|
logShellResolutionRetry(outcome);
|
|
327
363
|
opts.onShellResolutionRetry?.(outcome);
|
|
328
364
|
};
|
|
329
|
-
const composeBash = makeBashComposition(manager, () => currentSettings, opts.cwd, onShellResolutionRetry);
|
|
365
|
+
const composeBash = makeBashComposition(manager, () => currentSettings, opts.cwd, onShellResolutionRetry, fsScope);
|
|
330
366
|
const registerOwnBash = () => {
|
|
331
367
|
if (!currentSettings.enabled)
|
|
332
368
|
return;
|
|
@@ -409,11 +445,22 @@ export function registerSandbox(pi, opts) {
|
|
|
409
445
|
throw new Error(`timeout:${timeout}`);
|
|
410
446
|
// Post-run hint: same advisory as the tool path, appended after
|
|
411
447
|
// the child's own output so the user sees the fix pointer inline.
|
|
448
|
+
// One diagnosis per denial: the network hint wins when both
|
|
449
|
+
// classify (network has a user knob; fs remedies are model-side).
|
|
450
|
+
// A null fsScope() (uninitialized manager, merge error) skips the
|
|
451
|
+
// fs classification entirely — an empty-roots fallback would
|
|
452
|
+
// misclassify in-roots and protected targets as write-scope.
|
|
412
453
|
const net = networkDenialHint(tail);
|
|
454
|
+
const scope = net ? null : fsScope();
|
|
455
|
+
const fs = scope ? fsDenialHint(event.command, tail, opts.cwd, scope) : null;
|
|
413
456
|
if (net) {
|
|
414
457
|
logEvent({ source: "sandbox", level: "info", event: "network_denial_hint", fields: { class: net.cls } });
|
|
415
458
|
onData(Buffer.from(`\n[sandbox] ${net.hint}`));
|
|
416
459
|
}
|
|
460
|
+
else if (fs) {
|
|
461
|
+
logEvent({ source: "sandbox", level: "info", event: "fs_denial_hint", fields: { class: fs.cls, surface: "user-bash" } });
|
|
462
|
+
onData(Buffer.from(`\n[sandbox] ${fs.hint}`));
|
|
463
|
+
}
|
|
417
464
|
return { exitCode };
|
|
418
465
|
}
|
|
419
466
|
finally {
|
|
@@ -825,6 +872,15 @@ function logNetworkDenialHint(net) {
|
|
|
825
872
|
return;
|
|
826
873
|
logEvent({ source: "sandbox", level: "info", event: "network_denial_hint", fields: { class: net.cls } });
|
|
827
874
|
}
|
|
875
|
+
/** Sink line for the fs-denial hint (same low-cardinality contract as the
|
|
876
|
+
* network one — the before/after escape census reads these lines). The
|
|
877
|
+
* surface field distinguishes the tool-call thrown-error / result-text /
|
|
878
|
+
* !-command call sites in the log stream. */
|
|
879
|
+
function logFsDenialHint(fs, surface) {
|
|
880
|
+
if (!fs)
|
|
881
|
+
return;
|
|
882
|
+
logEvent({ source: "sandbox", level: "info", event: "fs_denial_hint", fields: { class: fs.cls, surface } });
|
|
883
|
+
}
|
|
828
884
|
/** Sink line for a failed domain-grant persist — the thrown message is
|
|
829
885
|
* mutateConfigJson's own path/reason text (diagnosable, no file content),
|
|
830
886
|
* same posture as sandbox_persist_failed / rule_save_failed. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.2-staging.
|
|
3
|
+
"version": "1.1.2-staging.1369.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": "806c992d00925d279ff5aa7beda45682c3875513"
|
|
62
62
|
}
|