Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,20 @@ SRT with:
- bounded time and aggregate output, with best-effort process-group termination
on cancellation, timeout, and completion.

Native command output keeps a prefix and rolling suffix for each stream, so late
summaries and errors survive truncation. Each stream stores at most
`maxOutputBytes` of copied raw bytes while the command runs, independent of output
volume or chunk count. When both streams are noisy they split the existing combined
response budget equally, with the odd byte reserved for stderr; a quiet stream gives
its unused allowance to the other. Sandbox violation annotations enter the same
stderr window before rendering. UTF-8 boundaries and inline
`[... N bytes omitted ...]` markers count toward the combined byte limit. The count
reports omitted raw bytes for that stream, including annotation bytes. If a stream's
allowance cannot fit its marker, only the retained text and the existing `truncated`
flag are returned. Truncation does not stop execution. Exit codes, timeout/signal
fields, and cancellation errors keep their existing semantics, and no new request,
result, or capability keys are introduced.

SRT restrictions remain inherited by descendants. Windows additionally uses a
kill-on-close Job Object. Native macOS does not provide an equivalent hard
process-lifetime boundary: a deliberately daemonized descendant can outlive
Expand Down
95 changes: 93 additions & 2 deletions packages/code/src/native-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from './native-sandbox.js';
import { restoreScratchTraversal } from './native-scratch.js';
import { WorkspaceToolError } from './workspace.js';
import { isWorkspaceToolResult } from './protocol.js';

const request = {
protocolVersion: 1 as const,
Expand Down Expand Up @@ -1355,14 +1356,104 @@ test('executes in the canonical workspace and bounds aggregate output', async t
operation: 'execute_command',
workspaceId: 'primary',
exitCode: 0,
stdout: '1234567890',
stderr: 'ab',
stdout: '123890',
stderr: 'abchij',
truncated: true,
timedOut: false,
},
);
});

test('retains real command summaries on both streams under the legacy combined budget', async t => {
const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-'));
t.after(() => rm(root, { recursive: true, force: true }));
const sandbox = new NativeSrtWorkspaceCommandSandbox({
workspaceRoot: root,
manager: fakeManager().manager,
});
t.after(() => sandbox.close());
const commandRequest = {
...request,
maxOutputBytes: 256,
command:
"printf 'OUT\\n'; printf '%20000d' 0; printf '\\n42 tests passed\\n'; printf 'ERR\\n' >&2; printf '%20000d' 0 >&2; printf '\\nlate stderr summary\\n' >&2",
};
const result = await sandbox.execute(commandRequest);
assert.equal(result.exitCode, 0);
assert.equal(result.timedOut, false);
assert.equal(result.truncated, true);
assert.ok(result.stdout.startsWith('OUT\n'));
assert.ok(result.stderr.startsWith('ERR\n'));
assert.ok(result.stdout.endsWith('\n42 tests passed\n'));
assert.ok(result.stderr.endsWith('\nlate stderr summary\n'));
assert.ok(result.stdout.includes('bytes omitted'));
assert.ok(result.stderr.includes('bytes omitted'));
assert.equal(isWorkspaceToolResult(commandRequest, result), true);
});

test('sandbox annotations survive full stdout and remain bounded when stderr is noisy', async t => {
const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-'));
t.after(() => rm(root, { recursive: true, force: true }));
const fake = fakeManager();
fake.manager.annotateStderrWithSandboxFailures = (_commandId, stderr) =>
stderr + '\n<sandbox_violations>\ndenied write\n</sandbox_violations>';
const sandbox = new NativeSrtWorkspaceCommandSandbox({
workspaceRoot: root,
manager: fake.manager,
});
t.after(() => sandbox.close());
for (const stderrCommand of ['', "printf '%20000d' 0 >&2;"]) {
const commandRequest = {
...request,
maxOutputBytes: 512,
command: `printf '%20000d' 0; ${stderrCommand} true`,
};
const result = await sandbox.execute(commandRequest);
assert.ok(
result.stderr.endsWith(
'<sandbox_violations>\ndenied write\n</sandbox_violations>'
)
);
assert.equal(isWorkspaceToolResult(commandRequest, result), true);
assert.equal(result.truncated, true);
}
fake.manager.annotateStderrWithSandboxFailures = () => {
throw new Error('annotation unavailable');
};
const result = await sandbox.execute({
...request,
maxOutputBytes: 128,
command: "printf '%20000d' 0; printf 'child failure' >&2",
});
assert.equal(result.stderr, 'child failure');
});

test('timeout settlement keeps late diagnostics, signal, and truncation without widening the result', async t => {
const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-'));
t.after(() => rm(root, { recursive: true, force: true }));
const sandbox = new NativeSrtWorkspaceCommandSandbox({
workspaceRoot: root,
manager: fakeManager().manager,
});
t.after(() => sandbox.close());
const commandRequest = {
...request,
maxOutputBytes: 128,
timeoutMs: 100,
command:
"printf 'START\\n'; printf '%20000d' 0; printf '\\nlast progress\\n'; printf 'last failure' >&2; sleep 30",
};
const result = await sandbox.execute(commandRequest);
assert.ok(result.stdout.startsWith('START\n'));
assert.ok(result.stdout.endsWith('\nlast progress\n'));
assert.equal(result.stderr, 'last failure');
assert.equal(result.timedOut, true);
assert.equal(result.truncated, true);
assert.equal(result.exitCode, null);
assert.equal(result.signal, 'SIGKILL');
assert.equal(isWorkspaceToolResult(commandRequest, result), true);
});

test('rejects an escaping or unavailable command working directory', async t => {
const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-'));
t.after(() => rm(root, { recursive: true, force: true }));
Expand Down
57 changes: 12 additions & 45 deletions packages/code/src/native-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
removePrivateStorageAcl,
} from './private-storage.js';
import { WorkspaceToolError } from './workspace.js';
import { OutputBuffer, renderCommandOutput } from './output.js';
import { restoreScratchTraversal } from './native-scratch.js';
import { writeLinkedWorktreeGitGuard } from './linked-worktree-git-guard.js';

Expand Down Expand Up @@ -1127,28 +1128,10 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox
}
let settled = false;
let timedOut = false;
let outputBytes = 0;
let truncated = false;
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
const append = (target: Buffer[], chunk: Buffer): void => {
const remaining = outputLimit - outputBytes;
if (remaining <= 0) {
truncated = true;
return;
}
const accepted = chunk.subarray(0, remaining);
target.push(accepted);
outputBytes += accepted.byteLength;
if (accepted.byteLength !== chunk.byteLength)
truncated = true;
};
child.stdout.on('data', (chunk: Buffer) =>
append(stdout, chunk),
);
child.stderr.on('data', (chunk: Buffer) =>
append(stderr, chunk),
);
const stdout = new OutputBuffer(outputLimit);
const stderr = new OutputBuffer(outputLimit);
child.stdout.on('data', (chunk: Buffer) => stdout.append(chunk));
child.stderr.on('data', (chunk: Buffer) => stderr.append(chunk));
const abort = (): void => {
if (settled) return;
this.killCommandTree(child);
Expand Down Expand Up @@ -1197,29 +1180,17 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox
);
return;
}
const stdoutValue = boundedUtf8(
Buffer.concat(stdout),
outputLimit,
);
const stderrBudget = Math.max(
0,
outputLimit - Buffer.byteLength(stdoutValue),
);
const rawStderr = Buffer.concat(stderr).toString('utf8');
let annotatedStderr = rawStderr;
try {
annotatedStderr =
this.manager.annotateStderrWithSandboxFailures(
commandId,
rawStderr,
// SRT appends violations to its input. Capture them in the same bounded stderr window.
stderr.append(
Buffer.from(
this.manager.annotateStderrWithSandboxFailures(commandId, ''),
),
);
} catch {
// Preserve the bounded child error if optional violation annotation fails.
}
const stderrValue = boundedUtf8(
Buffer.from(annotatedStderr),
stderrBudget,
);
const output = renderCommandOutput(stdout, stderr, outputLimit);
resolvePromise({
protocolVersion: BRIDGE_PROTOCOL_VERSION,
operation: 'execute_command',
Expand All @@ -1229,11 +1200,7 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox
? null
: this.protocolExitCode(code),
...(childSignal ? { signal: childSignal } : {}),
stdout: stdoutValue,
stderr: stderrValue,
truncated:
truncated ||
Buffer.byteLength(annotatedStderr) > stderrBudget,
...output,
timedOut,
});
});
Expand Down
Loading
Loading