From 6512c20c58b3a585efdc8f37133dfe978ce1e299 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 8 Sep 2026 23:07:07 -0400 Subject: [PATCH 1/6] fix: Harden Native Scratch Cleanup --- packages/code/src/native-sandbox.test.ts | 36 ++++++++++++ packages/code/src/native-sandbox.ts | 56 +++++------------- packages/code/src/native-scratch.ts | 74 ++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 41 deletions(-) create mode 100644 packages/code/src/native-scratch.ts diff --git a/packages/code/src/native-sandbox.test.ts b/packages/code/src/native-sandbox.test.ts index 2d40db0..2d65eeb 100644 --- a/packages/code/src/native-sandbox.test.ts +++ b/packages/code/src/native-sandbox.test.ts @@ -3,11 +3,15 @@ import { spawn } from 'node:child_process'; import { EventEmitter } from 'node:events'; import { access, + chmod, mkdtemp, mkdir, + open, realpath, + rename, rm, stat, + symlink, writeFile, } from 'node:fs/promises'; import { tmpdir, homedir } from 'node:os'; @@ -19,6 +23,7 @@ import type { SandboxRuntimeConfig } from '@anthropic-ai/sandbox-runtime'; import type { ChildProcessWithoutNullStreams } from 'node:child_process'; import { NativeSrtWorkspaceCommandSandbox } from './native-sandbox.js'; +import { restoreScratchTraversal } from './native-scratch.js'; import { WorkspaceToolError } from './workspace.js'; const request = { @@ -447,6 +452,37 @@ test('removes scratch storage after a command revokes traversal permissions', as await assert.rejects(access(result.stdout)); }); +test('scratch traversal never follows a descendant replaced after inspection', async (t) => { + if (process.platform === 'win32') return; + const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-race-')); + const outside = await mkdtemp(join(tmpdir(), 'librechat-code-outside-')); + const descendant = join(root, 'locked'); + const retired = join(root, 'retired'); + const outsideChild = join(outside, 'child'); + t.after(() => rm(root, { recursive: true, force: true })); + t.after(() => rm(outside, { recursive: true, force: true })); + await mkdir(descendant); + await mkdir(outsideChild); + await chmod(outside, 0o711); + await chmod(outsideChild, 0o711); + const rootHandle = await open(root, 'r'); + t.after(() => rootHandle.close()); + let swapped = false; + + await restoreScratchTraversal(rootHandle, { + async afterEntryInspected(_directoryFd, name) { + if (name !== 'locked' || swapped) return; + swapped = true; + await rename(descendant, retired); + await symlink(outside, descendant, 'dir'); + }, + }); + + assert.equal(swapped, true); + assert.equal((await stat(outside)).mode & 0o777, 0o711); + assert.equal((await stat(outsideChild)).mode & 0o777, 0o711); +}); + const proxyEnvironment = { HTTP_PROXY: 'http://upstream.invalid:8080', HTTPS_PROXY: 'http://upstream.invalid:8080', diff --git a/packages/code/src/native-sandbox.ts b/packages/code/src/native-sandbox.ts index bc57044..561b1bb 100644 --- a/packages/code/src/native-sandbox.ts +++ b/packages/code/src/native-sandbox.ts @@ -13,15 +13,13 @@ import { import { constants as fsConstants } from 'node:fs'; import { access, - chmod, - lstat, mkdtemp, open, - readdir, realpath, rm, stat, } from 'node:fs/promises'; +import type { FileHandle } from 'node:fs/promises'; import { SandboxManager } from '@anthropic-ai/sandbox-runtime'; @@ -37,6 +35,7 @@ import { removePrivateStorageAcl, } from './private-storage.js'; import { WorkspaceToolError } from './workspace.js'; +import { restoreScratchTraversal } from './native-scratch.js'; import type { ChildProcessWithoutNullStreams, @@ -245,6 +244,7 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox private initialized?: Promise; private canonicalRoot?: string; private scratchDirectory?: string; + private scratchHandle?: FileHandle; private execution?: Promise; private closing?: Promise; private resetFailed = false; @@ -774,12 +774,16 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox if (((await scratchHandle.stat()).mode & 0o777) !== 0o700) { throw new Error('Native sandbox scratch directory is not private'); } - } finally { + this.scratchHandle = scratchHandle; + } catch (error) { await scratchHandle.close(); + throw error; } this.scratchDirectory = await realpath(scratchDirectory); return this.scratchDirectory; } catch (error) { + await this.scratchHandle?.close().catch(() => undefined); + this.scratchHandle = undefined; await rm(scratchDirectory, { recursive: true, force: true }).catch( () => undefined, ); @@ -813,48 +817,18 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox try { await rm(scratchDirectory, { recursive: true, force: true }); } catch { - await this.restoreScratchTraversal(scratchDirectory); + if (!this.scratchHandle) { + throw new Error('Native sandbox scratch descriptor is unavailable'); + } + await restoreScratchTraversal(this.scratchHandle); await rm(scratchDirectory, { recursive: true, force: true }); + } finally { + await this.scratchHandle?.close(); + this.scratchHandle = undefined; } this.scratchDirectory = undefined; } - private async restoreScratchTraversal(root: string): Promise { - const pending = [root]; - for (let index = 0; index < pending.length; index += 1) { - const directory = pending[index]; - const metadata = await lstat(directory).catch( - (error: NodeJS.ErrnoException) => { - if (error.code === 'ENOENT') return undefined; - throw error; - }, - ); - if (!metadata?.isDirectory()) continue; - // Commands own their scratch contents and may remove all directory mode - // bits. Restore traversal before opening the directory with O_NOFOLLOW. - await chmod(directory, 0o700); - let handle; - try { - handle = await open( - directory, - fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW, - ); - } catch (error) { - const code = (error as NodeJS.ErrnoException).code; - if (['ENOENT', 'ELOOP', 'ENOTDIR'].includes(code ?? '')) continue; - throw error; - } - try { - if (!(await handle.stat()).isDirectory()) continue; - } finally { - await handle.close(); - } - for (const entry of await readdir(directory, { withFileTypes: true })) { - if (entry.isDirectory()) pending.push(join(directory, entry.name)); - } - } - } - async close(): Promise { if (this.closing) return this.closing; const closing = this.closeExclusive(); diff --git a/packages/code/src/native-scratch.ts b/packages/code/src/native-scratch.ts new file mode 100644 index 0000000..9d9d0b9 --- /dev/null +++ b/packages/code/src/native-scratch.ts @@ -0,0 +1,74 @@ +import { constants as fsConstants } from 'node:fs'; +import { open, readdir } from 'node:fs/promises'; +import type { FileHandle } from 'node:fs/promises'; + +import koffi from 'koffi'; + +const lib = process.platform === 'darwin' + ? koffi.load('/usr/lib/libSystem.B.dylib') + : process.platform === 'linux' + ? koffi.load('libc.so.6') + : undefined; +const fchmodat = lib?.func( + 'int fchmodat(int dirfd, const char *path, uint32_t mode, int flags)', +); +const AT_SYMLINK_NOFOLLOW = process.platform === 'darwin' ? 0x0020 : 0x0100; +const IGNORED_ENTRY_ERRNOS = new Set([ + koffi.os.errno.ENOENT, + koffi.os.errno.ELOOP, + koffi.os.errno.ENOTDIR, + koffi.os.errno.ENOTSUP, +]); + +export interface ScratchTraversalHooks { + /** Test seam for deterministic replacement-race coverage. */ + afterEntryInspected?(directoryFd: number, name: string): Promise; +} + +function descriptorPath(fd: number): string { + return process.platform === 'linux' ? `/proc/self/fd/${fd}` : `/dev/fd/${fd}`; +} + +function restoreEntryMode(directoryFd: number, name: string): boolean { + if (!fchmodat) { + throw new Error('Descriptor-relative scratch cleanup is unavailable'); + } + if (fchmodat(directoryFd, name, 0o700, AT_SYMLINK_NOFOLLOW) === 0) { + return true; + } + const errno = koffi.errno(); + if (IGNORED_ENTRY_ERRNOS.has(errno)) return false; + const error = new Error( + `Descriptor-relative scratch chmod failed with errno ${errno}`, + ) as NodeJS.ErrnoException; + error.errno = errno; + throw error; +} + +/** Restores traversal without resolving a worker-controlled descendant through an ambient path. */ +export async function restoreScratchTraversal( + root: FileHandle, + hooks: ScratchTraversalHooks = {}, +): Promise { + await root.chmod(0o700); + const entries = await readdir(descriptorPath(root.fd), { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + await hooks.afterEntryInspected?.(root.fd, entry.name); + if (!restoreEntryMode(root.fd, entry.name)) continue; + let child: FileHandle | undefined; + try { + child = await open( + `${descriptorPath(root.fd)}/${entry.name}`, + fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW, + ); + if (!(await child.stat()).isDirectory()) continue; + await restoreScratchTraversal(child, hooks); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (!['ENOENT', 'ELOOP', 'ENOTDIR'].includes(code ?? '')) throw error; + } finally { + await child?.close(); + } + } +} From 170939ecb1c8247448471fcc9c921abb06691519 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 8 Sep 2026 23:09:34 -0400 Subject: [PATCH 2/6] fix: Resolve Portable POSIX Cleanup Symbols --- packages/code/src/native-scratch.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/code/src/native-scratch.ts b/packages/code/src/native-scratch.ts index 9d9d0b9..58ec224 100644 --- a/packages/code/src/native-scratch.ts +++ b/packages/code/src/native-scratch.ts @@ -4,11 +4,11 @@ import type { FileHandle } from 'node:fs/promises'; import koffi from 'koffi'; -const lib = process.platform === 'darwin' - ? koffi.load('/usr/lib/libSystem.B.dylib') - : process.platform === 'linux' - ? koffi.load('libc.so.6') - : undefined; +// Resolve the process's POSIX symbols instead of naming glibc. BYOM workers +// may run on musl-based distributions, while Darwin exposes the same symbol. +const lib = ['darwin', 'linux'].includes(process.platform) + ? koffi.load(null) + : undefined; const fchmodat = lib?.func( 'int fchmodat(int dirfd, const char *path, uint32_t mode, int flags)', ); From bd169ce1e989b0fc605c12e4008ef711aebf14bb Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 8 Sep 2026 23:26:33 -0400 Subject: [PATCH 3/6] fix: Bound Descriptor-Relative Scratch Recovery --- packages/code/src/native-sandbox.test.ts | 41 ++++++- packages/code/src/native-sandbox.ts | 16 +-- packages/code/src/native-scratch.ts | 134 +++++++++++++++++------ packages/code/src/private-storage.ts | 7 +- 4 files changed, 154 insertions(+), 44 deletions(-) diff --git a/packages/code/src/native-sandbox.test.ts b/packages/code/src/native-sandbox.test.ts index 2d65eeb..9289271 100644 --- a/packages/code/src/native-sandbox.test.ts +++ b/packages/code/src/native-sandbox.test.ts @@ -444,7 +444,7 @@ test('removes scratch storage after a command revokes traversal permissions', as const result = await sandbox.execute({ ...request, command: - 'printf %s "$TMPDIR"; mkdir "$TMPDIR/locked"; touch "$TMPDIR/locked/file"; chmod 000 "$TMPDIR/locked" "$TMPDIR"', + 'printf %s "$TMPDIR"; mkdir -p "$TMPDIR/locked/deeper"; touch "$TMPDIR/locked/deeper/file"; chmod 000 "$TMPDIR/locked/deeper" "$TMPDIR/locked" "$TMPDIR"', }); assert.equal(result.exitCode, 0); @@ -483,6 +483,45 @@ test('scratch traversal never follows a descendant replaced after inspection', a assert.equal((await stat(outsideChild)).mode & 0o777, 0o711); }); +test('scratch traversal removes command-created Darwin ACLs', async (t) => { + if (process.platform !== 'darwin') return; + 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, + }); + const result = await sandbox.execute({ + ...request, + command: + 'printf %s "$TMPDIR"; mkdir -p "$TMPDIR/locked/deeper"; touch "$TMPDIR/locked/deeper/file"; chmod +a "$USER deny list,search,delete_child" "$TMPDIR/locked" "$TMPDIR"; chmod 000 "$TMPDIR/locked" "$TMPDIR"', + }); + + assert.equal(result.exitCode, 0); + await sandbox.close(); + await assert.rejects(access(result.stdout)); +}); + +test('scratch traversal bounds descriptors across a deep tree', async (t) => { + if (process.platform === 'win32') return; + const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-depth-')); + t.after(() => rm(root, { recursive: true, force: true })); + const directories = [root]; + for (let depth = 0; depth < 300; depth += 1) { + directories.push(join(directories[directories.length - 1], 'd')); + await mkdir(directories[directories.length - 1]); + } + for (const directory of directories.slice(1).reverse()) { + await chmod(directory, 0o000); + } + const rootHandle = await open(root, 'r'); + t.after(() => rootHandle.close()); + + await restoreScratchTraversal(rootHandle); + + assert.equal((await stat(directories[directories.length - 1])).mode & 0o777, 0o700); +}); + const proxyEnvironment = { HTTP_PROXY: 'http://upstream.invalid:8080', HTTPS_PROXY: 'http://upstream.invalid:8080', diff --git a/packages/code/src/native-sandbox.ts b/packages/code/src/native-sandbox.ts index 561b1bb..1adfd35 100644 --- a/packages/code/src/native-sandbox.ts +++ b/packages/code/src/native-sandbox.ts @@ -814,18 +814,20 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox private async removeScratchDirectory(): Promise { const scratchDirectory = this.scratchDirectory; if (!scratchDirectory) return; + const scratchHandle = this.scratchHandle; + if (!scratchHandle) { + throw new Error('Native sandbox scratch descriptor is unavailable'); + } try { await rm(scratchDirectory, { recursive: true, force: true }); } catch { - if (!this.scratchHandle) { - throw new Error('Native sandbox scratch descriptor is unavailable'); - } - await restoreScratchTraversal(this.scratchHandle); + await restoreScratchTraversal(scratchHandle); await rm(scratchDirectory, { recursive: true, force: true }); - } finally { - await this.scratchHandle?.close(); - this.scratchHandle = undefined; } + // Retain both the descriptor and path when cleanup fails so close() can + // retry without falling back to an attacker-replaceable ambient path. + await scratchHandle.close(); + this.scratchHandle = undefined; this.scratchDirectory = undefined; } diff --git a/packages/code/src/native-scratch.ts b/packages/code/src/native-scratch.ts index 58ec224..59338a7 100644 --- a/packages/code/src/native-scratch.ts +++ b/packages/code/src/native-scratch.ts @@ -1,18 +1,21 @@ import { constants as fsConstants } from 'node:fs'; -import { open, readdir } from 'node:fs/promises'; +import { readdir } from 'node:fs/promises'; import type { FileHandle } from 'node:fs/promises'; import koffi from 'koffi'; -// Resolve the process's POSIX symbols instead of naming glibc. BYOM workers -// may run on musl-based distributions, while Darwin exposes the same symbol. -const lib = ['darwin', 'linux'].includes(process.platform) - ? koffi.load(null) - : undefined; +import { removePrivateStorageAcl } from './private-storage.js'; + +const POSIX_PLATFORMS = new Set(['darwin', 'linux']); +const lib = POSIX_PLATFORMS.has(process.platform) ? koffi.load(null) : undefined; +const openat = lib?.func('int openat(int dirfd, const char *path, int flags, uint32_t mode)'); +const closeFd = lib?.func('int close(int fd)'); +const fchmod = lib?.func('int fchmod(int fd, uint32_t mode)'); const fchmodat = lib?.func( 'int fchmodat(int dirfd, const char *path, uint32_t mode, int flags)', ); const AT_SYMLINK_NOFOLLOW = process.platform === 'darwin' ? 0x0020 : 0x0100; +const O_EVTONLY = 0x8000; const IGNORED_ENTRY_ERRNOS = new Set([ koffi.os.errno.ENOENT, koffi.os.errno.ELOOP, @@ -29,46 +32,109 @@ function descriptorPath(fd: number): string { return process.platform === 'linux' ? `/proc/self/fd/${fd}` : `/dev/fd/${fd}`; } -function restoreEntryMode(directoryFd: number, name: string): boolean { - if (!fchmodat) { +function requirePosixBindings(): void { + if (!openat || !closeFd || !fchmod || !fchmodat) { throw new Error('Descriptor-relative scratch cleanup is unavailable'); } - if (fchmodat(directoryFd, name, 0o700, AT_SYMLINK_NOFOLLOW) === 0) { - return true; +} + +function ignoredEntryError(): boolean { + return IGNORED_ENTRY_ERRNOS.has(koffi.errno()); +} + +function restoreEntryMode(directoryFd: number, name: string): boolean { + requirePosixBindings(); + if (fchmodat!(directoryFd, name, 0o700, AT_SYMLINK_NOFOLLOW) === 0) return true; + if (ignoredEntryError()) return false; + throw new Error(`Descriptor-relative scratch chmod failed with errno ${koffi.errno()}`); +} + +function openDirectoryAt(directoryFd: number, name: string): number | undefined { + requirePosixBindings(); + const commonFlags = fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW; + const repairFlags = process.platform === 'darwin' + ? commonFlags | O_EVTONLY + : commonFlags | fsConstants.O_RDONLY; + const fd = openat!(directoryFd, name, repairFlags, 0); + if (fd >= 0) return fd; + if (ignoredEntryError()) return undefined; + throw new Error(`Descriptor-relative scratch open failed with errno ${koffi.errno()}`); +} + +function closeDirectory(fd: number): void { + requirePosixBindings(); + if (closeFd!(fd) !== 0) { + throw new Error(`Descriptor-relative scratch close failed with errno ${koffi.errno()}`); + } +} + +async function repairDirectory(fd: number, label: string): Promise { + requirePosixBindings(); + if (fchmod!(fd, 0o700) !== 0) { + throw new Error(`Descriptor-relative scratch chmod failed with errno ${koffi.errno()}`); + } + await removePrivateStorageAcl({ fd }, label); +} + +async function openRelativeDirectory( + rootFd: number, + components: string[], + hooks: ScratchTraversalHooks, +): Promise { + let currentFd = rootFd; + try { + for (const component of components) { + await hooks.afterEntryInspected?.(currentFd, component); + if (!restoreEntryMode(currentFd, component)) { + if (currentFd !== rootFd) { + const closingFd = currentFd; + currentFd = rootFd; + closeDirectory(closingFd); + } + return undefined; + } + const childFd = openDirectoryAt(currentFd, component); + if (currentFd !== rootFd) { + const closingFd = currentFd; + currentFd = rootFd; + closeDirectory(closingFd); + } + if (childFd === undefined) return undefined; + currentFd = childFd; + await repairDirectory(currentFd, `scratch directory ${components.join('/')}`); + } + return currentFd; + } catch (error) { + if (currentFd !== rootFd) closeDirectory(currentFd); + throw error; } - const errno = koffi.errno(); - if (IGNORED_ENTRY_ERRNOS.has(errno)) return false; - const error = new Error( - `Descriptor-relative scratch chmod failed with errno ${errno}`, - ) as NodeJS.ErrnoException; - error.errno = errno; - throw error; } -/** Restores traversal without resolving a worker-controlled descendant through an ambient path. */ +/** + * Restores traversal without resolving worker-controlled descendants through + * ambient paths. Relative component lists retain no descriptors; reopening a + * path holds at most two descriptors and refuses replacement symlinks. + */ export async function restoreScratchTraversal( root: FileHandle, hooks: ScratchTraversalHooks = {}, ): Promise { await root.chmod(0o700); - const entries = await readdir(descriptorPath(root.fd), { withFileTypes: true }); - for (const entry of entries) { - if (!entry.isDirectory()) continue; - await hooks.afterEntryInspected?.(root.fd, entry.name); - if (!restoreEntryMode(root.fd, entry.name)) continue; - let child: FileHandle | undefined; + await removePrivateStorageAcl(root, 'native sandbox scratch root'); + const pending: string[][] = [[]]; + for (let index = 0; index < pending.length; index += 1) { + const components = pending[index]; + const directoryFd = components.length === 0 + ? root.fd + : await openRelativeDirectory(root.fd, components, hooks); + if (directoryFd === undefined) continue; try { - child = await open( - `${descriptorPath(root.fd)}/${entry.name}`, - fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW, - ); - if (!(await child.stat()).isDirectory()) continue; - await restoreScratchTraversal(child, hooks); - } catch (error) { - const code = (error as NodeJS.ErrnoException).code; - if (!['ENOENT', 'ELOOP', 'ENOTDIR'].includes(code ?? '')) throw error; + const entries = await readdir(descriptorPath(directoryFd), { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) pending.push([...components, entry.name]); + } } finally { - await child?.close(); + if (directoryFd !== root.fd) closeDirectory(directoryFd); } } } diff --git a/packages/code/src/private-storage.ts b/packages/code/src/private-storage.ts index af4f185..25f23a2 100644 --- a/packages/code/src/private-storage.ts +++ b/packages/code/src/private-storage.ts @@ -25,7 +25,7 @@ async function macOsStorage() { } export async function assertPrivateStorageAcl( - handle: FileHandle, path: string, directory = false, + handle: Pick, path: string, directory = false, ): Promise { if (process.platform === 'darwin') { (await macOsStorage()).verifyMacOsAcl(handle.fd, path, directory); @@ -33,7 +33,10 @@ export async function assertPrivateStorageAcl( } /** Only application-owned files/directories may have their ACLs removed. */ -export async function removePrivateStorageAcl(handle: FileHandle, path: string): Promise { +export async function removePrivateStorageAcl( + handle: Pick, + path: string, +): Promise { if (process.platform === 'darwin') { (await macOsStorage()).removeMacOsAcl(handle.fd, path); } From eb761149fb8c6c11a09b4bfa6bd0c3c03182dbd6 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 8 Sep 2026 23:42:27 -0400 Subject: [PATCH 4/6] fix: Bound Native Scratch Recovery Work --- packages/code/src/native-sandbox.test.ts | 2153 ++++++++++++---------- packages/code/src/native-sandbox.ts | 1547 ++++++++-------- packages/code/src/native-scratch.ts | 233 ++- 3 files changed, 2079 insertions(+), 1854 deletions(-) diff --git a/packages/code/src/native-sandbox.test.ts b/packages/code/src/native-sandbox.test.ts index 9289271..9207e19 100644 --- a/packages/code/src/native-sandbox.test.ts +++ b/packages/code/src/native-sandbox.test.ts @@ -2,17 +2,17 @@ import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { EventEmitter } from 'node:events'; import { - access, - chmod, - mkdtemp, - mkdir, - open, - realpath, - rename, - rm, - stat, - symlink, - writeFile, + access, + chmod, + mkdtemp, + mkdir, + open, + realpath, + rename, + rm, + stat, + symlink, + writeFile, } from 'node:fs/promises'; import { tmpdir, homedir } from 'node:os'; import { join } from 'node:path'; @@ -27,1102 +27,1213 @@ import { restoreScratchTraversal } from './native-scratch.js'; import { WorkspaceToolError } from './workspace.js'; const request = { - protocolVersion: 1 as const, - operation: 'execute_command' as const, - workspaceId: 'primary', - command: 'printf hello', - timeoutMs: 1_000, - maxOutputBytes: 64, + protocolVersion: 1 as const, + operation: 'execute_command' as const, + workspaceId: 'primary', + command: 'printf hello', + timeoutMs: 1_000, + maxOutputBytes: 64, }; function fakeManager( - options: { - dependencyErrors?: string[]; - beforeWrap?: () => Promise; - appendGitSafeDirectory?: boolean; - inheritedGitEnvironment?: Record; - initializeError?: Error; - wrappedEnvironment?: NodeJS.ProcessEnv; - } = {}, + options: { + dependencyErrors?: string[]; + beforeWrap?: () => Promise; + appendGitSafeDirectory?: boolean; + inheritedGitEnvironment?: Record; + initializeError?: Error; + wrappedEnvironment?: NodeJS.ProcessEnv; + } = {}, ) { - let config: SandboxRuntimeConfig | undefined; - let reset = false; - let credentialSeenDuringWrap: string | undefined; - let gitLfsRequiredSeenDuringWrap: string | undefined; - let scratchSelectorSeenDuringWrap: string | undefined; - const manager = { - isSupportedPlatform: () => true, - async checkDependenciesAsync() { - return { warnings: [], errors: options.dependencyErrors ?? [] }; - }, - async initialize(value: SandboxRuntimeConfig) { - config = value; - if (options.initializeError) throw options.initializeError; - }, - async wrapWithSandboxArgv(command: string) { - await options.beforeWrap?.(); - credentialSeenDuringWrap = process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - gitLfsRequiredSeenDuringWrap = process.env.GIT_CONFIG_VALUE_3; - scratchSelectorSeenDuringWrap = process.env.CLAUDE_CODE_TMPDIR; - const ambientGitEnvironment = Object.fromEntries( - Object.entries(process.env).filter( - ([name, value]) => name.startsWith('GIT_CONFIG_') && value != null, - ), - ); - let gitEnvironment = ambientGitEnvironment; - if (options.appendGitSafeDirectory) { - const index = Number(ambientGitEnvironment.GIT_CONFIG_COUNT ?? '0'); - gitEnvironment = { - ...(options.inheritedGitEnvironment ?? {}), - GIT_CONFIG_COUNT: String(index + 1), - [`GIT_CONFIG_KEY_${index}`]: 'safe.directory', - [`GIT_CONFIG_VALUE_${index}`]: '/workspace', - }; - } - return { - argv: ['/bin/bash', '-c', command], - env: { - PATH: process.env.PATH, - ...gitEnvironment, - ...options.wrappedEnvironment, - ...(credentialSeenDuringWrap - ? { - LIBRECHAT_CODE_TEST_CREDENTIAL: - 'Authorization: Bearer srt-sentinel', - } - : {}), + let config: SandboxRuntimeConfig | undefined; + let reset = false; + let credentialSeenDuringWrap: string | undefined; + let gitLfsRequiredSeenDuringWrap: string | undefined; + let scratchSelectorSeenDuringWrap: string | undefined; + const manager = { + isSupportedPlatform: () => true, + async checkDependenciesAsync() { + return { warnings: [], errors: options.dependencyErrors ?? [] }; + }, + async initialize(value: SandboxRuntimeConfig) { + config = value; + if (options.initializeError) throw options.initializeError; + }, + async wrapWithSandboxArgv(command: string) { + await options.beforeWrap?.(); + credentialSeenDuringWrap = + process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + gitLfsRequiredSeenDuringWrap = process.env.GIT_CONFIG_VALUE_3; + scratchSelectorSeenDuringWrap = process.env.CLAUDE_CODE_TMPDIR; + const ambientGitEnvironment = Object.fromEntries( + Object.entries(process.env).filter( + ([name, value]) => + name.startsWith('GIT_CONFIG_') && value != null, + ), + ); + let gitEnvironment = ambientGitEnvironment; + if (options.appendGitSafeDirectory) { + const index = Number( + ambientGitEnvironment.GIT_CONFIG_COUNT ?? '0', + ); + gitEnvironment = { + ...(options.inheritedGitEnvironment ?? {}), + GIT_CONFIG_COUNT: String(index + 1), + [`GIT_CONFIG_KEY_${index}`]: 'safe.directory', + [`GIT_CONFIG_VALUE_${index}`]: '/workspace', + }; + } + return { + argv: ['/bin/bash', '-c', command], + env: { + PATH: process.env.PATH, + ...gitEnvironment, + ...options.wrappedEnvironment, + ...(credentialSeenDuringWrap + ? { + LIBRECHAT_CODE_TEST_CREDENTIAL: + 'Authorization: Bearer srt-sentinel', + } + : {}), + }, + }; + }, + annotateStderrWithSandboxFailures(_commandId: string, stderr: string) { + return stderr; + }, + cleanupAfterCommand() {}, + async reset() { + reset = true; + }, + }; + return { + manager, + get config() { + return config; + }, + get reset() { + return reset; + }, + get credentialSeenDuringWrap() { + return credentialSeenDuringWrap; + }, + get gitLfsRequiredSeenDuringWrap() { + return gitLfsRequiredSeenDuringWrap; }, - }; - }, - annotateStderrWithSandboxFailures(_commandId: string, stderr: string) { - return stderr; - }, - cleanupAfterCommand() {}, - async reset() { - reset = true; - }, - }; - return { - manager, - get config() { - return config; - }, - get reset() { - return reset; - }, - get credentialSeenDuringWrap() { - return credentialSeenDuringWrap; - }, - get gitLfsRequiredSeenDuringWrap() { - return gitLfsRequiredSeenDuringWrap; - }, - get scratchSelectorSeenDuringWrap() { - return scratchSelectorSeenDuringWrap; - }, - }; + get scratchSelectorSeenDuringWrap() { + return scratchSelectorSeenDuringWrap; + }, + }; } -test('exclusive lifecycle rejects a second workspace sharing an SRT manager', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - const first = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - const second = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - t.after(() => first.close()); - t.after(() => second.close()); - await first.prepare(); - await assert.rejects( - second.prepare(), - /already belongs to another workspace/, - ); - await second.close(); - assert.equal( - fake.reset, - false, - 'a rejected owner must not reset the live manager', - ); - assert.equal((await first.execute(request)).stdout, 'hello'); - await first.close(); - await second.prepare(); - assert.equal((await second.execute(request)).stdout, 'hello'); +test('exclusive lifecycle rejects a second workspace sharing an SRT manager', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + const first = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + const second = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + t.after(() => first.close()); + t.after(() => second.close()); + await first.prepare(); + await assert.rejects( + second.prepare(), + /already belongs to another workspace/, + ); + await second.close(); + assert.equal( + fake.reset, + false, + 'a rejected owner must not reset the live manager', + ); + assert.equal((await first.execute(request)).stdout, 'hello'); + await first.close(); + await second.prepare(); + assert.equal((await second.execute(request)).stdout, 'hello'); }); -test('exclusive lifecycle rejects overlapping commands and waits before resetting', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - let entered!: () => void; - const wrapping = new Promise((resolve) => { - entered = resolve; - }); - let release!: () => void; - const gate = new Promise((resolve) => { - release = resolve; - }); - const fake = fakeManager({ - beforeWrap: async () => { - entered(); - await gate; - }, - }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - t.after(() => sandbox.close()); - const execution = sandbox.execute(request); - await wrapping; - await assert.rejects(sandbox.execute(request), /active command/); - const closing = sandbox.close(); - const secondClose = sandbox.close(); - await new Promise((resolve) => setImmediate(resolve)); - assert.equal(fake.reset, false); - await assert.rejects(sandbox.prepare(), /closing/); - release(); - assert.equal((await execution).stdout, 'hello'); - await Promise.all([closing, secondClose]); - assert.equal(fake.reset, true); +test('exclusive lifecycle rejects overlapping commands and waits before resetting', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + let entered!: () => void; + const wrapping = new Promise(resolve => { + entered = resolve; + }); + let release!: () => void; + const gate = new Promise(resolve => { + release = resolve; + }); + const fake = fakeManager({ + beforeWrap: async () => { + entered(); + await gate; + }, + }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + t.after(() => sandbox.close()); + const execution = sandbox.execute(request); + await wrapping; + await assert.rejects(sandbox.execute(request), /active command/); + const closing = sandbox.close(); + const secondClose = sandbox.close(); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(fake.reset, false); + await assert.rejects(sandbox.prepare(), /closing/); + release(); + assert.equal((await execution).stdout, 'hello'); + await Promise.all([closing, secondClose]); + assert.equal(fake.reset, true); }); -test('exclusive lifecycle retains ownership after a failed reset until cleanup succeeds', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - let failReset = true; - fake.manager.reset = async () => { - if (failReset) throw new Error('reset failed'); - }; - const first = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - const second = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - await first.prepare(); - await assert.rejects(first.close(), /reset failed/); - await assert.rejects(first.execute(request), /requires cleanup/); - await assert.rejects(second.prepare(), /already belongs/); - failReset = false; - await first.close(); - await second.prepare(); - await second.close(); +test('exclusive lifecycle retains ownership after a failed reset until cleanup succeeds', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + let failReset = true; + fake.manager.reset = async () => { + if (failReset) throw new Error('reset failed'); + }; + const first = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + const second = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + await first.prepare(); + await assert.rejects(first.close(), /reset failed/); + await assert.rejects(first.execute(request), /requires cleanup/); + await assert.rejects(second.prepare(), /already belongs/); + failReset = false; + await first.close(); + await second.prepare(); + await second.close(); }); -test('exclusive lifecycle waits for initialization before resetting the manager', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - let entered!: () => void; - const initializing = new Promise((resolve) => { - entered = resolve; - }); - let release!: () => void; - const gate = new Promise((resolve) => { - release = resolve; - }); - fake.manager.initialize = async () => { - entered(); - await gate; - }; - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - const preparing = sandbox.prepare(); - await initializing; - const closing = sandbox.close(); - await new Promise((resolve) => setImmediate(resolve)); - assert.equal(fake.reset, false); - release(); - await preparing; - await closing; - assert.equal(fake.reset, true); +test('exclusive lifecycle waits for initialization before resetting the manager', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + let entered!: () => void; + const initializing = new Promise(resolve => { + entered = resolve; + }); + let release!: () => void; + const gate = new Promise(resolve => { + release = resolve; + }); + fake.manager.initialize = async () => { + entered(); + await gate; + }; + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + const preparing = sandbox.prepare(); + await initializing; + const closing = sandbox.close(); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(fake.reset, false); + release(); + await preparing; + await closing; + assert.equal(fake.reset, true); }); -test('initializes SRT with a default-deny network and scrubbed worker credentials', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - const identity = join(tmpdir(), 'librechat-code-identity.json'); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - protectedPaths: [identity], - environment: { - PATH: '/usr/bin', - Path: '/windows/system32', - LANG: 'en_US.UTF-8', - lc_api_token: 'lowercase-secret', - LIBRECHAT_CODE_WORKER_TOKEN: 'secret', - AWS_SECRET_ACCESS_KEY: 'secret', - }, - manager: fake.manager, - }); - - await sandbox.prepare(); - const canonicalRoot = await realpath(root); - const canonicalIdentity = await realpath(identity).catch(async () => - join(await realpath(tmpdir()), 'librechat-code-identity.json'), - ); - const canonicalHome = await realpath(homedir()); - const scratchDirectory = fake.config?.filesystem.allowWrite[1]; - assert.equal(typeof scratchDirectory, 'string'); - assert.deepEqual(fake.config?.network.allowedDomains, []); - assert.equal(fake.config?.network.strictAllowlist, true); - assert.equal(fake.config?.network.allowAllUnixSockets, false); - assert.deepEqual(fake.config?.filesystem.allowRead, [ - canonicalRoot, - scratchDirectory, - ]); - assert.deepEqual(fake.config?.filesystem.allowWrite, [ - canonicalRoot, - scratchDirectory, - ]); - assert.equal((await stat(scratchDirectory!)).mode & 0o777, 0o700); - assert.ok(fake.config?.filesystem.denyRead.includes(canonicalHome)); - assert.ok(fake.config?.filesystem.denyWrite.includes(canonicalIdentity)); - assert.ok( - fake.config?.filesystem.denyWrite.some((path) => - path.endsWith('/tmp/claude'), - ), - ); - const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); - assert.ok(denied?.includes('LIBRECHAT_CODE_WORKER_TOKEN')); - assert.ok(denied?.includes('AWS_SECRET_ACCESS_KEY')); - assert.ok(!denied?.includes('PATH')); - assert.ok(denied?.includes('Path')); - assert.ok(denied?.includes('lc_api_token')); - assert.ok(denied?.includes('CLAUDE_CODE_TMPDIR')); - assert.ok(denied?.includes('CLAUDE_TMPDIR')); - await sandbox.close(); - assert.equal(fake.reset, true); - await assert.rejects(access(scratchDirectory!)); +test('initializes SRT with a default-deny network and scrubbed worker credentials', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + const identity = join(tmpdir(), 'librechat-code-identity.json'); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + protectedPaths: [identity], + environment: { + PATH: '/usr/bin', + Path: '/windows/system32', + LANG: 'en_US.UTF-8', + lc_api_token: 'lowercase-secret', + LIBRECHAT_CODE_WORKER_TOKEN: 'secret', + AWS_SECRET_ACCESS_KEY: 'secret', + }, + manager: fake.manager, + }); + + await sandbox.prepare(); + const canonicalRoot = await realpath(root); + const canonicalIdentity = await realpath(identity).catch(async () => + join(await realpath(tmpdir()), 'librechat-code-identity.json'), + ); + const canonicalHome = await realpath(homedir()); + const scratchDirectory = fake.config?.filesystem.allowWrite[1]; + assert.equal(typeof scratchDirectory, 'string'); + assert.deepEqual(fake.config?.network.allowedDomains, []); + assert.equal(fake.config?.network.strictAllowlist, true); + assert.equal(fake.config?.network.allowAllUnixSockets, false); + assert.deepEqual(fake.config?.filesystem.allowRead, [ + canonicalRoot, + scratchDirectory, + ]); + assert.deepEqual(fake.config?.filesystem.allowWrite, [ + canonicalRoot, + scratchDirectory, + ]); + assert.equal((await stat(scratchDirectory!)).mode & 0o777, 0o700); + assert.ok(fake.config?.filesystem.denyRead.includes(canonicalHome)); + assert.ok(fake.config?.filesystem.denyWrite.includes(canonicalIdentity)); + assert.ok( + fake.config?.filesystem.denyWrite.some(path => + path.endsWith('/tmp/claude'), + ), + ); + const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); + assert.ok(denied?.includes('LIBRECHAT_CODE_WORKER_TOKEN')); + assert.ok(denied?.includes('AWS_SECRET_ACCESS_KEY')); + assert.ok(!denied?.includes('PATH')); + assert.ok(denied?.includes('Path')); + assert.ok(denied?.includes('lc_api_token')); + assert.ok(denied?.includes('CLAUDE_CODE_TMPDIR')); + assert.ok(denied?.includes('CLAUDE_TMPDIR')); + await sandbox.close(); + assert.equal(fake.reset, true); + await assert.rejects(access(scratchDirectory!)); }); -test('provides an isolated scratch directory to commands and restores the host environment', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const originalTmpdir = process.env.TMPDIR; - const originalSrtTmpdir = process.env.CLAUDE_CODE_TMPDIR; - const originalLegacySrtTmpdir = process.env.CLAUDE_TMPDIR; - const fake = fakeManager(); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - - const result = await sandbox.execute({ - ...request, - maxOutputBytes: 1_024, - command: 'touch "$TMPDIR/probe" && printf %s "$TMPDIR"', - }); - - assert.equal(result.exitCode, 0); - assert.match(result.stdout, /librechat-code-srt-/); - await access(join(result.stdout, 'probe')); - assert.equal(fake.scratchSelectorSeenDuringWrap, result.stdout); - assert.equal(process.env.TMPDIR, originalTmpdir); - assert.equal(process.env.CLAUDE_CODE_TMPDIR, originalSrtTmpdir); - assert.equal(process.env.CLAUDE_TMPDIR, originalLegacySrtTmpdir); - await sandbox.close(); - await assert.rejects(access(result.stdout)); +test('provides an isolated scratch directory to commands and restores the host environment', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const originalTmpdir = process.env.TMPDIR; + const originalSrtTmpdir = process.env.CLAUDE_CODE_TMPDIR; + const originalLegacySrtTmpdir = process.env.CLAUDE_TMPDIR; + const fake = fakeManager(); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + + const result = await sandbox.execute({ + ...request, + maxOutputBytes: 1_024, + command: 'touch "$TMPDIR/probe" && printf %s "$TMPDIR"', + }); + + assert.equal(result.exitCode, 0); + assert.match(result.stdout, /librechat-code-srt-/); + await access(join(result.stdout, 'probe')); + assert.equal(fake.scratchSelectorSeenDuringWrap, result.stdout); + assert.equal(process.env.TMPDIR, originalTmpdir); + assert.equal(process.env.CLAUDE_CODE_TMPDIR, originalSrtTmpdir); + assert.equal(process.env.CLAUDE_TMPDIR, originalLegacySrtTmpdir); + await sandbox.close(); + await assert.rejects(access(result.stdout)); }); -test('removes scratch storage when SRT initialization fails', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager({ initializeError: new Error('init failed') }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - - await assert.rejects(sandbox.prepare(), /init failed/); - const scratchDirectory = fake.config?.filesystem.allowWrite[1]; - assert.equal(typeof scratchDirectory, 'string'); - await assert.rejects(access(scratchDirectory!)); - assert.equal(fake.reset, true); +test('removes scratch storage when SRT initialization fails', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager({ initializeError: new Error('init failed') }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + + await assert.rejects(sandbox.prepare(), /init failed/); + const scratchDirectory = fake.config?.filesystem.allowWrite[1]; + assert.equal(typeof scratchDirectory, 'string'); + await assert.rejects(access(scratchDirectory!)); + assert.equal(fake.reset, true); }); -test('rejects workspaces nested inside SRT shared scratch storage', async (t) => { - if (process.platform === 'win32') return; - const sharedRoot = '/tmp/claude'; - await mkdir(sharedRoot, { recursive: true }); - const root = await mkdtemp(join(sharedRoot, 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fakeManager().manager, - }); - - await assert.rejects( - sandbox.prepare(), - (error: WorkspaceToolError) => - error.code === 'REGISTRATION_INVALID' && - /inherited writable path/.test(error.message), - ); +test('rejects workspaces nested inside SRT shared scratch storage', async t => { + if (process.platform === 'win32') return; + const sharedRoot = '/tmp/claude'; + await mkdir(sharedRoot, { recursive: true }); + const root = await mkdtemp(join(sharedRoot, 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fakeManager().manager, + }); + + await assert.rejects( + sandbox.prepare(), + (error: WorkspaceToolError) => + error.code === 'REGISTRATION_INVALID' && + /inherited writable path/.test(error.message), + ); }); test('rejects a workspace that contains worker scratch storage', async () => { - if (process.platform === 'win32') return; - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: tmpdir(), - manager: fakeManager().manager, - }); - - await assert.rejects( - sandbox.prepare(), - (error: WorkspaceToolError) => - error.code === 'REGISTRATION_INVALID' && - /contain worker scratch storage/.test(error.message), - ); - await sandbox.close(); + if (process.platform === 'win32') return; + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: tmpdir(), + manager: fakeManager().manager, + }); + + await assert.rejects( + sandbox.prepare(), + (error: WorkspaceToolError) => + error.code === 'REGISTRATION_INVALID' && + /contain worker scratch storage/.test(error.message), + ); + await sandbox.close(); +}); + +test('keeps concurrent sandbox scratch directories independent', async t => { + const firstRoot = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + const secondRoot = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(firstRoot, { recursive: true, force: true })); + t.after(() => rm(secondRoot, { recursive: true, force: true })); + let releaseWrap!: () => void; + let wrapStarted!: () => void; + const wrapStartedPromise = new Promise(resolve => { + wrapStarted = resolve; + }); + const holdWrap = new Promise(resolve => { + releaseWrap = resolve; + }); + const firstFake = fakeManager({ + async beforeWrap() { + wrapStarted(); + await holdWrap; + }, + }); + const secondFake = fakeManager(); + const firstSandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: firstRoot, + manager: firstFake.manager, + }); + const secondSandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: secondRoot, + manager: secondFake.manager, + }); + const firstExecution = firstSandbox.execute({ + ...request, + command: 'printf first', + }); + await wrapStartedPromise; + await secondSandbox.prepare(); + const firstScratch = firstFake.config?.filesystem.allowWrite[1]; + const secondScratch = secondFake.config?.filesystem.allowWrite[1]; + assert.equal(typeof firstScratch, 'string'); + assert.equal(typeof secondScratch, 'string'); + assert.notEqual(firstScratch, secondScratch); + assert.ok(!secondScratch!.startsWith(`${firstScratch}/`)); + releaseWrap(); + await firstExecution; + await firstSandbox.close(); + await access(secondScratch!); + await secondSandbox.close(); }); -test('keeps concurrent sandbox scratch directories independent', async (t) => { - const firstRoot = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - const secondRoot = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(firstRoot, { recursive: true, force: true })); - t.after(() => rm(secondRoot, { recursive: true, force: true })); - let releaseWrap!: () => void; - let wrapStarted!: () => void; - const wrapStartedPromise = new Promise((resolve) => { - wrapStarted = resolve; - }); - const holdWrap = new Promise((resolve) => { - releaseWrap = resolve; - }); - const firstFake = fakeManager({ - async beforeWrap() { - wrapStarted(); - await holdWrap; - }, - }); - const secondFake = fakeManager(); - const firstSandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: firstRoot, - manager: firstFake.manager, - }); - const secondSandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: secondRoot, - manager: secondFake.manager, - }); - const firstExecution = firstSandbox.execute({ - ...request, - command: 'printf first', - }); - await wrapStartedPromise; - await secondSandbox.prepare(); - const firstScratch = firstFake.config?.filesystem.allowWrite[1]; - const secondScratch = secondFake.config?.filesystem.allowWrite[1]; - assert.equal(typeof firstScratch, 'string'); - assert.equal(typeof secondScratch, 'string'); - assert.notEqual(firstScratch, secondScratch); - assert.ok(!secondScratch!.startsWith(`${firstScratch}/`)); - releaseWrap(); - await firstExecution; - await firstSandbox.close(); - await access(secondScratch!); - await secondSandbox.close(); +test('removes scratch storage after a command revokes traversal permissions', async t => { + if (process.platform === 'win32') return; + 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, + }); + + const result = await sandbox.execute({ + ...request, + command: + 'printf %s "$TMPDIR"; mkdir -p "$TMPDIR/locked/deeper"; touch "$TMPDIR/locked/deeper/file"; chmod 000 "$TMPDIR/locked/deeper" "$TMPDIR/locked" "$TMPDIR"', + }); + + assert.equal(result.exitCode, 0); + await sandbox.close(); + await assert.rejects(access(result.stdout)); +}); + +test('scratch traversal never follows a descendant replaced after inspection', async t => { + if (process.platform === 'win32') return; + const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-race-')); + const outside = await mkdtemp(join(tmpdir(), 'librechat-code-outside-')); + const descendant = join(root, 'locked'); + const retired = join(root, 'retired'); + const outsideChild = join(outside, 'child'); + t.after(() => rm(root, { recursive: true, force: true })); + t.after(() => rm(outside, { recursive: true, force: true })); + await mkdir(descendant); + await mkdir(outsideChild); + await chmod(outside, 0o711); + await chmod(outsideChild, 0o711); + const rootHandle = await open(root, 'r'); + t.after(() => rootHandle.close()); + let swapped = false; + + await restoreScratchTraversal(rootHandle, { + async afterEntryInspected(_directoryFd, name) { + if (name !== 'locked' || swapped) return; + swapped = true; + await rename(descendant, retired); + await symlink(outside, descendant, 'dir'); + }, + }); + + assert.equal(swapped, true); + assert.equal((await stat(outside)).mode & 0o777, 0o711); + assert.equal((await stat(outsideChild)).mode & 0o777, 0o711); }); -test('removes scratch storage after a command revokes traversal permissions', async (t) => { - if (process.platform === 'win32') return; - 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, - }); - - const result = await sandbox.execute({ - ...request, - command: - 'printf %s "$TMPDIR"; mkdir -p "$TMPDIR/locked/deeper"; touch "$TMPDIR/locked/deeper/file"; chmod 000 "$TMPDIR/locked/deeper" "$TMPDIR/locked" "$TMPDIR"', - }); - - assert.equal(result.exitCode, 0); - await sandbox.close(); - await assert.rejects(access(result.stdout)); +test('scratch traversal removes command-created Darwin ACLs', async t => { + if (process.platform !== 'darwin') return; + 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, + }); + const result = await sandbox.execute({ + ...request, + command: + 'printf %s "$TMPDIR"; mkdir -p "$TMPDIR/locked/deeper"; touch "$TMPDIR/locked/deeper/file"; chmod +a "$USER deny list,search,delete_child" "$TMPDIR/locked" "$TMPDIR"; chmod 000 "$TMPDIR/locked" "$TMPDIR"', + }); + + assert.equal(result.exitCode, 0); + await sandbox.close(); + await assert.rejects(access(result.stdout)); }); -test('scratch traversal never follows a descendant replaced after inspection', async (t) => { - if (process.platform === 'win32') return; - const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-race-')); - const outside = await mkdtemp(join(tmpdir(), 'librechat-code-outside-')); - const descendant = join(root, 'locked'); - const retired = join(root, 'retired'); - const outsideChild = join(outside, 'child'); - t.after(() => rm(root, { recursive: true, force: true })); - t.after(() => rm(outside, { recursive: true, force: true })); - await mkdir(descendant); - await mkdir(outsideChild); - await chmod(outside, 0o711); - await chmod(outsideChild, 0o711); - const rootHandle = await open(root, 'r'); - t.after(() => rootHandle.close()); - let swapped = false; - - await restoreScratchTraversal(rootHandle, { - async afterEntryInspected(_directoryFd, name) { - if (name !== 'locked' || swapped) return; - swapped = true; - await rename(descendant, retired); - await symlink(outside, descendant, 'dir'); - }, - }); - - assert.equal(swapped, true); - assert.equal((await stat(outside)).mode & 0o777, 0o711); - assert.equal((await stat(outsideChild)).mode & 0o777, 0o711); +test('scratch traversal bounds descriptors and work across a deep tree', async t => { + if (process.platform === 'win32') return; + const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-depth-')); + t.after(() => rm(root, { recursive: true, force: true })); + const directories = [root]; + for (let depth = 0; depth < 100; depth += 1) { + directories.push(join(directories[directories.length - 1], 'd')); + await mkdir(directories[directories.length - 1]); + } + for (const directory of directories.slice(1).reverse()) { + await chmod(directory, 0o000); + } + const rootHandle = await open(root, 'r'); + t.after(() => rootHandle.close()); + + await restoreScratchTraversal(rootHandle); + + assert.equal( + (await stat(directories[directories.length - 1])).mode & 0o777, + 0o700, + ); }); -test('scratch traversal removes command-created Darwin ACLs', async (t) => { - if (process.platform !== 'darwin') return; - 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, - }); - const result = await sandbox.execute({ - ...request, - command: - 'printf %s "$TMPDIR"; mkdir -p "$TMPDIR/locked/deeper"; touch "$TMPDIR/locked/deeper/file"; chmod +a "$USER deny list,search,delete_child" "$TMPDIR/locked" "$TMPDIR"; chmod 000 "$TMPDIR/locked" "$TMPDIR"', - }); - - assert.equal(result.exitCode, 0); - await sandbox.close(); - await assert.rejects(access(result.stdout)); +test('scratch traversal rejects trees beyond its recovery depth limit', async t => { + if (process.platform === 'win32') return; + const root = await mkdtemp( + join(tmpdir(), 'librechat-code-scratch-depth-limit-'), + ); + t.after(() => rm(root, { recursive: true, force: true })); + let directory = root; + for (let depth = 0; depth < 129; depth += 1) { + directory = join(directory, 'd'); + await mkdir(directory); + } + const rootHandle = await open(root, 'r'); + t.after(() => rootHandle.close()); + + await assert.rejects( + restoreScratchTraversal(rootHandle), + /scratch cleanup exceeded its depth limit/, + ); }); -test('scratch traversal bounds descriptors across a deep tree', async (t) => { - if (process.platform === 'win32') return; - const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-depth-')); - t.after(() => rm(root, { recursive: true, force: true })); - const directories = [root]; - for (let depth = 0; depth < 300; depth += 1) { - directories.push(join(directories[directories.length - 1], 'd')); - await mkdir(directories[directories.length - 1]); - } - for (const directory of directories.slice(1).reverse()) { - await chmod(directory, 0o000); - } - const rootHandle = await open(root, 'r'); - t.after(() => rootHandle.close()); - - await restoreScratchTraversal(rootHandle); - - assert.equal((await stat(directories[directories.length - 1])).mode & 0o777, 0o700); +test('does not replace scratch state while cleanup remains pending', async t => { + if (process.platform === 'win32') return; + const workspace = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + const retained = await mkdtemp(join(tmpdir(), 'librechat-code-retained-')); + const retainedHandle = await open(retained, 'r'); + t.after(() => retainedHandle.close()); + t.after(() => rm(retained, { recursive: true, force: true })); + t.after(() => rm(workspace, { recursive: true, force: true })); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: workspace, + manager: fakeManager().manager, + }); + const mutable = sandbox as unknown as { + scratchDirectory?: string; + scratchHandle?: typeof retainedHandle; + createScratchDirectory(paths: string[]): Promise; + }; + mutable.scratchDirectory = retained; + mutable.scratchHandle = retainedHandle; + + await assert.rejects( + mutable.createScratchDirectory([]), + /scratch cleanup is still pending/, + ); + assert.equal(mutable.scratchDirectory, retained); + assert.equal(mutable.scratchHandle, retainedHandle); }); const proxyEnvironment = { - HTTP_PROXY: 'http://upstream.invalid:8080', - HTTPS_PROXY: 'http://upstream.invalid:8080', - ALL_PROXY: 'socks5://upstream.invalid:1080', - NO_PROXY: 'upstream.internal', - http_proxy: 'http://upstream.invalid:8080', - https_proxy: 'http://upstream.invalid:8080', - all_proxy: 'socks5://upstream.invalid:1080', - no_proxy: 'upstream.internal', + HTTP_PROXY: 'http://upstream.invalid:8080', + HTTPS_PROXY: 'http://upstream.invalid:8080', + ALL_PROXY: 'socks5://upstream.invalid:1080', + NO_PROXY: 'upstream.internal', + http_proxy: 'http://upstream.invalid:8080', + https_proxy: 'http://upstream.invalid:8080', + all_proxy: 'socks5://upstream.invalid:1080', + no_proxy: 'upstream.internal', }; const windowsEnvironment = { - SYSTEMROOT: 'C:\\Windows', - SystemRoot: 'C:\\Windows', - SYSTEMDRIVE: 'C:', - windir: 'C:\\Windows', - ComSpec: 'C:\\Windows\\System32\\cmd.exe', - PATHEXT: '.COM;.EXE;.BAT;.CMD', - TEMP: 'C:\\Temp', - Temp: 'C:\\Temp', - TMP: 'C:\\Temp', - USERPROFILE: 'C:\\Users\\sandbox', - HOMEDRIVE: 'C:', - HOMEPATH: '\\Users\\sandbox', - APPDATA: 'C:\\Users\\sandbox\\AppData\\Roaming', - LOCALAPPDATA: 'C:\\Users\\sandbox\\AppData\\Local', + SYSTEMROOT: 'C:\\Windows', + SystemRoot: 'C:\\Windows', + SYSTEMDRIVE: 'C:', + windir: 'C:\\Windows', + ComSpec: 'C:\\Windows\\System32\\cmd.exe', + PATHEXT: '.COM;.EXE;.BAT;.CMD', + TEMP: 'C:\\Temp', + Temp: 'C:\\Temp', + TMP: 'C:\\Temp', + USERPROFILE: 'C:\\Users\\sandbox', + HOMEDRIVE: 'C:', + HOMEPATH: '\\Users\\sandbox', + APPDATA: 'C:\\Users\\sandbox\\AppData\\Roaming', + LOCALAPPDATA: 'C:\\Users\\sandbox\\AppData\\Local', }; for (const platform of ['darwin', 'linux', 'win32'] as const) { - test(`preserves required ${platform} environment names without allowing credentials`, async (t) => { + test(`preserves required ${platform} environment names without allowing credentials`, async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + const credentials = { + LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret', + LIBRECHAT_CODE_HTTP_PROXY: 'worker-secret', + AWS_SECRET_ACCESS_KEY: 'aws-secret', + GITHUB_TOKEN: 'github-secret', + HTTP_PROXY_TOKEN: 'proxy-secret', + CUSTOM_PROXY: 'proxy-secret', + SYSTEMROOT_TOKEN: 'runtime-secret', + NODE_OPTIONS: '--require /host/private.js', + LD_PRELOAD: '/host/private.so', + }; + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + platform, + allowedDomains: ['github.com'], + environment: { + ...proxyEnvironment, + ...windowsEnvironment, + ...credentials, + HtTp_PrOxY: 'http://mixed-case.invalid:8080', + PATH: '/usr/bin', + LC_ALL: 'C.UTF-8', + }, + manager: fake.manager, + }); + t.after(() => sandbox.close()); + await sandbox.prepare(); + const denied = new Set( + fake.config?.credentials?.envVars + ?.filter(({ mode }) => mode === 'deny') + .map(({ name }) => name), + ); + for (const name of [ + ...Object.keys(proxyEnvironment), + 'PATH', + 'LC_ALL', + ]) { + assert.equal( + denied.has(name), + false, + `${name} must remain available`, + ); + } + for (const name of Object.keys(windowsEnvironment)) { + assert.equal( + denied.has(name), + platform !== 'win32', + `${name} must be platform-specific`, + ); + } + assert.equal(denied.has('HtTp_PrOxY'), platform !== 'win32'); + for (const name of Object.keys(credentials)) { + assert.equal(denied.has(name), true, `${name} must remain denied`); + } + assert.deepEqual(fake.config?.network.allowedDomains, ['github.com']); + assert.equal(fake.config?.network.strictAllowlist, true); + }); +} + +test('uses SRT proxy values without restoring inherited proxies or credentials', async t => { const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - const credentials = { - LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret', - LIBRECHAT_CODE_HTTP_PROXY: 'worker-secret', - AWS_SECRET_ACCESS_KEY: 'aws-secret', - GITHUB_TOKEN: 'github-secret', - HTTP_PROXY_TOKEN: 'proxy-secret', - CUSTOM_PROXY: 'proxy-secret', - SYSTEMROOT_TOKEN: 'runtime-secret', - NODE_OPTIONS: '--require /host/private.js', - LD_PRELOAD: '/host/private.so', + const wrappedEnvironment = { + HTTP_PROXY: 'http://localhost:3128', + HTTPS_PROXY: 'http://localhost:3128', + ALL_PROXY: 'http://localhost:3128', + NO_PROXY: 'localhost', }; + const fake = fakeManager({ wrappedEnvironment }); const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, platform, allowedDomains: ['github.com'], - environment: { - ...proxyEnvironment, ...windowsEnvironment, ...credentials, - HtTp_PrOxY: 'http://mixed-case.invalid:8080', - PATH: '/usr/bin', LC_ALL: 'C.UTF-8', - }, - manager: fake.manager, + workspaceRoot: root, + environment: { ...proxyEnvironment, GITHUB_TOKEN: 'host-secret' }, + manager: fake.manager, }); t.after(() => sandbox.close()); - await sandbox.prepare(); - const denied = new Set(fake.config?.credentials?.envVars - ?.filter(({ mode }) => mode === 'deny').map(({ name }) => name)); - for (const name of [...Object.keys(proxyEnvironment), 'PATH', 'LC_ALL']) { - assert.equal(denied.has(name), false, `${name} must remain available`); - } - for (const name of Object.keys(windowsEnvironment)) { - assert.equal(denied.has(name), platform !== 'win32', `${name} must be platform-specific`); - } - assert.equal(denied.has('HtTp_PrOxY'), platform !== 'win32'); - for (const name of Object.keys(credentials)) { - assert.equal(denied.has(name), true, `${name} must remain denied`); - } - assert.deepEqual(fake.config?.network.allowedDomains, ['github.com']); - assert.equal(fake.config?.network.strictAllowlist, true); - }); -} - -test('uses SRT proxy values without restoring inherited proxies or credentials', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const wrappedEnvironment = { - HTTP_PROXY: 'http://localhost:3128', HTTPS_PROXY: 'http://localhost:3128', - ALL_PROXY: 'http://localhost:3128', NO_PROXY: 'localhost', - }; - const fake = fakeManager({ wrappedEnvironment }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - environment: { ...proxyEnvironment, GITHUB_TOKEN: 'host-secret' }, - manager: fake.manager, - }); - t.after(() => sandbox.close()); - const result = await sandbox.execute({ - ...request, maxOutputBytes: 256, - command: 'printf "%s|%s|%s|%s|%s" "$HTTP_PROXY" "$HTTPS_PROXY" "$ALL_PROXY" "$NO_PROXY" "${GITHUB_TOKEN-unset}"', - }); - assert.equal(result.exitCode, 0); - assert.equal(result.stdout, `${Object.values(wrappedEnvironment).join('|')}|unset`); + const result = await sandbox.execute({ + ...request, + maxOutputBytes: 256, + command: + 'printf "%s|%s|%s|%s|%s" "$HTTP_PROXY" "$HTTPS_PROXY" "$ALL_PROXY" "$NO_PROXY" "${GITHUB_TOKEN-unset}"', + }); + assert.equal(result.exitCode, 0); + assert.equal( + result.stdout, + `${Object.values(wrappedEnvironment).join('|')}|unset`, + ); }); -test('masks a host credential for only its injection host and restores the parent environment', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - const original = process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - t.after(() => { - if (original === undefined) - delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - else process.env.LIBRECHAT_CODE_TEST_CREDENTIAL = original; - }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - allowedDomains: ['github.com'], - maskedEnvironment: { - variables: [ - { - name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', - extract: '^Authorization: Bearer (.+)$', - injectHosts: ['github.com'], +test('masks a host credential for only its injection host and restores the parent environment', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + const original = process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + t.after(() => { + if (original === undefined) + delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + else process.env.LIBRECHAT_CODE_TEST_CREDENTIAL = original; + }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + allowedDomains: ['github.com'], + maskedEnvironment: { + variables: [ + { + name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', + extract: '^Authorization: Bearer (.+)$', + injectHosts: ['github.com'], + }, + ], + async resolve() { + return { + LIBRECHAT_CODE_TEST_CREDENTIAL: + 'Authorization: Bearer real-secret', + }; + }, }, - ], - async resolve() { - return { - LIBRECHAT_CODE_TEST_CREDENTIAL: 'Authorization: Bearer real-secret', - }; - }, - }, - manager: fake.manager, - }); - - const result = await sandbox.execute({ - ...request, - command: 'printf %s "$LIBRECHAT_CODE_TEST_CREDENTIAL"', - }); - - assert.equal( - fake.credentialSeenDuringWrap, - 'Authorization: Bearer real-secret', - ); - assert.equal(result.stdout, 'Authorization: Bearer srt-sentinel'); - assert.equal(process.env.LIBRECHAT_CODE_TEST_CREDENTIAL, undefined); - assert.deepEqual(fake.config?.network.tlsTerminate, {}); - assert.deepEqual(fake.config?.credentials?.envVars?.at(-1), { - name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', - extract: '^Authorization: Bearer (.+)$', - injectHosts: ['github.com'], - mode: 'mask', - onExtractNoMatch: 'error', - }); + manager: fake.manager, + }); + + const result = await sandbox.execute({ + ...request, + command: 'printf %s "$LIBRECHAT_CODE_TEST_CREDENTIAL"', + }); + + assert.equal( + fake.credentialSeenDuringWrap, + 'Authorization: Bearer real-secret', + ); + assert.equal(result.stdout, 'Authorization: Bearer srt-sentinel'); + assert.equal(process.env.LIBRECHAT_CODE_TEST_CREDENTIAL, undefined); + assert.deepEqual(fake.config?.network.tlsTerminate, {}); + assert.deepEqual(fake.config?.credentials?.envVars?.at(-1), { + name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', + extract: '^Authorization: Bearer (.+)$', + injectHosts: ['github.com'], + mode: 'mask', + onExtractNoMatch: 'error', + }); }); -test('serializes credential handoff across concurrent sandbox instances', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const original = process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - t.after(() => { - if (original === undefined) - delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - else process.env.LIBRECHAT_CODE_TEST_CREDENTIAL = original; - }); - let firstEntered!: () => void; - const firstEnteredPromise = new Promise((resolve) => { - firstEntered = resolve; - }); - let releaseFirst!: () => void; - const firstGate = new Promise((resolve) => { - releaseFirst = resolve; - }); - let secondEntered = false; - const first = fakeManager({ - async beforeWrap() { - firstEntered(); - await firstGate; - }, - }); - const second = fakeManager({ - async beforeWrap() { - secondEntered = true; - }, - }); - const sandbox = ( - manager: ReturnType['manager'], - value: string, - ) => - new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - allowedDomains: ['github.com'], - maskedEnvironment: { - variables: [ - { - name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', - injectHosts: ['github.com'], - }, - ], - async resolve() { - return { LIBRECHAT_CODE_TEST_CREDENTIAL: value }; +test('serializes credential handoff across concurrent sandbox instances', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const original = process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + t.after(() => { + if (original === undefined) + delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + else process.env.LIBRECHAT_CODE_TEST_CREDENTIAL = original; + }); + let firstEntered!: () => void; + const firstEnteredPromise = new Promise(resolve => { + firstEntered = resolve; + }); + let releaseFirst!: () => void; + const firstGate = new Promise(resolve => { + releaseFirst = resolve; + }); + let secondEntered = false; + const first = fakeManager({ + async beforeWrap() { + firstEntered(); + await firstGate; + }, + }); + const second = fakeManager({ + async beforeWrap() { + secondEntered = true; }, - }, - manager, }); + const sandbox = ( + manager: ReturnType['manager'], + value: string, + ) => + new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + allowedDomains: ['github.com'], + maskedEnvironment: { + variables: [ + { + name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', + injectHosts: ['github.com'], + }, + ], + async resolve() { + return { LIBRECHAT_CODE_TEST_CREDENTIAL: value }; + }, + }, + manager, + }); - const firstExecution = sandbox(first.manager, 'first-secret').execute( - request, - ); - await firstEnteredPromise; - const secondExecution = sandbox(second.manager, 'second-secret').execute( - request, - ); - await new Promise((resolve) => setImmediate(resolve)); - assert.equal(secondEntered, false); - releaseFirst(); - await firstExecution; - await secondExecution; - - assert.equal(first.credentialSeenDuringWrap, 'first-secret'); - assert.equal(second.credentialSeenDuringWrap, 'second-secret'); - assert.equal(process.env.LIBRECHAT_CODE_TEST_CREDENTIAL, undefined); + const firstExecution = sandbox(first.manager, 'first-secret').execute( + request, + ); + await firstEnteredPromise; + const secondExecution = sandbox(second.manager, 'second-secret').execute( + request, + ); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(secondEntered, false); + releaseFirst(); + await firstExecution; + await secondExecution; + + assert.equal(first.credentialSeenDuringWrap, 'first-secret'); + assert.equal(second.credentialSeenDuringWrap, 'second-secret'); + assert.equal(process.env.LIBRECHAT_CODE_TEST_CREDENTIAL, undefined); }); -test('isolates Git from host-level global and system configuration', 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, - }); +test('isolates Git from host-level global and system configuration', 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, + }); - const result = await sandbox.execute({ - ...request, - command: 'printf "%s|%s" "$GIT_CONFIG_GLOBAL" "$GIT_CONFIG_NOSYSTEM"', - }); + const result = await sandbox.execute({ + ...request, + command: 'printf "%s|%s" "$GIT_CONFIG_GLOBAL" "$GIT_CONFIG_NOSYSTEM"', + }); - assert.equal(result.stdout, '/dev/null|1'); + assert.equal(result.stdout, '/dev/null|1'); }); -test('restores trusted Git LFS filters without reading host Git configuration', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager({ - appendGitSafeDirectory: true, - inheritedGitEnvironment: { - GIT_CONFIG_COUNT: '1', - GIT_CONFIG_KEY_0: 'include.path', - GIT_CONFIG_VALUE_0: '/untrusted/host-config', - }, - }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - environment: { - ...process.env, - GIT_CONFIG_COUNT: '1', - GIT_CONFIG_KEY_0: 'include.path', - GIT_CONFIG_VALUE_0: '/untrusted/host-config', - }, - manager: fake.manager, - }); - - const result = await sandbox.execute({ - ...request, - maxOutputBytes: 256, - command: - 'printf "%s|%s|%s|%s|%s|%s" "$(git config --get filter.lfs.clean)" "$(git config --get filter.lfs.smudge)" "$(git config --get filter.lfs.process)" "$(git config --get filter.lfs.required)" "$(git config --get safe.directory)" "$(git config --get include.path)"', - }); - - assert.equal( - result.stdout, - 'git-lfs clean -- %f|git-lfs smudge -- %f|git-lfs filter-process|true|/workspace|', - ); - assert.equal(fake.gitLfsRequiredSeenDuringWrap, 'true'); - const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); - assert.ok(!denied?.includes('GIT_CONFIG_COUNT')); - assert.ok(!denied?.includes('GIT_CONFIG_KEY_0')); - assert.ok(!denied?.includes('GIT_CONFIG_VALUE_0')); +test('restores trusted Git LFS filters without reading host Git configuration', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager({ + appendGitSafeDirectory: true, + inheritedGitEnvironment: { + GIT_CONFIG_COUNT: '1', + GIT_CONFIG_KEY_0: 'include.path', + GIT_CONFIG_VALUE_0: '/untrusted/host-config', + }, + }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + environment: { + ...process.env, + GIT_CONFIG_COUNT: '1', + GIT_CONFIG_KEY_0: 'include.path', + GIT_CONFIG_VALUE_0: '/untrusted/host-config', + }, + manager: fake.manager, + }); + + const result = await sandbox.execute({ + ...request, + maxOutputBytes: 256, + command: + 'printf "%s|%s|%s|%s|%s|%s" "$(git config --get filter.lfs.clean)" "$(git config --get filter.lfs.smudge)" "$(git config --get filter.lfs.process)" "$(git config --get filter.lfs.required)" "$(git config --get safe.directory)" "$(git config --get include.path)"', + }); + + assert.equal( + result.stdout, + 'git-lfs clean -- %f|git-lfs smudge -- %f|git-lfs filter-process|true|/workspace|', + ); + assert.equal(fake.gitLfsRequiredSeenDuringWrap, 'true'); + const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); + assert.ok(!denied?.includes('GIT_CONFIG_COUNT')); + assert.ok(!denied?.includes('GIT_CONFIG_KEY_0')); + assert.ok(!denied?.includes('GIT_CONFIG_VALUE_0')); }); -test('filters environment names case-insensitively only on Windows', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - platform: 'win32', - environment: { - PATH: '/usr/bin', - Path: 'C:\\Windows\\System32', - LC_API_TOKEN: 'secret', - librechat_code_worker_token: 'secret', - librechat_code_github_authorization: 'secret', - git_config_count: '1', - }, - maskedEnvironment: { - variables: [ - { - name: 'LIBRECHAT_CODE_GITHUB_AUTHORIZATION', - injectHosts: ['github.com'], +test('filters environment names case-insensitively only on Windows', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + platform: 'win32', + environment: { + PATH: '/usr/bin', + Path: 'C:\\Windows\\System32', + LC_API_TOKEN: 'secret', + librechat_code_worker_token: 'secret', + librechat_code_github_authorization: 'secret', + git_config_count: '1', }, - ], - async resolve() { - return {}; - }, - }, - manager: fake.manager, - }); - - await sandbox.prepare(); - const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); - assert.ok(!denied?.includes('PATH')); - assert.ok(!denied?.includes('Path')); - assert.ok(!denied?.includes('LC_API_TOKEN')); - assert.ok(denied?.includes('librechat_code_worker_token')); - assert.ok(!denied?.includes('librechat_code_github_authorization')); - assert.ok(!denied?.includes('git_config_count')); -}); + maskedEnvironment: { + variables: [ + { + name: 'LIBRECHAT_CODE_GITHUB_AUTHORIZATION', + injectHosts: ['github.com'], + }, + ], + async resolve() { + return {}; + }, + }, + manager: fake.manager, + }); -test('fails closed when the configured POSIX shell is unavailable', 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, - platform: 'linux', - shellPath: join(root, 'missing-bash'), - manager: fakeManager().manager, - }); - - await assert.rejects( - sandbox.prepare(), - (error: unknown) => - error instanceof WorkspaceToolError && - error.code === 'COMMAND_UNAVAILABLE' && - /shell is unavailable/i.test(error.message), - ); + await sandbox.prepare(); + const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); + assert.ok(!denied?.includes('PATH')); + assert.ok(!denied?.includes('Path')); + assert.ok(!denied?.includes('LC_API_TOKEN')); + assert.ok(denied?.includes('librechat_code_worker_token')); + assert.ok(!denied?.includes('librechat_code_github_authorization')); + assert.ok(!denied?.includes('git_config_count')); }); -test('fails closed when SRT dependencies are unavailable', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager({ dependencyErrors: ['bubblewrap missing'] }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - - await assert.rejects( - sandbox.prepare(), - (error: unknown) => - error instanceof WorkspaceToolError && - error.code === 'COMMAND_UNAVAILABLE' && - /bubblewrap missing/.test(error.message), - ); +test('fails closed when the configured POSIX shell is unavailable', 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, + platform: 'linux', + shellPath: join(root, 'missing-bash'), + manager: fakeManager().manager, + }); + + await assert.rejects( + sandbox.prepare(), + (error: unknown) => + error instanceof WorkspaceToolError && + error.code === 'COMMAND_UNAVAILABLE' && + /shell is unavailable/i.test(error.message), + ); }); -test('refuses workspace roots that expose worker home or control files', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - const controlDirectory = join(root, '.control'); - await mkdir(controlDirectory); - const controlFile = join(controlDirectory, 'identity.json'); - await writeFile(controlFile, '{}'); - t.after(() => rm(root, { recursive: true, force: true })); - - await assert.rejects( - new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: homedir(), - manager: fakeManager().manager, - }).prepare(), - /cannot contain the worker home directory/i, - ); - await assert.rejects( - new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - protectedPaths: [controlFile], - manager: fakeManager().manager, - }).prepare(), - /cannot contain worker control files/i, - ); +test('fails closed when SRT dependencies are unavailable', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager({ dependencyErrors: ['bubblewrap missing'] }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + + await assert.rejects( + sandbox.prepare(), + (error: unknown) => + error instanceof WorkspaceToolError && + error.code === 'COMMAND_UNAVAILABLE' && + /bubblewrap missing/.test(error.message), + ); }); -test('executes in the canonical workspace and bounds aggregate output', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - await mkdir(join(root, 'src')); - t.after(() => rm(root, { recursive: true, force: true })); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fakeManager().manager, - }); - - assert.equal(sandbox.mutationFailuresAreAtomic, true); - assert.deepEqual( - await sandbox.execute({ - ...request, - command: "printf '1234567890'; printf 'abcdefghij' >&2", - cwd: 'src', - maxOutputBytes: 12, - }), - { - protocolVersion: 1, - operation: 'execute_command', - workspaceId: 'primary', - exitCode: 0, - stdout: '1234567890', - stderr: 'ab', - truncated: true, - timedOut: false, - }, - ); +test('refuses workspace roots that expose worker home or control files', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + const controlDirectory = join(root, '.control'); + await mkdir(controlDirectory); + const controlFile = join(controlDirectory, 'identity.json'); + await writeFile(controlFile, '{}'); + t.after(() => rm(root, { recursive: true, force: true })); + + await assert.rejects( + new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: homedir(), + manager: fakeManager().manager, + }).prepare(), + /cannot contain the worker home directory/i, + ); + await assert.rejects( + new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + protectedPaths: [controlFile], + manager: fakeManager().manager, + }).prepare(), + /cannot contain worker control files/i, + ); }); -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 })); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fakeManager().manager, - }); - - await assert.rejects( - sandbox.execute({ ...request, cwd: '..' }), - (error: unknown) => - error instanceof WorkspaceToolError && error.code === 'INVALID_REQUEST', - ); +test('executes in the canonical workspace and bounds aggregate output', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + await mkdir(join(root, 'src')); + t.after(() => rm(root, { recursive: true, force: true })); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fakeManager().manager, + }); + + assert.equal(sandbox.mutationFailuresAreAtomic, true); + assert.deepEqual( + await sandbox.execute({ + ...request, + command: "printf '1234567890'; printf 'abcdefghij' >&2", + cwd: 'src', + maxOutputBytes: 12, + }), + { + protocolVersion: 1, + operation: 'execute_command', + workspaceId: 'primary', + exitCode: 0, + stdout: '1234567890', + stderr: 'ab', + truncated: true, + timedOut: false, + }, + ); }); -test('terminates detached command descendants before returning', 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, - }); - - const result = await sandbox.execute({ - ...request, - command: '(sleep 0.2; printf late > late.txt) >/dev/null 2>&1 &', - }); - assert.equal(result.exitCode, 0); - await new Promise((resolve) => setTimeout(resolve, 350)); - await assert.rejects(access(join(root, 'late.txt'))); +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 })); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fakeManager().manager, + }); + + await assert.rejects( + sandbox.execute({ ...request, cwd: '..' }), + (error: unknown) => + error instanceof WorkspaceToolError && + error.code === 'INVALID_REQUEST', + ); }); -test('reports cancellation after command start as a potentially committed mutation', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - let commandStarted!: () => void; - const commandStartedPromise = new Promise((resolve) => { - commandStarted = resolve; - }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fakeManager().manager, - spawnCommand(command, args, options) { - const child = spawn(command, [...args], options); - commandStarted(); - return child; - }, - }); - const controller = new AbortController(); - const execution = sandbox.execute( - { ...request, command: 'sleep 30' }, - controller.signal, - ); - await commandStartedPromise; - controller.abort(); - - await assert.rejects( - execution, - (error: unknown) => - error instanceof WorkspaceToolError && - error.code === 'EXECUTION_ABORTED' && - error.mutationMayHaveCommitted === true, - ); +test('terminates detached command descendants before returning', 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, + }); + + const result = await sandbox.execute({ + ...request, + command: '(sleep 0.2; printf late > late.txt) >/dev/null 2>&1 &', + }); + assert.equal(result.exitCode, 0); + await new Promise(resolve => setTimeout(resolve, 350)); + await assert.rejects(access(join(root, 'late.txt'))); }); -test('closes stdin immediately when the command protocol provides no input', 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, - }); - - const result = await sandbox.execute({ - ...request, - command: 'cat', - timeoutMs: 250, - }); - assert.equal(result.exitCode, 0); - assert.equal(result.timedOut, false); +test('reports cancellation after command start as a potentially committed mutation', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + let commandStarted!: () => void; + const commandStartedPromise = new Promise(resolve => { + commandStarted = resolve; + }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fakeManager().manager, + spawnCommand(command, args, options) { + const child = spawn(command, [...args], options); + commandStarted(); + return child; + }, + }); + const controller = new AbortController(); + const execution = sandbox.execute( + { ...request, command: 'sleep 30' }, + controller.signal, + ); + await commandStartedPromise; + controller.abort(); + + await assert.rejects( + execution, + (error: unknown) => + error instanceof WorkspaceToolError && + error.code === 'EXECUTION_ABORTED' && + error.mutationMayHaveCommitted === true, + ); }); -test('maps platform-native exit statuses into the bridge protocol range', async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const spawnCommand = () => { - const child = new EventEmitter() as ChildProcessWithoutNullStreams; - Object.assign(child, { - stdin: new PassThrough(), - stdout: new PassThrough(), - stderr: new PassThrough(), - pid: undefined, - kill: () => true, +test('closes stdin immediately when the command protocol provides no input', 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, + }); + + const result = await sandbox.execute({ + ...request, + command: 'cat', + timeoutMs: 250, }); - queueMicrotask(() => child.emit('close', 300, null)); - return child; - }; - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fakeManager().manager, - spawnCommand, - }); - - const result = await sandbox.execute(request); - assert.equal(result.exitCode, 1); + assert.equal(result.exitCode, 0); + assert.equal(result.timedOut, false); }); -test('cleans allocated command state exactly once on every execution exit', async (t) => { - for (const outcome of [ - 'abort-before-spawn', - 'spawn-throw', - 'close', - 'error', - 'abort-after-spawn', - 'timeout', - 'wrap-throw', - ] as const) { - for (const cleanupThrows of [false, true]) { - await t.test(`${outcome}, cleanup throws: ${cleanupThrows}`, async (t) => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const controller = new AbortController(); - let cleanupCalls = 0; - let spawnCalls = 0; - let allocated = false; - const fake = fakeManager({ - async beforeWrap() { - if (outcome === 'wrap-throw') throw new Error('wrap failed'); - allocated = true; - if (outcome === 'abort-before-spawn') controller.abort(); - }, - }); - fake.manager.cleanupAfterCommand = () => { - cleanupCalls += 1; - assert.equal(allocated, true); - allocated = false; - if (cleanupThrows) throw new Error('cleanup failed'); - }; - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - spawnCommand() { - spawnCalls += 1; - assert.equal(allocated, true); - if (outcome === 'spawn-throw') throw new Error('spawn failed'); - const child = new EventEmitter() as ChildProcessWithoutNullStreams; - let closeQueued = false; - const close = () => { - if (!closeQueued) { - closeQueued = true; - queueMicrotask(() => child.emit('close', null, 'SIGKILL')); - } - return true; - }; - Object.assign(child, { - stdin: new PassThrough(), - stdout: new PassThrough(), - stderr: new PassThrough(), - pid: undefined, - kill: close, - }); - queueMicrotask(() => { - assert.equal(cleanupCalls, 0); - if (outcome === 'error') { - child.emit('error', new Error('spawn failed')); - } else if (outcome === 'abort-after-spawn') { - controller.abort(); - } else if (outcome === 'close') { - child.emit('close', 0, null); - } - }); - return child; - }, +test('maps platform-native exit statuses into the bridge protocol range', async t => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const spawnCommand = () => { + const child = new EventEmitter() as ChildProcessWithoutNullStreams; + Object.assign(child, { + stdin: new PassThrough(), + stdout: new PassThrough(), + stderr: new PassThrough(), + pid: undefined, + kill: () => true, }); - const execution = sandbox.execute( - { ...request, timeoutMs: 10 }, - controller.signal, - ); - if (outcome === 'close' || outcome === 'timeout') { - const result = await execution; - assert.equal(result.exitCode, outcome === 'close' ? 0 : null); - assert.equal(result.timedOut, outcome === 'timeout'); - } else { - await assert.rejects(execution, (error: unknown) => - error instanceof WorkspaceToolError && - error.code === (outcome.startsWith('abort') - ? 'EXECUTION_ABORTED' - : 'COMMAND_UNAVAILABLE') && - error.mutationMayHaveCommitted === (outcome === 'abort-after-spawn'), - ); + queueMicrotask(() => child.emit('close', 300, null)); + return child; + }; + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fakeManager().manager, + spawnCommand, + }); + + const result = await sandbox.execute(request); + assert.equal(result.exitCode, 1); +}); + +test('cleans allocated command state exactly once on every execution exit', async t => { + for (const outcome of [ + 'abort-before-spawn', + 'spawn-throw', + 'close', + 'error', + 'abort-after-spawn', + 'timeout', + 'wrap-throw', + ] as const) { + for (const cleanupThrows of [false, true]) { + await t.test( + `${outcome}, cleanup throws: ${cleanupThrows}`, + async t => { + const root = await mkdtemp( + join(tmpdir(), 'librechat-code-native-'), + ); + t.after(() => rm(root, { recursive: true, force: true })); + const controller = new AbortController(); + let cleanupCalls = 0; + let spawnCalls = 0; + let allocated = false; + const fake = fakeManager({ + async beforeWrap() { + if (outcome === 'wrap-throw') + throw new Error('wrap failed'); + allocated = true; + if (outcome === 'abort-before-spawn') + controller.abort(); + }, + }); + fake.manager.cleanupAfterCommand = () => { + cleanupCalls += 1; + assert.equal(allocated, true); + allocated = false; + if (cleanupThrows) throw new Error('cleanup failed'); + }; + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + spawnCommand() { + spawnCalls += 1; + assert.equal(allocated, true); + if (outcome === 'spawn-throw') + throw new Error('spawn failed'); + const child = + new EventEmitter() as ChildProcessWithoutNullStreams; + let closeQueued = false; + const close = () => { + if (!closeQueued) { + closeQueued = true; + queueMicrotask(() => + child.emit('close', null, 'SIGKILL'), + ); + } + return true; + }; + Object.assign(child, { + stdin: new PassThrough(), + stdout: new PassThrough(), + stderr: new PassThrough(), + pid: undefined, + kill: close, + }); + queueMicrotask(() => { + assert.equal(cleanupCalls, 0); + if (outcome === 'error') { + child.emit( + 'error', + new Error('spawn failed'), + ); + } else if (outcome === 'abort-after-spawn') { + controller.abort(); + } else if (outcome === 'close') { + child.emit('close', 0, null); + } + }); + return child; + }, + }); + const execution = sandbox.execute( + { ...request, timeoutMs: 10 }, + controller.signal, + ); + if (outcome === 'close' || outcome === 'timeout') { + const result = await execution; + assert.equal( + result.exitCode, + outcome === 'close' ? 0 : null, + ); + assert.equal(result.timedOut, outcome === 'timeout'); + } else { + await assert.rejects( + execution, + (error: unknown) => + error instanceof WorkspaceToolError && + error.code === + (outcome.startsWith('abort') + ? 'EXECUTION_ABORTED' + : 'COMMAND_UNAVAILABLE') && + error.mutationMayHaveCommitted === + (outcome === 'abort-after-spawn'), + ); + } + assert.equal( + spawnCalls, + outcome === 'abort-before-spawn' || + outcome === 'wrap-throw' + ? 0 + : 1, + ); + assert.equal( + cleanupCalls, + outcome === 'wrap-throw' ? 0 : 1, + ); + assert.equal(allocated, false); + await sandbox.close(); + assert.equal(fake.reset, true); + }, + ); } - assert.equal( - spawnCalls, - outcome === 'abort-before-spawn' || outcome === 'wrap-throw' ? 0 : 1, - ); - assert.equal(cleanupCalls, outcome === 'wrap-throw' ? 0 : 1); - assert.equal(allocated, false); - await sandbox.close(); - assert.equal(fake.reset, true); - }); } - } }); diff --git a/packages/code/src/native-sandbox.ts b/packages/code/src/native-sandbox.ts index 1adfd35..41c4a3f 100644 --- a/packages/code/src/native-sandbox.ts +++ b/packages/code/src/native-sandbox.ts @@ -2,112 +2,105 @@ import { spawn } from 'node:child_process'; import { randomUUID } from 'node:crypto'; import { homedir, tmpdir } from 'node:os'; import { - basename, - dirname, - isAbsolute, - join, - relative, - resolve, - sep, + basename, + dirname, + isAbsolute, + join, + relative, + resolve, + sep, } from 'node:path'; import { constants as fsConstants } from 'node:fs'; -import { - access, - mkdtemp, - open, - realpath, - rm, - stat, -} from 'node:fs/promises'; +import { access, mkdtemp, open, realpath, rm, stat } from 'node:fs/promises'; import type { FileHandle } from 'node:fs/promises'; import { SandboxManager } from '@anthropic-ai/sandbox-runtime'; import { - BRIDGE_PROTOCOL_VERSION, - BRIDGE_WORKSPACE_COMMAND_DEFAULT_OUTPUT_BYTES, - BRIDGE_WORKSPACE_COMMAND_DEFAULT_TIMEOUT_MS, - isWorkspaceToolRequest, + BRIDGE_PROTOCOL_VERSION, + BRIDGE_WORKSPACE_COMMAND_DEFAULT_OUTPUT_BYTES, + BRIDGE_WORKSPACE_COMMAND_DEFAULT_TIMEOUT_MS, + isWorkspaceToolRequest, } from './protocol.js'; import { - assertPrivateStorageAcl, - assertPrivateStorageAncestors, - removePrivateStorageAcl, + assertPrivateStorageAcl, + assertPrivateStorageAncestors, + removePrivateStorageAcl, } from './private-storage.js'; import { WorkspaceToolError } from './workspace.js'; import { restoreScratchTraversal } from './native-scratch.js'; import type { - ChildProcessWithoutNullStreams, - SpawnOptionsWithoutStdio, + ChildProcessWithoutNullStreams, + SpawnOptionsWithoutStdio, } from 'node:child_process'; import type { SandboxRuntimeConfig } from '@anthropic-ai/sandbox-runtime'; import type { - WorkspaceExecuteCommandRequest, - WorkspaceExecuteCommandResult, + WorkspaceExecuteCommandRequest, + WorkspaceExecuteCommandResult, } from './protocol.js'; import type { WorkspaceCommandSandbox } from './workspace.js'; const SAFE_CHILD_ENV_NAMES = new Set([ - 'COLORTERM', - 'HOME', - 'LANG', - 'LC_ALL', - 'LOGNAME', - 'NO_COLOR', - 'PATH', - 'SHELL', - 'TERM', - 'TMPDIR', - 'USER', + 'COLORTERM', + 'HOME', + 'LANG', + 'LC_ALL', + 'LOGNAME', + 'NO_COLOR', + 'PATH', + 'SHELL', + 'TERM', + 'TMPDIR', + 'USER', ]); // Preserve the conventional proxy names, not arbitrary *_PROXY variables. // SRT owns their final values and may replace them with its filtered proxy. const PROXY_CHILD_ENV_NAMES = new Set([ - 'HTTP_PROXY', - 'HTTPS_PROXY', - 'ALL_PROXY', - 'NO_PROXY', - 'http_proxy', - 'https_proxy', - 'all_proxy', - 'no_proxy', + 'HTTP_PROXY', + 'HTTPS_PROXY', + 'ALL_PROXY', + 'NO_PROXY', + 'http_proxy', + 'https_proxy', + 'all_proxy', + 'no_proxy', ]); // Windows resolves these names case-insensitively. Keep the exception // platform-specific so similarly named POSIX variables remain denied. const WINDOWS_CHILD_ENV_NAMES = new Set([ - 'SYSTEMROOT', - 'SYSTEMDRIVE', - 'WINDIR', - 'COMSPEC', - 'PATHEXT', - 'TEMP', - 'TMP', - 'USERPROFILE', - 'HOMEDRIVE', - 'HOMEPATH', - 'APPDATA', - 'LOCALAPPDATA', + 'SYSTEMROOT', + 'SYSTEMDRIVE', + 'WINDIR', + 'COMSPEC', + 'PATHEXT', + 'TEMP', + 'TMP', + 'USERPROFILE', + 'HOMEDRIVE', + 'HOMEPATH', + 'APPDATA', + 'LOCALAPPDATA', ]); let hostEnvironmentMutationQueue: Promise = Promise.resolve(); const TRUSTED_GIT_ENVIRONMENT = { - GIT_CONFIG_COUNT: '4', - GIT_CONFIG_KEY_0: 'filter.lfs.clean', - GIT_CONFIG_VALUE_0: 'git-lfs clean -- %f', - GIT_CONFIG_KEY_1: 'filter.lfs.smudge', - GIT_CONFIG_VALUE_1: 'git-lfs smudge -- %f', - GIT_CONFIG_KEY_2: 'filter.lfs.process', - GIT_CONFIG_VALUE_2: 'git-lfs filter-process', - GIT_CONFIG_KEY_3: 'filter.lfs.required', - GIT_CONFIG_VALUE_3: 'true', + GIT_CONFIG_COUNT: '4', + GIT_CONFIG_KEY_0: 'filter.lfs.clean', + GIT_CONFIG_VALUE_0: 'git-lfs clean -- %f', + GIT_CONFIG_KEY_1: 'filter.lfs.smudge', + GIT_CONFIG_VALUE_1: 'git-lfs smudge -- %f', + GIT_CONFIG_KEY_2: 'filter.lfs.process', + GIT_CONFIG_VALUE_2: 'git-lfs filter-process', + GIT_CONFIG_KEY_3: 'filter.lfs.required', + GIT_CONFIG_VALUE_3: 'true', } as const; const { - GIT_CONFIG_COUNT: TRUSTED_GIT_CONFIG_COUNT, - ...TRUSTED_GIT_CONFIG_ENTRIES + GIT_CONFIG_COUNT: TRUSTED_GIT_CONFIG_COUNT, + ...TRUSTED_GIT_CONFIG_ENTRIES } = TRUSTED_GIT_ENVIRONMENT; const NATIVE_SANDBOX_SCRATCH_PREFIX = 'librechat-code-srt-'; @@ -115,745 +108,803 @@ const NATIVE_SANDBOX_SCRATCH_PREFIX = 'librechat-code-srt-'; // TMPDIR must also deny them or separate worker processes can exchange files. const SRT_SHARED_SCRATCH_PATHS = ['/tmp/claude', '/private/tmp/claude']; const SRT_SCRATCH_SELECTOR_NAMES = [ - 'CLAUDE_CODE_TMPDIR', - 'CLAUDE_TMPDIR', + 'CLAUDE_CODE_TMPDIR', + 'CLAUDE_TMPDIR', ] as const; // Capture this before any command wrapper can temporarily mutate process.env. const HOST_TEMPORARY_ROOT = tmpdir(); interface NativeSandboxManager { - isSupportedPlatform(): boolean; - checkDependenciesAsync(): Promise<{ warnings: string[]; errors: string[] }>; - initialize(config: SandboxRuntimeConfig): Promise; - wrapWithSandboxArgv( - command: string, - binShell?: string, - customConfig?: Partial, - abortSignal?: AbortSignal, - cwd?: string, - options?: { commandId?: string; commandText?: string }, - ): Promise<{ argv: string[]; env: NodeJS.ProcessEnv }>; - annotateStderrWithSandboxFailures(commandId: string, stderr: string): string; - cleanupAfterCommand(): void; - reset(): Promise; + isSupportedPlatform(): boolean; + checkDependenciesAsync(): Promise<{ warnings: string[]; errors: string[] }>; + initialize(config: SandboxRuntimeConfig): Promise; + wrapWithSandboxArgv( + command: string, + binShell?: string, + customConfig?: Partial, + abortSignal?: AbortSignal, + cwd?: string, + options?: { commandId?: string; commandText?: string }, + ): Promise<{ argv: string[]; env: NodeJS.ProcessEnv }>; + annotateStderrWithSandboxFailures( + commandId: string, + stderr: string, + ): string; + cleanupAfterCommand(): void; + reset(): Promise; } // SRT's default manager is process-global, including its policy and cleanup // state. Distinct workspace objects must not reconfigure the same manager. const managerOwners = new WeakMap< - NativeSandboxManager, - NativeSrtWorkspaceCommandSandbox + NativeSandboxManager, + NativeSrtWorkspaceCommandSandbox >(); type SpawnCommand = ( - command: string, - args: readonly string[], - options: SpawnOptionsWithoutStdio, + command: string, + args: readonly string[], + options: SpawnOptionsWithoutStdio, ) => ChildProcessWithoutNullStreams; export interface NativeSrtWorkspaceCommandSandboxOptions { - workspaceRoot: string; - /** Trusted worker files that must never become workspace-readable or writable. */ - protectedPaths?: string[]; - allowedDomains?: string[]; - environment?: NodeJS.ProcessEnv; - manager?: NativeSandboxManager; - spawnCommand?: SpawnCommand; - homeDirectory?: string; - platform?: NodeJS.Platform; - /** Trusted shell path used by SRT on POSIX hosts. */ - shellPath?: string; - /** Host-owned credentials exposed only as SRT sentinels inside the sandbox. */ - maskedEnvironment?: { - variables: Array<{ - name: string; - injectHosts: string[]; - extract?: string; - }>; - resolve(signal?: AbortSignal): Promise>; - wrapCommand?(command: string, platform: NodeJS.Platform): string; - }; + workspaceRoot: string; + /** Trusted worker files that must never become workspace-readable or writable. */ + protectedPaths?: string[]; + allowedDomains?: string[]; + environment?: NodeJS.ProcessEnv; + manager?: NativeSandboxManager; + spawnCommand?: SpawnCommand; + homeDirectory?: string; + platform?: NodeJS.Platform; + /** Trusted shell path used by SRT on POSIX hosts. */ + shellPath?: string; + /** Host-owned credentials exposed only as SRT sentinels inside the sandbox. */ + maskedEnvironment?: { + variables: Array<{ + name: string; + injectHosts: string[]; + extract?: string; + }>; + resolve(signal?: AbortSignal): Promise>; + wrapCommand?(command: string, platform: NodeJS.Platform): string; + }; } function isWithin(root: string, candidate: string): boolean { - const path = relative(root, candidate); - return ( - path === '' || - (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path)) - ); + const path = relative(root, candidate); + return ( + path === '' || + (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path)) + ); } async function canonicalPath(path: string): Promise { - const absolute = resolve(path); - let cursor = absolute; - const missingSegments: string[] = []; - for (;;) { - try { - return join(await realpath(cursor), ...missingSegments); - } catch { - const parent = dirname(cursor); - if (parent === cursor) - throw new Error(`Cannot canonicalize protected path: ${path}`); - missingSegments.unshift(basename(cursor)); - cursor = parent; + const absolute = resolve(path); + let cursor = absolute; + const missingSegments: string[] = []; + for (;;) { + try { + return join(await realpath(cursor), ...missingSegments); + } catch { + const parent = dirname(cursor); + if (parent === cursor) + throw new Error(`Cannot canonicalize protected path: ${path}`); + missingSegments.unshift(basename(cursor)); + cursor = parent; + } } - } } function boundedUtf8(buffer: Buffer, budget: number): string { - let end = Math.min(buffer.byteLength, budget); - while (end > 0) { - const value = buffer.subarray(0, end).toString('utf8'); - if (Buffer.byteLength(value) <= budget) return value; - end -= 1; - } - return ''; + let end = Math.min(buffer.byteLength, budget); + while (end > 0) { + const value = buffer.subarray(0, end).toString('utf8'); + if (Buffer.byteLength(value) <= budget) return value; + end -= 1; + } + return ''; } function deniedEnvironmentNames( - environment: NodeJS.ProcessEnv, - platform: NodeJS.Platform, + environment: NodeJS.ProcessEnv, + platform: NodeJS.Platform, ): string[] { - return Object.keys(environment) - .filter((name) => { - const normalized = platform === 'win32' ? name.toUpperCase() : name; - return ( - normalized.startsWith('LIBRECHAT_CODE_') || - (!SAFE_CHILD_ENV_NAMES.has(normalized) && - !PROXY_CHILD_ENV_NAMES.has(normalized) && - !(platform === 'win32' && WINDOWS_CHILD_ENV_NAMES.has(normalized)) && - !normalized.startsWith('LC_')) - ); - }) - .sort(); + return Object.keys(environment) + .filter(name => { + const normalized = platform === 'win32' ? name.toUpperCase() : name; + return ( + normalized.startsWith('LIBRECHAT_CODE_') || + (!SAFE_CHILD_ENV_NAMES.has(normalized) && + !PROXY_CHILD_ENV_NAMES.has(normalized) && + !( + platform === 'win32' && + WINDOWS_CHILD_ENV_NAMES.has(normalized) + ) && + !normalized.startsWith('LC_')) + ); + }) + .sort(); } function normalizedEnvironmentName( - name: string, - platform: NodeJS.Platform, + name: string, + platform: NodeJS.Platform, ): string { - return platform === 'win32' ? name.toUpperCase() : name; + return platform === 'win32' ? name.toUpperCase() : name; } export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox { - readonly mutationFailuresAreAtomic = true as const; - private readonly manager: NativeSandboxManager; - private readonly spawnCommand: SpawnCommand; - private readonly environment: NodeJS.ProcessEnv; - private readonly platform: NodeJS.Platform; - private initialized?: Promise; - private canonicalRoot?: string; - private scratchDirectory?: string; - private scratchHandle?: FileHandle; - private execution?: Promise; - private closing?: Promise; - private resetFailed = false; - - constructor( - private readonly options: NativeSrtWorkspaceCommandSandboxOptions, - ) { - this.manager = options.manager ?? SandboxManager; - this.spawnCommand = options.spawnCommand ?? spawn; - this.environment = { ...(options.environment ?? process.env) }; - this.platform = options.platform ?? process.platform; - } - - /** Fail closed before the worker advertises command execution. */ - async prepare(): Promise { - await this.initialize(); - } - - private async initialize(): Promise { - if (this.closing || this.resetFailed) { - throw new WorkspaceToolError( - 'Native sandbox is closing or requires cleanup', - 'COMMAND_UNAVAILABLE', - ); - } - if (this.initialized) return this.initialized; - const owner = managerOwners.get(this.manager); - if (owner && owner !== this) { - throw new WorkspaceToolError( - 'Native sandbox manager already belongs to another workspace; use a separate worker process', - 'COMMAND_UNAVAILABLE', - ); - } - managerOwners.set(this.manager, this); - this.initialized = this.initializeOnce().catch(async (error) => { - await this.manager.reset().catch(() => { - this.resetFailed = true; - }); - await this.removeScratchDirectory().catch(() => undefined); - if (!this.resetFailed) managerOwners.delete(this.manager); - this.initialized = undefined; - throw error; - }); - return this.initialized; - } - - private async initializeOnce(): Promise { - if (!this.manager.isSupportedPlatform()) { - throw new WorkspaceToolError( - 'Native sandbox is unsupported on this platform', - 'COMMAND_UNAVAILABLE', - ); - } - const root = await realpath(this.options.workspaceRoot); - if (!(await stat(root)).isDirectory()) { - throw new WorkspaceToolError( - 'Native sandbox workspace is unavailable', - 'COMMAND_UNAVAILABLE', - ); - } - const home = await canonicalPath(this.options.homeDirectory ?? homedir()); - if (isWithin(root, home)) { - throw new WorkspaceToolError( - 'Native sandbox workspace cannot contain the worker home directory', - 'REGISTRATION_INVALID', - ); - } - const protectedPaths = await Promise.all( - (this.options.protectedPaths ?? []).map(canonicalPath), - ); - if (protectedPaths.some((path) => isWithin(root, path))) { - throw new WorkspaceToolError( - 'Native sandbox workspace cannot contain worker control files', - 'REGISTRATION_INVALID', - ); + readonly mutationFailuresAreAtomic = true as const; + private readonly manager: NativeSandboxManager; + private readonly spawnCommand: SpawnCommand; + private readonly environment: NodeJS.ProcessEnv; + private readonly platform: NodeJS.Platform; + private initialized?: Promise; + private canonicalRoot?: string; + private scratchDirectory?: string; + private scratchHandle?: FileHandle; + private execution?: Promise; + private closing?: Promise; + private resetFailed = false; + + constructor( + private readonly options: NativeSrtWorkspaceCommandSandboxOptions, + ) { + this.manager = options.manager ?? SandboxManager; + this.spawnCommand = options.spawnCommand ?? spawn; + this.environment = { ...(options.environment ?? process.env) }; + this.platform = options.platform ?? process.platform; } - const sharedScratchPaths = await Promise.all( - (this.platform === 'win32' ? [] : SRT_SHARED_SCRATCH_PATHS).map( - canonicalPath, - ), - ); - const inheritedWritablePaths = [ - ...sharedScratchPaths, - ...(await Promise.all( - [join(home, '.npm', '_logs'), join(home, '.claude', 'debug')].map( - canonicalPath, - ), - )), - ]; - const deniedInheritedWritablePaths = [...new Set(inheritedWritablePaths)]; - if (deniedInheritedWritablePaths.some((path) => isWithin(path, root))) { - throw new WorkspaceToolError( - 'Native sandbox workspace cannot be inside an inherited writable path', - 'REGISTRATION_INVALID', - ); + + /** Fail closed before the worker advertises command execution. */ + async prepare(): Promise { + await this.initialize(); } - const dependencies = await this.manager.checkDependenciesAsync(); - if (dependencies.errors.length > 0) { - throw new WorkspaceToolError( - `Native sandbox dependencies are unavailable: ${dependencies.errors.join('; ')}`, - 'COMMAND_UNAVAILABLE', - ); + + private async initialize(): Promise { + if (this.closing || this.resetFailed) { + throw new WorkspaceToolError( + 'Native sandbox is closing or requires cleanup', + 'COMMAND_UNAVAILABLE', + ); + } + if (this.initialized) return this.initialized; + const owner = managerOwners.get(this.manager); + if (owner && owner !== this) { + throw new WorkspaceToolError( + 'Native sandbox manager already belongs to another workspace; use a separate worker process', + 'COMMAND_UNAVAILABLE', + ); + } + managerOwners.set(this.manager, this); + this.initialized = this.initializeOnce().catch(async error => { + await this.manager.reset().catch(() => { + this.resetFailed = true; + }); + await this.removeScratchDirectory().catch(() => undefined); + if (!this.resetFailed) managerOwners.delete(this.manager); + this.initialized = undefined; + throw error; + }); + return this.initialized; } - if (this.platform !== 'win32') { - try { - await access(this.options.shellPath ?? '/bin/bash', fsConstants.X_OK); - } catch { - throw new WorkspaceToolError( - `Native sandbox shell is unavailable: ${this.options.shellPath ?? '/bin/bash'}`, - 'COMMAND_UNAVAILABLE', + + private async initializeOnce(): Promise { + if (!this.manager.isSupportedPlatform()) { + throw new WorkspaceToolError( + 'Native sandbox is unsupported on this platform', + 'COMMAND_UNAVAILABLE', + ); + } + const root = await realpath(this.options.workspaceRoot); + if (!(await stat(root)).isDirectory()) { + throw new WorkspaceToolError( + 'Native sandbox workspace is unavailable', + 'COMMAND_UNAVAILABLE', + ); + } + const home = await canonicalPath( + this.options.homeDirectory ?? homedir(), ); - } - } - const canonicalScratchDirectory = - await this.createScratchDirectory(sharedScratchPaths); - if ( - canonicalScratchDirectory && - isWithin(root, canonicalScratchDirectory) - ) { - throw new WorkspaceToolError( - 'Native sandbox workspace cannot contain worker scratch storage', - 'REGISTRATION_INVALID', - ); - } - const config: SandboxRuntimeConfig = { - network: { - allowedDomains: [...(this.options.allowedDomains ?? [])], - deniedDomains: [], - strictAllowlist: true, - allowAllUnixSockets: false, - allowLocalBinding: false, - ...(this.options.maskedEnvironment ? { tlsTerminate: {} } : {}), - }, - filesystem: { - denyRead: [ - home, - ...sharedScratchPaths.filter((path) => - deniedInheritedWritablePaths.includes(path), - ), - ], - allowRead: [ - root, - ...(canonicalScratchDirectory ? [canonicalScratchDirectory] : []), - ], - allowWrite: [ - root, - ...(canonicalScratchDirectory ? [canonicalScratchDirectory] : []), - ], - denyWrite: [...protectedPaths, ...deniedInheritedWritablePaths], - allowGitConfig: false, - }, - credentials: { - files: protectedPaths.map((path) => ({ - path, - mode: 'deny' as const, - })), - envVars: [ - ...deniedEnvironmentNames( - { - ...this.environment, - CLAUDE_CODE_TMPDIR: '', - CLAUDE_TMPDIR: '', + if (isWithin(root, home)) { + throw new WorkspaceToolError( + 'Native sandbox workspace cannot contain the worker home directory', + 'REGISTRATION_INVALID', + ); + } + const protectedPaths = await Promise.all( + (this.options.protectedPaths ?? []).map(canonicalPath), + ); + if (protectedPaths.some(path => isWithin(root, path))) { + throw new WorkspaceToolError( + 'Native sandbox workspace cannot contain worker control files', + 'REGISTRATION_INVALID', + ); + } + const sharedScratchPaths = await Promise.all( + (this.platform === 'win32' ? [] : SRT_SHARED_SCRATCH_PATHS).map( + canonicalPath, + ), + ); + const inheritedWritablePaths = [ + ...sharedScratchPaths, + ...(await Promise.all( + [ + join(home, '.npm', '_logs'), + join(home, '.claude', 'debug'), + ].map(canonicalPath), + )), + ]; + const deniedInheritedWritablePaths = [ + ...new Set(inheritedWritablePaths), + ]; + if (deniedInheritedWritablePaths.some(path => isWithin(path, root))) { + throw new WorkspaceToolError( + 'Native sandbox workspace cannot be inside an inherited writable path', + 'REGISTRATION_INVALID', + ); + } + const dependencies = await this.manager.checkDependenciesAsync(); + if (dependencies.errors.length > 0) { + throw new WorkspaceToolError( + `Native sandbox dependencies are unavailable: ${dependencies.errors.join('; ')}`, + 'COMMAND_UNAVAILABLE', + ); + } + if (this.platform !== 'win32') { + try { + await access( + this.options.shellPath ?? '/bin/bash', + fsConstants.X_OK, + ); + } catch { + throw new WorkspaceToolError( + `Native sandbox shell is unavailable: ${this.options.shellPath ?? '/bin/bash'}`, + 'COMMAND_UNAVAILABLE', + ); + } + } + const canonicalScratchDirectory = + await this.createScratchDirectory(sharedScratchPaths); + if ( + canonicalScratchDirectory && + isWithin(root, canonicalScratchDirectory) + ) { + throw new WorkspaceToolError( + 'Native sandbox workspace cannot contain worker scratch storage', + 'REGISTRATION_INVALID', + ); + } + const config: SandboxRuntimeConfig = { + network: { + allowedDomains: [...(this.options.allowedDomains ?? [])], + deniedDomains: [], + strictAllowlist: true, + allowAllUnixSockets: false, + allowLocalBinding: false, + ...(this.options.maskedEnvironment ? { tlsTerminate: {} } : {}), }, - this.platform, - ) - .filter((name) => { - const normalized = normalizedEnvironmentName( - name, - this.platform, - ); - return ( - !Object.hasOwn(TRUSTED_GIT_ENVIRONMENT, normalized) && - !this.options.maskedEnvironment?.variables.some( - (variable) => - normalizedEnvironmentName( - variable.name, - this.platform, - ) === normalized, - ) - ); - }) - .map((name) => ({ name, mode: 'deny' as const })), - ...(this.options.maskedEnvironment?.variables.map((variable) => ({ - ...variable, - mode: 'mask' as const, - ...(variable.extract ? { onExtractNoMatch: 'error' as const } : {}), - })) ?? []), - ], - }, - allowAppleEvents: false, - enableWeakerNestedSandbox: false, - enableWeakerNetworkIsolation: false, - git: { safeDirectories: [root] }, - }; - await this.manager.initialize(config); - this.canonicalRoot = root; - } - - async execute( - request: WorkspaceExecuteCommandRequest, - signal?: AbortSignal, - ): Promise { - if (this.execution || this.closing) { - throw new WorkspaceToolError( - 'Native sandbox already has an active command or is closing', - 'COMMAND_UNAVAILABLE', - ); - } - const execution = this.executeExclusive(request, signal); - this.execution = execution; - try { - return await execution; - } finally { - this.execution = undefined; - } - } - - private async executeExclusive( - request: WorkspaceExecuteCommandRequest, - signal?: AbortSignal, - ): Promise { - if ( - !isWorkspaceToolRequest(request) || - request.operation !== 'execute_command' - ) { - throw new WorkspaceToolError( - 'Invalid native sandbox command', - 'INVALID_REQUEST', - ); + filesystem: { + denyRead: [ + home, + ...sharedScratchPaths.filter(path => + deniedInheritedWritablePaths.includes(path), + ), + ], + allowRead: [ + root, + ...(canonicalScratchDirectory + ? [canonicalScratchDirectory] + : []), + ], + allowWrite: [ + root, + ...(canonicalScratchDirectory + ? [canonicalScratchDirectory] + : []), + ], + denyWrite: [...protectedPaths, ...deniedInheritedWritablePaths], + allowGitConfig: false, + }, + credentials: { + files: protectedPaths.map(path => ({ + path, + mode: 'deny' as const, + })), + envVars: [ + ...deniedEnvironmentNames( + { + ...this.environment, + CLAUDE_CODE_TMPDIR: '', + CLAUDE_TMPDIR: '', + }, + this.platform, + ) + .filter(name => { + const normalized = normalizedEnvironmentName( + name, + this.platform, + ); + return ( + !Object.hasOwn( + TRUSTED_GIT_ENVIRONMENT, + normalized, + ) && + !this.options.maskedEnvironment?.variables.some( + variable => + normalizedEnvironmentName( + variable.name, + this.platform, + ) === normalized, + ) + ); + }) + .map(name => ({ name, mode: 'deny' as const })), + ...(this.options.maskedEnvironment?.variables.map( + variable => ({ + ...variable, + mode: 'mask' as const, + ...(variable.extract + ? { onExtractNoMatch: 'error' as const } + : {}), + }), + ) ?? []), + ], + }, + allowAppleEvents: false, + enableWeakerNestedSandbox: false, + enableWeakerNetworkIsolation: false, + git: { safeDirectories: [root] }, + }; + await this.manager.initialize(config); + this.canonicalRoot = root; } - if (signal?.aborted) { - throw new WorkspaceToolError( - 'Workspace command execution aborted', - 'EXECUTION_ABORTED', - ); + + async execute( + request: WorkspaceExecuteCommandRequest, + signal?: AbortSignal, + ): Promise { + if (this.execution || this.closing) { + throw new WorkspaceToolError( + 'Native sandbox already has an active command or is closing', + 'COMMAND_UNAVAILABLE', + ); + } + const execution = this.executeExclusive(request, signal); + this.execution = execution; + try { + return await execution; + } finally { + this.execution = undefined; + } } - await this.initialize(); - const root = this.canonicalRoot!; - let cwd: string; - try { - cwd = await realpath(resolve(root, request.cwd ?? '.')); - if (!isWithin(root, cwd) || !(await stat(cwd)).isDirectory()) - throw new Error('invalid cwd'); - } catch { - throw new WorkspaceToolError( - 'Command working directory is unavailable', - 'INVALID_PATH', - ); + + private async executeExclusive( + request: WorkspaceExecuteCommandRequest, + signal?: AbortSignal, + ): Promise { + if ( + !isWorkspaceToolRequest(request) || + request.operation !== 'execute_command' + ) { + throw new WorkspaceToolError( + 'Invalid native sandbox command', + 'INVALID_REQUEST', + ); + } + if (signal?.aborted) { + throw new WorkspaceToolError( + 'Workspace command execution aborted', + 'EXECUTION_ABORTED', + ); + } + await this.initialize(); + const root = this.canonicalRoot!; + let cwd: string; + try { + cwd = await realpath(resolve(root, request.cwd ?? '.')); + if (!isWithin(root, cwd) || !(await stat(cwd)).isDirectory()) + throw new Error('invalid cwd'); + } catch { + throw new WorkspaceToolError( + 'Command working directory is unavailable', + 'INVALID_PATH', + ); + } + const commandId = `librechat-code-${randomUUID()}`; + const sandboxedCommand = this.options.maskedEnvironment?.wrapCommand + ? this.options.maskedEnvironment.wrapCommand( + request.command, + this.platform, + ) + : request.command; + let wrapped: Awaited< + ReturnType + >; + try { + const credentialEnvironment = + await this.options.maskedEnvironment?.resolve(signal); + wrapped = await this.withTemporaryHostEnvironment( + { + ...TRUSTED_GIT_ENVIRONMENT, + ...(credentialEnvironment ?? {}), + ...this.scratchSelectorEnvironment(), + }, + () => + this.manager.wrapWithSandboxArgv( + sandboxedCommand, + this.platform === 'win32' + ? undefined + : (this.options.shellPath ?? '/bin/bash'), + undefined, + signal, + cwd, + { commandId, commandText: request.command }, + ), + ); + } catch (error) { + if (signal?.aborted) { + throw new WorkspaceToolError( + 'Workspace command execution aborted', + 'EXECUTION_ABORTED', + ); + } + throw new WorkspaceToolError( + 'Native sandbox command could not start', + 'COMMAND_UNAVAILABLE', + ); + } + try { + if (signal?.aborted) { + throw new WorkspaceToolError( + 'Workspace command execution aborted', + 'EXECUTION_ABORTED', + ); + } + return await this.runWrapped( + request, + wrapped, + cwd, + commandId, + signal, + ); + } finally { + // A successful wrap owns command state even when no child is spawned. + try { + this.manager.cleanupAfterCommand(); + } catch { + // Cleanup is retried by close(); command settlement must still finish. + } + } } - const commandId = `librechat-code-${randomUUID()}`; - const sandboxedCommand = this.options.maskedEnvironment?.wrapCommand - ? this.options.maskedEnvironment.wrapCommand( - request.command, - this.platform, - ) - : request.command; - let wrapped: Awaited< - ReturnType - >; - try { - const credentialEnvironment = - await this.options.maskedEnvironment?.resolve(signal); - wrapped = await this.withTemporaryHostEnvironment( - { - ...TRUSTED_GIT_ENVIRONMENT, - ...(credentialEnvironment ?? {}), - ...this.scratchSelectorEnvironment(), - }, - () => - this.manager.wrapWithSandboxArgv( - sandboxedCommand, - this.platform === 'win32' - ? undefined - : (this.options.shellPath ?? '/bin/bash'), - undefined, - signal, - cwd, - { commandId, commandText: request.command }, - ), - ); - } catch (error) { - if (signal?.aborted) { - throw new WorkspaceToolError( - 'Workspace command execution aborted', - 'EXECUTION_ABORTED', - ); - } - throw new WorkspaceToolError( - 'Native sandbox command could not start', - 'COMMAND_UNAVAILABLE', - ); + + private async withTemporaryHostEnvironment( + values: Record, + action: () => Promise, + ): Promise { + const previousMutation = hostEnvironmentMutationQueue; + let releaseMutation!: () => void; + hostEnvironmentMutationQueue = new Promise(resolve => { + releaseMutation = resolve; + }); + await previousMutation; + const previous = new Map(); + try { + for (const [name, value] of Object.entries(values)) { + previous.set(name, process.env[name]); + process.env[name] = value; + } + return await action(); + } finally { + for (const [name, value] of previous) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + releaseMutation(); + } } - try { - if (signal?.aborted) { - throw new WorkspaceToolError( - 'Workspace command execution aborted', - 'EXECUTION_ABORTED', + + private async runWrapped( + request: WorkspaceExecuteCommandRequest, + wrapped: { argv: string[]; env: NodeJS.ProcessEnv }, + cwd: string, + commandId: string, + signal?: AbortSignal, + ): Promise { + const outputLimit = + request.maxOutputBytes ?? + BRIDGE_WORKSPACE_COMMAND_DEFAULT_OUTPUT_BYTES; + const timeoutMs = + request.timeoutMs ?? BRIDGE_WORKSPACE_COMMAND_DEFAULT_TIMEOUT_MS; + return await new Promise( + (resolvePromise, reject) => { + let child: ChildProcessWithoutNullStreams; + try { + child = this.spawnCommand( + wrapped.argv[0], + wrapped.argv.slice(1), + { + cwd, + env: { + ...wrapped.env, + ...this.scratchEnvironment(), + ...TRUSTED_GIT_CONFIG_ENTRIES, + GIT_CONFIG_COUNT: + wrapped.env.GIT_CONFIG_COUNT ?? + TRUSTED_GIT_CONFIG_COUNT, + GIT_CONFIG_GLOBAL: + this.platform === 'win32' + ? 'NUL' + : '/dev/null', + GIT_CONFIG_NOSYSTEM: '1', + }, + detached: this.platform !== 'win32', + shell: false, + windowsHide: true, + }, + ); + child.stdin.end(); + } catch { + reject( + new WorkspaceToolError( + 'Native sandbox command could not start', + 'COMMAND_UNAVAILABLE', + ), + ); + return; + } + 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 abort = (): void => { + if (settled) return; + this.killCommandTree(child); + }; + signal?.addEventListener('abort', abort, { once: true }); + if (signal?.aborted) abort(); + const timer = setTimeout(() => { + if (settled) return; + timedOut = true; + this.killCommandTree(child); + }, timeoutMs); + const cleanup = (): void => { + clearTimeout(timer); + signal?.removeEventListener('abort', abort); + }; + child.once('error', () => { + if (settled) return; + settled = true; + const mayHaveStarted = child.pid != null; + this.killCommandTree(child); + cleanup(); + reject( + new WorkspaceToolError( + 'Native sandbox command could not start', + 'COMMAND_UNAVAILABLE', + mayHaveStarted, + ), + ); + }); + child.once('close', (code, childSignal) => { + if (settled) return; + settled = true; + this.killCommandTree(child); + cleanup(); + if (signal?.aborted) { + reject( + new WorkspaceToolError( + 'Workspace command execution aborted', + 'EXECUTION_ABORTED', + true, + ), + ); + 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, + ); + } catch { + // Preserve the bounded child error if optional violation annotation fails. + } + const stderrValue = boundedUtf8( + Buffer.from(annotatedStderr), + stderrBudget, + ); + resolvePromise({ + protocolVersion: BRIDGE_PROTOCOL_VERSION, + operation: 'execute_command', + workspaceId: request.workspaceId, + exitCode: + timedOut || childSignal + ? null + : this.protocolExitCode(code), + ...(childSignal ? { signal: childSignal } : {}), + stdout: stdoutValue, + stderr: stderrValue, + truncated: + truncated || + Buffer.byteLength(annotatedStderr) > stderrBudget, + timedOut, + }); + }); + }, ); - } - return await this.runWrapped(request, wrapped, cwd, commandId, signal); - } finally { - // A successful wrap owns command state even when no child is spawned. - try { - this.manager.cleanupAfterCommand(); - } catch { - // Cleanup is retried by close(); command settlement must still finish. - } } - } - - private async withTemporaryHostEnvironment( - values: Record, - action: () => Promise, - ): Promise { - const previousMutation = hostEnvironmentMutationQueue; - let releaseMutation!: () => void; - hostEnvironmentMutationQueue = new Promise((resolve) => { - releaseMutation = resolve; - }); - await previousMutation; - const previous = new Map(); - try { - for (const [name, value] of Object.entries(values)) { - previous.set(name, process.env[name]); - process.env[name] = value; - } - return await action(); - } finally { - for (const [name, value] of previous) { - if (value === undefined) delete process.env[name]; - else process.env[name] = value; - } - releaseMutation(); - } - } - - private async runWrapped( - request: WorkspaceExecuteCommandRequest, - wrapped: { argv: string[]; env: NodeJS.ProcessEnv }, - cwd: string, - commandId: string, - signal?: AbortSignal, - ): Promise { - const outputLimit = - request.maxOutputBytes ?? BRIDGE_WORKSPACE_COMMAND_DEFAULT_OUTPUT_BYTES; - const timeoutMs = - request.timeoutMs ?? BRIDGE_WORKSPACE_COMMAND_DEFAULT_TIMEOUT_MS; - return await new Promise( - (resolvePromise, reject) => { - let child: ChildProcessWithoutNullStreams; + + private killCommandTree(child: ChildProcessWithoutNullStreams): void { try { - child = this.spawnCommand(wrapped.argv[0], wrapped.argv.slice(1), { - cwd, - env: { - ...wrapped.env, - ...this.scratchEnvironment(), - ...TRUSTED_GIT_CONFIG_ENTRIES, - GIT_CONFIG_COUNT: - wrapped.env.GIT_CONFIG_COUNT ?? TRUSTED_GIT_CONFIG_COUNT, - GIT_CONFIG_GLOBAL: - this.platform === 'win32' ? 'NUL' : '/dev/null', - GIT_CONFIG_NOSYSTEM: '1', - }, - detached: this.platform !== 'win32', - shell: false, - windowsHide: true, - }); - child.stdin.end(); + if (this.platform !== 'win32' && child.pid != null) { + process.kill(-child.pid, 'SIGKILL'); + } else { + child.kill('SIGKILL'); + } } catch { - reject( - new WorkspaceToolError( - 'Native sandbox command could not start', - 'COMMAND_UNAVAILABLE', - ), - ); - return; + // The command group has already exited. } - 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 abort = (): void => { - if (settled) return; - this.killCommandTree(child); - }; - signal?.addEventListener('abort', abort, { once: true }); - if (signal?.aborted) abort(); - const timer = setTimeout(() => { - if (settled) return; - timedOut = true; - this.killCommandTree(child); - }, timeoutMs); - const cleanup = (): void => { - clearTimeout(timer); - signal?.removeEventListener('abort', abort); - }; - child.once('error', () => { - if (settled) return; - settled = true; - const mayHaveStarted = child.pid != null; - this.killCommandTree(child); - cleanup(); - reject( - new WorkspaceToolError( - 'Native sandbox command could not start', - 'COMMAND_UNAVAILABLE', - mayHaveStarted, - ), - ); - }); - child.once('close', (code, childSignal) => { - if (settled) return; - settled = true; - this.killCommandTree(child); - cleanup(); - if (signal?.aborted) { - reject( - new WorkspaceToolError( - 'Workspace command execution aborted', - 'EXECUTION_ABORTED', - true, - ), + } + + private protocolExitCode(code: number | null): number { + return Number.isSafeInteger(code) && + code != null && + code >= 0 && + code <= 255 + ? code + : 1; + } + + private async createScratchDirectory( + sharedScratchPaths: string[], + ): Promise { + // Windows SRT supplies the restricted account's private TEMP directory. + if (this.platform === 'win32') return undefined; + if (this.scratchDirectory || this.scratchHandle) { + throw new Error( + 'Native sandbox scratch cleanup is still pending; close the sandbox before reinitializing', ); - 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, + } + const canonicalTemporaryRoot = await canonicalPath(HOST_TEMPORARY_ROOT); + const sharedScratchRoot = sharedScratchPaths.find(path => + isWithin(path, canonicalTemporaryRoot), + ); + const scratchDirectory = await mkdtemp( + join( + sharedScratchRoot + ? dirname(sharedScratchRoot) + : canonicalTemporaryRoot, + NATIVE_SANDBOX_SCRATCH_PREFIX, + ), + ); + try { + await assertPrivateStorageAncestors(scratchDirectory); + const scratchHandle = await open(scratchDirectory, 'r'); + try { + await removePrivateStorageAcl(scratchHandle, scratchDirectory); + await scratchHandle.chmod(0o700); + await assertPrivateStorageAcl( + scratchHandle, + scratchDirectory, + true, + ); + if (((await scratchHandle.stat()).mode & 0o777) !== 0o700) { + throw new Error( + 'Native sandbox scratch directory is not private', + ); + } + this.scratchHandle = scratchHandle; + } catch (error) { + await scratchHandle.close(); + throw error; + } + this.scratchDirectory = await realpath(scratchDirectory); + return this.scratchDirectory; + } catch (error) { + await this.scratchHandle?.close().catch(() => undefined); + this.scratchHandle = undefined; + await rm(scratchDirectory, { recursive: true, force: true }).catch( + () => undefined, ); - } catch { - // Preserve the bounded child error if optional violation annotation fails. - } - const stderrValue = boundedUtf8( - Buffer.from(annotatedStderr), - stderrBudget, - ); - resolvePromise({ - protocolVersion: BRIDGE_PROTOCOL_VERSION, - operation: 'execute_command', - workspaceId: request.workspaceId, - exitCode: - timedOut || childSignal ? null : this.protocolExitCode(code), - ...(childSignal ? { signal: childSignal } : {}), - stdout: stdoutValue, - stderr: stderrValue, - truncated: - truncated || Buffer.byteLength(annotatedStderr) > stderrBudget, - timedOut, - }); - }); - }, - ); - } - - private killCommandTree(child: ChildProcessWithoutNullStreams): void { - try { - if (this.platform !== 'win32' && child.pid != null) { - process.kill(-child.pid, 'SIGKILL'); - } else { - child.kill('SIGKILL'); - } - } catch { - // The command group has already exited. + throw error; + } } - } - - private protocolExitCode(code: number | null): number { - return Number.isSafeInteger(code) && - code != null && - code >= 0 && - code <= 255 - ? code - : 1; - } - - private async createScratchDirectory( - sharedScratchPaths: string[], - ): Promise { - // Windows SRT supplies the restricted account's private TEMP directory. - if (this.platform === 'win32') return undefined; - const canonicalTemporaryRoot = await canonicalPath(HOST_TEMPORARY_ROOT); - const sharedScratchRoot = sharedScratchPaths.find((path) => - isWithin(path, canonicalTemporaryRoot), - ); - const scratchDirectory = await mkdtemp( - join( - sharedScratchRoot - ? dirname(sharedScratchRoot) - : canonicalTemporaryRoot, - NATIVE_SANDBOX_SCRATCH_PREFIX, - ), - ); - try { - await assertPrivateStorageAncestors(scratchDirectory); - const scratchHandle = await open(scratchDirectory, 'r'); - try { - await removePrivateStorageAcl(scratchHandle, scratchDirectory); - await scratchHandle.chmod(0o700); - await assertPrivateStorageAcl( - scratchHandle, - scratchDirectory, - true, + + private scratchEnvironment(): NodeJS.ProcessEnv { + const scratchDirectory = this.scratchDirectory; + if (!scratchDirectory) return {}; + return this.platform === 'win32' + ? { + TMPDIR: scratchDirectory, + TEMP: scratchDirectory, + TMP: scratchDirectory, + } + : { TMPDIR: scratchDirectory }; + } + + private scratchSelectorEnvironment(): NodeJS.ProcessEnv { + const scratchDirectory = this.scratchDirectory; + if (!scratchDirectory) return {}; + return Object.fromEntries( + SRT_SCRATCH_SELECTOR_NAMES.map(name => [name, scratchDirectory]), ); - if (((await scratchHandle.stat()).mode & 0o777) !== 0o700) { - throw new Error('Native sandbox scratch directory is not private'); + } + + private async removeScratchDirectory(): Promise { + const scratchDirectory = this.scratchDirectory; + if (!scratchDirectory) return; + const scratchHandle = this.scratchHandle; + if (!scratchHandle) { + throw new Error('Native sandbox scratch descriptor is unavailable'); + } + try { + await rm(scratchDirectory, { recursive: true, force: true }); + } catch { + await restoreScratchTraversal(scratchHandle); + await rm(scratchDirectory, { recursive: true, force: true }); } - this.scratchHandle = scratchHandle; - } catch (error) { + // Retain both the descriptor and path when cleanup fails so close() can + // retry without falling back to an attacker-replaceable ambient path. await scratchHandle.close(); - throw error; - } - this.scratchDirectory = await realpath(scratchDirectory); - return this.scratchDirectory; - } catch (error) { - await this.scratchHandle?.close().catch(() => undefined); - this.scratchHandle = undefined; - await rm(scratchDirectory, { recursive: true, force: true }).catch( - () => undefined, - ); - throw error; + this.scratchHandle = undefined; + this.scratchDirectory = undefined; } - } - - private scratchEnvironment(): NodeJS.ProcessEnv { - const scratchDirectory = this.scratchDirectory; - if (!scratchDirectory) return {}; - return this.platform === 'win32' - ? { - TMPDIR: scratchDirectory, - TEMP: scratchDirectory, - TMP: scratchDirectory, + + async close(): Promise { + if (this.closing) return this.closing; + const closing = this.closeExclusive(); + this.closing = closing; + try { + await closing; + } finally { + this.closing = undefined; } - : { TMPDIR: scratchDirectory }; - } - - private scratchSelectorEnvironment(): NodeJS.ProcessEnv { - const scratchDirectory = this.scratchDirectory; - if (!scratchDirectory) return {}; - return Object.fromEntries( - SRT_SCRATCH_SELECTOR_NAMES.map((name) => [name, scratchDirectory]), - ); - } - - private async removeScratchDirectory(): Promise { - const scratchDirectory = this.scratchDirectory; - if (!scratchDirectory) return; - const scratchHandle = this.scratchHandle; - if (!scratchHandle) { - throw new Error('Native sandbox scratch descriptor is unavailable'); - } - try { - await rm(scratchDirectory, { recursive: true, force: true }); - } catch { - await restoreScratchTraversal(scratchHandle); - await rm(scratchDirectory, { recursive: true, force: true }); } - // Retain both the descriptor and path when cleanup fails so close() can - // retry without falling back to an attacker-replaceable ambient path. - await scratchHandle.close(); - this.scratchHandle = undefined; - this.scratchDirectory = undefined; - } - - async close(): Promise { - if (this.closing) return this.closing; - const closing = this.closeExclusive(); - this.closing = closing; - try { - await closing; - } finally { - this.closing = undefined; - } - } - - private async closeExclusive(): Promise { - // Never reset proxy/credential state or remove scratch beneath a live child. - await this.execution?.catch(() => undefined); - await this.initialized?.catch(() => undefined); - if (managerOwners.get(this.manager) === this) { - this.resetFailed = true; - await this.manager.reset(); - this.resetFailed = false; - managerOwners.delete(this.manager); + + private async closeExclusive(): Promise { + // Never reset proxy/credential state or remove scratch beneath a live child. + await this.execution?.catch(() => undefined); + await this.initialized?.catch(() => undefined); + if (managerOwners.get(this.manager) === this) { + this.resetFailed = true; + await this.manager.reset(); + this.resetFailed = false; + managerOwners.delete(this.manager); + } + this.initialized = undefined; + this.canonicalRoot = undefined; + await this.removeScratchDirectory(); } - this.initialized = undefined; - this.canonicalRoot = undefined; - await this.removeScratchDirectory(); - } } diff --git a/packages/code/src/native-scratch.ts b/packages/code/src/native-scratch.ts index 59338a7..ae8ef0f 100644 --- a/packages/code/src/native-scratch.ts +++ b/packages/code/src/native-scratch.ts @@ -1,5 +1,5 @@ import { constants as fsConstants } from 'node:fs'; -import { readdir } from 'node:fs/promises'; +import { opendir } from 'node:fs/promises'; import type { FileHandle } from 'node:fs/promises'; import koffi from 'koffi'; @@ -7,107 +7,137 @@ import koffi from 'koffi'; import { removePrivateStorageAcl } from './private-storage.js'; const POSIX_PLATFORMS = new Set(['darwin', 'linux']); -const lib = POSIX_PLATFORMS.has(process.platform) ? koffi.load(null) : undefined; -const openat = lib?.func('int openat(int dirfd, const char *path, int flags, uint32_t mode)'); +const lib = POSIX_PLATFORMS.has(process.platform) + ? koffi.load(null) + : undefined; +const openat = lib?.func( + 'int openat(int dirfd, const char *path, int flags, uint32_t mode)', +); const closeFd = lib?.func('int close(int fd)'); const fchmod = lib?.func('int fchmod(int fd, uint32_t mode)'); const fchmodat = lib?.func( - 'int fchmodat(int dirfd, const char *path, uint32_t mode, int flags)', + 'int fchmodat(int dirfd, const char *path, uint32_t mode, int flags)', ); const AT_SYMLINK_NOFOLLOW = process.platform === 'darwin' ? 0x0020 : 0x0100; const O_EVTONLY = 0x8000; +// Recovery is a last-resort shutdown path over an attacker-controlled tree. +// Keep both its memory use and its descriptor-relative reopen work bounded. +const MAX_SCRATCH_DIRECTORIES = 10_000; +const MAX_SCRATCH_ENTRIES = 100_000; +const MAX_SCRATCH_DEPTH = 128; +const MAX_SCRATCH_COMPONENT_VISITS = 16_384; const IGNORED_ENTRY_ERRNOS = new Set([ - koffi.os.errno.ENOENT, - koffi.os.errno.ELOOP, - koffi.os.errno.ENOTDIR, - koffi.os.errno.ENOTSUP, + koffi.os.errno.ENOENT, + koffi.os.errno.ELOOP, + koffi.os.errno.ENOTDIR, + koffi.os.errno.ENOTSUP, ]); export interface ScratchTraversalHooks { - /** Test seam for deterministic replacement-race coverage. */ - afterEntryInspected?(directoryFd: number, name: string): Promise; + /** Test seam for deterministic replacement-race coverage. */ + afterEntryInspected?(directoryFd: number, name: string): Promise; } function descriptorPath(fd: number): string { - return process.platform === 'linux' ? `/proc/self/fd/${fd}` : `/dev/fd/${fd}`; + return process.platform === 'linux' + ? `/proc/self/fd/${fd}` + : `/dev/fd/${fd}`; } function requirePosixBindings(): void { - if (!openat || !closeFd || !fchmod || !fchmodat) { - throw new Error('Descriptor-relative scratch cleanup is unavailable'); - } + if (!openat || !closeFd || !fchmod || !fchmodat) { + throw new Error('Descriptor-relative scratch cleanup is unavailable'); + } } function ignoredEntryError(): boolean { - return IGNORED_ENTRY_ERRNOS.has(koffi.errno()); + return IGNORED_ENTRY_ERRNOS.has(koffi.errno()); } function restoreEntryMode(directoryFd: number, name: string): boolean { - requirePosixBindings(); - if (fchmodat!(directoryFd, name, 0o700, AT_SYMLINK_NOFOLLOW) === 0) return true; - if (ignoredEntryError()) return false; - throw new Error(`Descriptor-relative scratch chmod failed with errno ${koffi.errno()}`); + requirePosixBindings(); + if (fchmodat!(directoryFd, name, 0o700, AT_SYMLINK_NOFOLLOW) === 0) + return true; + if (ignoredEntryError()) return false; + throw new Error( + `Descriptor-relative scratch chmod failed with errno ${koffi.errno()}`, + ); } -function openDirectoryAt(directoryFd: number, name: string): number | undefined { - requirePosixBindings(); - const commonFlags = fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW; - const repairFlags = process.platform === 'darwin' - ? commonFlags | O_EVTONLY - : commonFlags | fsConstants.O_RDONLY; - const fd = openat!(directoryFd, name, repairFlags, 0); - if (fd >= 0) return fd; - if (ignoredEntryError()) return undefined; - throw new Error(`Descriptor-relative scratch open failed with errno ${koffi.errno()}`); +function openDirectoryAt( + directoryFd: number, + name: string, +): number | undefined { + requirePosixBindings(); + const commonFlags = fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW; + const repairFlags = + process.platform === 'darwin' + ? commonFlags | O_EVTONLY + : commonFlags | fsConstants.O_RDONLY; + const fd = openat!(directoryFd, name, repairFlags, 0); + if (fd >= 0) return fd; + if (ignoredEntryError()) return undefined; + throw new Error( + `Descriptor-relative scratch open failed with errno ${koffi.errno()}`, + ); } function closeDirectory(fd: number): void { - requirePosixBindings(); - if (closeFd!(fd) !== 0) { - throw new Error(`Descriptor-relative scratch close failed with errno ${koffi.errno()}`); - } + requirePosixBindings(); + if (closeFd!(fd) !== 0) { + throw new Error( + `Descriptor-relative scratch close failed with errno ${koffi.errno()}`, + ); + } } async function repairDirectory(fd: number, label: string): Promise { - requirePosixBindings(); - if (fchmod!(fd, 0o700) !== 0) { - throw new Error(`Descriptor-relative scratch chmod failed with errno ${koffi.errno()}`); - } - await removePrivateStorageAcl({ fd }, label); + requirePosixBindings(); + if (fchmod!(fd, 0o700) !== 0) { + throw new Error( + `Descriptor-relative scratch chmod failed with errno ${koffi.errno()}`, + ); + } + await removePrivateStorageAcl({ fd }, label); } async function openRelativeDirectory( - rootFd: number, - components: string[], - hooks: ScratchTraversalHooks, + rootFd: number, + components: string[], + hooks: ScratchTraversalHooks, + consumeComponentVisit: () => void, ): Promise { - let currentFd = rootFd; - try { - for (const component of components) { - await hooks.afterEntryInspected?.(currentFd, component); - if (!restoreEntryMode(currentFd, component)) { - if (currentFd !== rootFd) { - const closingFd = currentFd; - currentFd = rootFd; - closeDirectory(closingFd); + let currentFd = rootFd; + try { + for (const component of components) { + consumeComponentVisit(); + await hooks.afterEntryInspected?.(currentFd, component); + if (!restoreEntryMode(currentFd, component)) { + if (currentFd !== rootFd) { + const closingFd = currentFd; + currentFd = rootFd; + closeDirectory(closingFd); + } + return undefined; + } + const childFd = openDirectoryAt(currentFd, component); + if (currentFd !== rootFd) { + const closingFd = currentFd; + currentFd = rootFd; + closeDirectory(closingFd); + } + if (childFd === undefined) return undefined; + currentFd = childFd; + await repairDirectory( + currentFd, + `scratch directory ${components.join('/')}`, + ); } - return undefined; - } - const childFd = openDirectoryAt(currentFd, component); - if (currentFd !== rootFd) { - const closingFd = currentFd; - currentFd = rootFd; - closeDirectory(closingFd); - } - if (childFd === undefined) return undefined; - currentFd = childFd; - await repairDirectory(currentFd, `scratch directory ${components.join('/')}`); + return currentFd; + } catch (error) { + if (currentFd !== rootFd) closeDirectory(currentFd); + throw error; } - return currentFd; - } catch (error) { - if (currentFd !== rootFd) closeDirectory(currentFd); - throw error; - } } /** @@ -116,25 +146,58 @@ async function openRelativeDirectory( * path holds at most two descriptors and refuses replacement symlinks. */ export async function restoreScratchTraversal( - root: FileHandle, - hooks: ScratchTraversalHooks = {}, + root: FileHandle, + hooks: ScratchTraversalHooks = {}, ): Promise { - await root.chmod(0o700); - await removePrivateStorageAcl(root, 'native sandbox scratch root'); - const pending: string[][] = [[]]; - for (let index = 0; index < pending.length; index += 1) { - const components = pending[index]; - const directoryFd = components.length === 0 - ? root.fd - : await openRelativeDirectory(root.fd, components, hooks); - if (directoryFd === undefined) continue; - try { - const entries = await readdir(descriptorPath(directoryFd), { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) pending.push([...components, entry.name]); - } - } finally { - if (directoryFd !== root.fd) closeDirectory(directoryFd); + await root.chmod(0o700); + await removePrivateStorageAcl(root, 'native sandbox scratch root'); + const pending: string[][] = [[]]; + let componentVisits = 0; + const consumeComponentVisit = () => { + componentVisits += 1; + if (componentVisits > MAX_SCRATCH_COMPONENT_VISITS) { + throw new Error( + 'Native sandbox scratch cleanup exceeded its work limit', + ); + } + }; + let entriesInspected = 0; + for (let index = 0; index < pending.length; index += 1) { + const components = pending[index]; + const directoryFd = + components.length === 0 + ? root.fd + : await openRelativeDirectory( + root.fd, + components, + hooks, + consumeComponentVisit, + ); + if (directoryFd === undefined) continue; + try { + const directory = await opendir(descriptorPath(directoryFd)); + for await (const entry of directory) { + entriesInspected += 1; + if (entriesInspected > MAX_SCRATCH_ENTRIES) { + throw new Error( + 'Native sandbox scratch cleanup exceeded its entry limit', + ); + } + if (!entry.isDirectory()) continue; + if (components.length >= MAX_SCRATCH_DEPTH) { + throw new Error( + 'Native sandbox scratch cleanup exceeded its depth limit', + ); + } + if (pending.length >= MAX_SCRATCH_DIRECTORIES) { + throw new Error( + 'Native sandbox scratch cleanup exceeded its directory limit', + ); + } + pending.push([...components, entry.name]); + } + } finally { + if (directoryFd !== root.fd) closeDirectory(directoryFd); + } } - } } From d0cdeaf730b7befd927a90f764b5297b382bc670 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 8 Sep 2026 23:42:42 -0400 Subject: [PATCH 5/6] Revert "fix: Bound Native Scratch Recovery Work" This reverts commit eb761149fb8c6c11a09b4bfa6bd0c3c03182dbd6. --- packages/code/src/native-sandbox.test.ts | 2153 ++++++++++------------ packages/code/src/native-sandbox.ts | 1547 ++++++++-------- packages/code/src/native-scratch.ts | 233 +-- 3 files changed, 1854 insertions(+), 2079 deletions(-) diff --git a/packages/code/src/native-sandbox.test.ts b/packages/code/src/native-sandbox.test.ts index 9207e19..9289271 100644 --- a/packages/code/src/native-sandbox.test.ts +++ b/packages/code/src/native-sandbox.test.ts @@ -2,17 +2,17 @@ import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { EventEmitter } from 'node:events'; import { - access, - chmod, - mkdtemp, - mkdir, - open, - realpath, - rename, - rm, - stat, - symlink, - writeFile, + access, + chmod, + mkdtemp, + mkdir, + open, + realpath, + rename, + rm, + stat, + symlink, + writeFile, } from 'node:fs/promises'; import { tmpdir, homedir } from 'node:os'; import { join } from 'node:path'; @@ -27,1213 +27,1102 @@ import { restoreScratchTraversal } from './native-scratch.js'; import { WorkspaceToolError } from './workspace.js'; const request = { - protocolVersion: 1 as const, - operation: 'execute_command' as const, - workspaceId: 'primary', - command: 'printf hello', - timeoutMs: 1_000, - maxOutputBytes: 64, + protocolVersion: 1 as const, + operation: 'execute_command' as const, + workspaceId: 'primary', + command: 'printf hello', + timeoutMs: 1_000, + maxOutputBytes: 64, }; function fakeManager( - options: { - dependencyErrors?: string[]; - beforeWrap?: () => Promise; - appendGitSafeDirectory?: boolean; - inheritedGitEnvironment?: Record; - initializeError?: Error; - wrappedEnvironment?: NodeJS.ProcessEnv; - } = {}, + options: { + dependencyErrors?: string[]; + beforeWrap?: () => Promise; + appendGitSafeDirectory?: boolean; + inheritedGitEnvironment?: Record; + initializeError?: Error; + wrappedEnvironment?: NodeJS.ProcessEnv; + } = {}, ) { - let config: SandboxRuntimeConfig | undefined; - let reset = false; - let credentialSeenDuringWrap: string | undefined; - let gitLfsRequiredSeenDuringWrap: string | undefined; - let scratchSelectorSeenDuringWrap: string | undefined; - const manager = { - isSupportedPlatform: () => true, - async checkDependenciesAsync() { - return { warnings: [], errors: options.dependencyErrors ?? [] }; - }, - async initialize(value: SandboxRuntimeConfig) { - config = value; - if (options.initializeError) throw options.initializeError; - }, - async wrapWithSandboxArgv(command: string) { - await options.beforeWrap?.(); - credentialSeenDuringWrap = - process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - gitLfsRequiredSeenDuringWrap = process.env.GIT_CONFIG_VALUE_3; - scratchSelectorSeenDuringWrap = process.env.CLAUDE_CODE_TMPDIR; - const ambientGitEnvironment = Object.fromEntries( - Object.entries(process.env).filter( - ([name, value]) => - name.startsWith('GIT_CONFIG_') && value != null, - ), - ); - let gitEnvironment = ambientGitEnvironment; - if (options.appendGitSafeDirectory) { - const index = Number( - ambientGitEnvironment.GIT_CONFIG_COUNT ?? '0', - ); - gitEnvironment = { - ...(options.inheritedGitEnvironment ?? {}), - GIT_CONFIG_COUNT: String(index + 1), - [`GIT_CONFIG_KEY_${index}`]: 'safe.directory', - [`GIT_CONFIG_VALUE_${index}`]: '/workspace', - }; - } - return { - argv: ['/bin/bash', '-c', command], - env: { - PATH: process.env.PATH, - ...gitEnvironment, - ...options.wrappedEnvironment, - ...(credentialSeenDuringWrap - ? { - LIBRECHAT_CODE_TEST_CREDENTIAL: - 'Authorization: Bearer srt-sentinel', - } - : {}), - }, - }; - }, - annotateStderrWithSandboxFailures(_commandId: string, stderr: string) { - return stderr; - }, - cleanupAfterCommand() {}, - async reset() { - reset = true; - }, - }; - return { - manager, - get config() { - return config; - }, - get reset() { - return reset; - }, - get credentialSeenDuringWrap() { - return credentialSeenDuringWrap; - }, - get gitLfsRequiredSeenDuringWrap() { - return gitLfsRequiredSeenDuringWrap; - }, - get scratchSelectorSeenDuringWrap() { - return scratchSelectorSeenDuringWrap; + let config: SandboxRuntimeConfig | undefined; + let reset = false; + let credentialSeenDuringWrap: string | undefined; + let gitLfsRequiredSeenDuringWrap: string | undefined; + let scratchSelectorSeenDuringWrap: string | undefined; + const manager = { + isSupportedPlatform: () => true, + async checkDependenciesAsync() { + return { warnings: [], errors: options.dependencyErrors ?? [] }; + }, + async initialize(value: SandboxRuntimeConfig) { + config = value; + if (options.initializeError) throw options.initializeError; + }, + async wrapWithSandboxArgv(command: string) { + await options.beforeWrap?.(); + credentialSeenDuringWrap = process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + gitLfsRequiredSeenDuringWrap = process.env.GIT_CONFIG_VALUE_3; + scratchSelectorSeenDuringWrap = process.env.CLAUDE_CODE_TMPDIR; + const ambientGitEnvironment = Object.fromEntries( + Object.entries(process.env).filter( + ([name, value]) => name.startsWith('GIT_CONFIG_') && value != null, + ), + ); + let gitEnvironment = ambientGitEnvironment; + if (options.appendGitSafeDirectory) { + const index = Number(ambientGitEnvironment.GIT_CONFIG_COUNT ?? '0'); + gitEnvironment = { + ...(options.inheritedGitEnvironment ?? {}), + GIT_CONFIG_COUNT: String(index + 1), + [`GIT_CONFIG_KEY_${index}`]: 'safe.directory', + [`GIT_CONFIG_VALUE_${index}`]: '/workspace', + }; + } + return { + argv: ['/bin/bash', '-c', command], + env: { + PATH: process.env.PATH, + ...gitEnvironment, + ...options.wrappedEnvironment, + ...(credentialSeenDuringWrap + ? { + LIBRECHAT_CODE_TEST_CREDENTIAL: + 'Authorization: Bearer srt-sentinel', + } + : {}), }, - }; + }; + }, + annotateStderrWithSandboxFailures(_commandId: string, stderr: string) { + return stderr; + }, + cleanupAfterCommand() {}, + async reset() { + reset = true; + }, + }; + return { + manager, + get config() { + return config; + }, + get reset() { + return reset; + }, + get credentialSeenDuringWrap() { + return credentialSeenDuringWrap; + }, + get gitLfsRequiredSeenDuringWrap() { + return gitLfsRequiredSeenDuringWrap; + }, + get scratchSelectorSeenDuringWrap() { + return scratchSelectorSeenDuringWrap; + }, + }; } -test('exclusive lifecycle rejects a second workspace sharing an SRT manager', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - const first = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - const second = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - t.after(() => first.close()); - t.after(() => second.close()); - await first.prepare(); - await assert.rejects( - second.prepare(), - /already belongs to another workspace/, - ); - await second.close(); - assert.equal( - fake.reset, - false, - 'a rejected owner must not reset the live manager', - ); - assert.equal((await first.execute(request)).stdout, 'hello'); - await first.close(); - await second.prepare(); - assert.equal((await second.execute(request)).stdout, 'hello'); +test('exclusive lifecycle rejects a second workspace sharing an SRT manager', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + const first = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + const second = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + t.after(() => first.close()); + t.after(() => second.close()); + await first.prepare(); + await assert.rejects( + second.prepare(), + /already belongs to another workspace/, + ); + await second.close(); + assert.equal( + fake.reset, + false, + 'a rejected owner must not reset the live manager', + ); + assert.equal((await first.execute(request)).stdout, 'hello'); + await first.close(); + await second.prepare(); + assert.equal((await second.execute(request)).stdout, 'hello'); }); -test('exclusive lifecycle rejects overlapping commands and waits before resetting', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - let entered!: () => void; - const wrapping = new Promise(resolve => { - entered = resolve; - }); - let release!: () => void; - const gate = new Promise(resolve => { - release = resolve; - }); - const fake = fakeManager({ - beforeWrap: async () => { - entered(); - await gate; - }, - }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - t.after(() => sandbox.close()); - const execution = sandbox.execute(request); - await wrapping; - await assert.rejects(sandbox.execute(request), /active command/); - const closing = sandbox.close(); - const secondClose = sandbox.close(); - await new Promise(resolve => setImmediate(resolve)); - assert.equal(fake.reset, false); - await assert.rejects(sandbox.prepare(), /closing/); - release(); - assert.equal((await execution).stdout, 'hello'); - await Promise.all([closing, secondClose]); - assert.equal(fake.reset, true); +test('exclusive lifecycle rejects overlapping commands and waits before resetting', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + let entered!: () => void; + const wrapping = new Promise((resolve) => { + entered = resolve; + }); + let release!: () => void; + const gate = new Promise((resolve) => { + release = resolve; + }); + const fake = fakeManager({ + beforeWrap: async () => { + entered(); + await gate; + }, + }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + t.after(() => sandbox.close()); + const execution = sandbox.execute(request); + await wrapping; + await assert.rejects(sandbox.execute(request), /active command/); + const closing = sandbox.close(); + const secondClose = sandbox.close(); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(fake.reset, false); + await assert.rejects(sandbox.prepare(), /closing/); + release(); + assert.equal((await execution).stdout, 'hello'); + await Promise.all([closing, secondClose]); + assert.equal(fake.reset, true); }); -test('exclusive lifecycle retains ownership after a failed reset until cleanup succeeds', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - let failReset = true; - fake.manager.reset = async () => { - if (failReset) throw new Error('reset failed'); - }; - const first = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - const second = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - await first.prepare(); - await assert.rejects(first.close(), /reset failed/); - await assert.rejects(first.execute(request), /requires cleanup/); - await assert.rejects(second.prepare(), /already belongs/); - failReset = false; - await first.close(); - await second.prepare(); - await second.close(); +test('exclusive lifecycle retains ownership after a failed reset until cleanup succeeds', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + let failReset = true; + fake.manager.reset = async () => { + if (failReset) throw new Error('reset failed'); + }; + const first = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + const second = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + await first.prepare(); + await assert.rejects(first.close(), /reset failed/); + await assert.rejects(first.execute(request), /requires cleanup/); + await assert.rejects(second.prepare(), /already belongs/); + failReset = false; + await first.close(); + await second.prepare(); + await second.close(); }); -test('exclusive lifecycle waits for initialization before resetting the manager', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - let entered!: () => void; - const initializing = new Promise(resolve => { - entered = resolve; - }); - let release!: () => void; - const gate = new Promise(resolve => { - release = resolve; - }); - fake.manager.initialize = async () => { - entered(); - await gate; - }; - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - const preparing = sandbox.prepare(); - await initializing; - const closing = sandbox.close(); - await new Promise(resolve => setImmediate(resolve)); - assert.equal(fake.reset, false); - release(); - await preparing; - await closing; - assert.equal(fake.reset, true); +test('exclusive lifecycle waits for initialization before resetting the manager', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + let entered!: () => void; + const initializing = new Promise((resolve) => { + entered = resolve; + }); + let release!: () => void; + const gate = new Promise((resolve) => { + release = resolve; + }); + fake.manager.initialize = async () => { + entered(); + await gate; + }; + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + const preparing = sandbox.prepare(); + await initializing; + const closing = sandbox.close(); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(fake.reset, false); + release(); + await preparing; + await closing; + assert.equal(fake.reset, true); }); -test('initializes SRT with a default-deny network and scrubbed worker credentials', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - const identity = join(tmpdir(), 'librechat-code-identity.json'); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - protectedPaths: [identity], - environment: { - PATH: '/usr/bin', - Path: '/windows/system32', - LANG: 'en_US.UTF-8', - lc_api_token: 'lowercase-secret', - LIBRECHAT_CODE_WORKER_TOKEN: 'secret', - AWS_SECRET_ACCESS_KEY: 'secret', - }, - manager: fake.manager, - }); - - await sandbox.prepare(); - const canonicalRoot = await realpath(root); - const canonicalIdentity = await realpath(identity).catch(async () => - join(await realpath(tmpdir()), 'librechat-code-identity.json'), - ); - const canonicalHome = await realpath(homedir()); - const scratchDirectory = fake.config?.filesystem.allowWrite[1]; - assert.equal(typeof scratchDirectory, 'string'); - assert.deepEqual(fake.config?.network.allowedDomains, []); - assert.equal(fake.config?.network.strictAllowlist, true); - assert.equal(fake.config?.network.allowAllUnixSockets, false); - assert.deepEqual(fake.config?.filesystem.allowRead, [ - canonicalRoot, - scratchDirectory, - ]); - assert.deepEqual(fake.config?.filesystem.allowWrite, [ - canonicalRoot, - scratchDirectory, - ]); - assert.equal((await stat(scratchDirectory!)).mode & 0o777, 0o700); - assert.ok(fake.config?.filesystem.denyRead.includes(canonicalHome)); - assert.ok(fake.config?.filesystem.denyWrite.includes(canonicalIdentity)); - assert.ok( - fake.config?.filesystem.denyWrite.some(path => - path.endsWith('/tmp/claude'), - ), - ); - const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); - assert.ok(denied?.includes('LIBRECHAT_CODE_WORKER_TOKEN')); - assert.ok(denied?.includes('AWS_SECRET_ACCESS_KEY')); - assert.ok(!denied?.includes('PATH')); - assert.ok(denied?.includes('Path')); - assert.ok(denied?.includes('lc_api_token')); - assert.ok(denied?.includes('CLAUDE_CODE_TMPDIR')); - assert.ok(denied?.includes('CLAUDE_TMPDIR')); - await sandbox.close(); - assert.equal(fake.reset, true); - await assert.rejects(access(scratchDirectory!)); +test('initializes SRT with a default-deny network and scrubbed worker credentials', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + const identity = join(tmpdir(), 'librechat-code-identity.json'); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + protectedPaths: [identity], + environment: { + PATH: '/usr/bin', + Path: '/windows/system32', + LANG: 'en_US.UTF-8', + lc_api_token: 'lowercase-secret', + LIBRECHAT_CODE_WORKER_TOKEN: 'secret', + AWS_SECRET_ACCESS_KEY: 'secret', + }, + manager: fake.manager, + }); + + await sandbox.prepare(); + const canonicalRoot = await realpath(root); + const canonicalIdentity = await realpath(identity).catch(async () => + join(await realpath(tmpdir()), 'librechat-code-identity.json'), + ); + const canonicalHome = await realpath(homedir()); + const scratchDirectory = fake.config?.filesystem.allowWrite[1]; + assert.equal(typeof scratchDirectory, 'string'); + assert.deepEqual(fake.config?.network.allowedDomains, []); + assert.equal(fake.config?.network.strictAllowlist, true); + assert.equal(fake.config?.network.allowAllUnixSockets, false); + assert.deepEqual(fake.config?.filesystem.allowRead, [ + canonicalRoot, + scratchDirectory, + ]); + assert.deepEqual(fake.config?.filesystem.allowWrite, [ + canonicalRoot, + scratchDirectory, + ]); + assert.equal((await stat(scratchDirectory!)).mode & 0o777, 0o700); + assert.ok(fake.config?.filesystem.denyRead.includes(canonicalHome)); + assert.ok(fake.config?.filesystem.denyWrite.includes(canonicalIdentity)); + assert.ok( + fake.config?.filesystem.denyWrite.some((path) => + path.endsWith('/tmp/claude'), + ), + ); + const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); + assert.ok(denied?.includes('LIBRECHAT_CODE_WORKER_TOKEN')); + assert.ok(denied?.includes('AWS_SECRET_ACCESS_KEY')); + assert.ok(!denied?.includes('PATH')); + assert.ok(denied?.includes('Path')); + assert.ok(denied?.includes('lc_api_token')); + assert.ok(denied?.includes('CLAUDE_CODE_TMPDIR')); + assert.ok(denied?.includes('CLAUDE_TMPDIR')); + await sandbox.close(); + assert.equal(fake.reset, true); + await assert.rejects(access(scratchDirectory!)); }); -test('provides an isolated scratch directory to commands and restores the host environment', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const originalTmpdir = process.env.TMPDIR; - const originalSrtTmpdir = process.env.CLAUDE_CODE_TMPDIR; - const originalLegacySrtTmpdir = process.env.CLAUDE_TMPDIR; - const fake = fakeManager(); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - - const result = await sandbox.execute({ - ...request, - maxOutputBytes: 1_024, - command: 'touch "$TMPDIR/probe" && printf %s "$TMPDIR"', - }); - - assert.equal(result.exitCode, 0); - assert.match(result.stdout, /librechat-code-srt-/); - await access(join(result.stdout, 'probe')); - assert.equal(fake.scratchSelectorSeenDuringWrap, result.stdout); - assert.equal(process.env.TMPDIR, originalTmpdir); - assert.equal(process.env.CLAUDE_CODE_TMPDIR, originalSrtTmpdir); - assert.equal(process.env.CLAUDE_TMPDIR, originalLegacySrtTmpdir); - await sandbox.close(); - await assert.rejects(access(result.stdout)); +test('provides an isolated scratch directory to commands and restores the host environment', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const originalTmpdir = process.env.TMPDIR; + const originalSrtTmpdir = process.env.CLAUDE_CODE_TMPDIR; + const originalLegacySrtTmpdir = process.env.CLAUDE_TMPDIR; + const fake = fakeManager(); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + + const result = await sandbox.execute({ + ...request, + maxOutputBytes: 1_024, + command: 'touch "$TMPDIR/probe" && printf %s "$TMPDIR"', + }); + + assert.equal(result.exitCode, 0); + assert.match(result.stdout, /librechat-code-srt-/); + await access(join(result.stdout, 'probe')); + assert.equal(fake.scratchSelectorSeenDuringWrap, result.stdout); + assert.equal(process.env.TMPDIR, originalTmpdir); + assert.equal(process.env.CLAUDE_CODE_TMPDIR, originalSrtTmpdir); + assert.equal(process.env.CLAUDE_TMPDIR, originalLegacySrtTmpdir); + await sandbox.close(); + await assert.rejects(access(result.stdout)); }); -test('removes scratch storage when SRT initialization fails', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager({ initializeError: new Error('init failed') }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - - await assert.rejects(sandbox.prepare(), /init failed/); - const scratchDirectory = fake.config?.filesystem.allowWrite[1]; - assert.equal(typeof scratchDirectory, 'string'); - await assert.rejects(access(scratchDirectory!)); - assert.equal(fake.reset, true); +test('removes scratch storage when SRT initialization fails', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager({ initializeError: new Error('init failed') }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + + await assert.rejects(sandbox.prepare(), /init failed/); + const scratchDirectory = fake.config?.filesystem.allowWrite[1]; + assert.equal(typeof scratchDirectory, 'string'); + await assert.rejects(access(scratchDirectory!)); + assert.equal(fake.reset, true); }); -test('rejects workspaces nested inside SRT shared scratch storage', async t => { - if (process.platform === 'win32') return; - const sharedRoot = '/tmp/claude'; - await mkdir(sharedRoot, { recursive: true }); - const root = await mkdtemp(join(sharedRoot, 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fakeManager().manager, - }); - - await assert.rejects( - sandbox.prepare(), - (error: WorkspaceToolError) => - error.code === 'REGISTRATION_INVALID' && - /inherited writable path/.test(error.message), - ); +test('rejects workspaces nested inside SRT shared scratch storage', async (t) => { + if (process.platform === 'win32') return; + const sharedRoot = '/tmp/claude'; + await mkdir(sharedRoot, { recursive: true }); + const root = await mkdtemp(join(sharedRoot, 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fakeManager().manager, + }); + + await assert.rejects( + sandbox.prepare(), + (error: WorkspaceToolError) => + error.code === 'REGISTRATION_INVALID' && + /inherited writable path/.test(error.message), + ); }); test('rejects a workspace that contains worker scratch storage', async () => { - if (process.platform === 'win32') return; - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: tmpdir(), - manager: fakeManager().manager, - }); - - await assert.rejects( - sandbox.prepare(), - (error: WorkspaceToolError) => - error.code === 'REGISTRATION_INVALID' && - /contain worker scratch storage/.test(error.message), - ); - await sandbox.close(); -}); - -test('keeps concurrent sandbox scratch directories independent', async t => { - const firstRoot = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - const secondRoot = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(firstRoot, { recursive: true, force: true })); - t.after(() => rm(secondRoot, { recursive: true, force: true })); - let releaseWrap!: () => void; - let wrapStarted!: () => void; - const wrapStartedPromise = new Promise(resolve => { - wrapStarted = resolve; - }); - const holdWrap = new Promise(resolve => { - releaseWrap = resolve; - }); - const firstFake = fakeManager({ - async beforeWrap() { - wrapStarted(); - await holdWrap; - }, - }); - const secondFake = fakeManager(); - const firstSandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: firstRoot, - manager: firstFake.manager, - }); - const secondSandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: secondRoot, - manager: secondFake.manager, - }); - const firstExecution = firstSandbox.execute({ - ...request, - command: 'printf first', - }); - await wrapStartedPromise; - await secondSandbox.prepare(); - const firstScratch = firstFake.config?.filesystem.allowWrite[1]; - const secondScratch = secondFake.config?.filesystem.allowWrite[1]; - assert.equal(typeof firstScratch, 'string'); - assert.equal(typeof secondScratch, 'string'); - assert.notEqual(firstScratch, secondScratch); - assert.ok(!secondScratch!.startsWith(`${firstScratch}/`)); - releaseWrap(); - await firstExecution; - await firstSandbox.close(); - await access(secondScratch!); - await secondSandbox.close(); + if (process.platform === 'win32') return; + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: tmpdir(), + manager: fakeManager().manager, + }); + + await assert.rejects( + sandbox.prepare(), + (error: WorkspaceToolError) => + error.code === 'REGISTRATION_INVALID' && + /contain worker scratch storage/.test(error.message), + ); + await sandbox.close(); }); -test('removes scratch storage after a command revokes traversal permissions', async t => { - if (process.platform === 'win32') return; - 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, - }); - - const result = await sandbox.execute({ - ...request, - command: - 'printf %s "$TMPDIR"; mkdir -p "$TMPDIR/locked/deeper"; touch "$TMPDIR/locked/deeper/file"; chmod 000 "$TMPDIR/locked/deeper" "$TMPDIR/locked" "$TMPDIR"', - }); - - assert.equal(result.exitCode, 0); - await sandbox.close(); - await assert.rejects(access(result.stdout)); -}); - -test('scratch traversal never follows a descendant replaced after inspection', async t => { - if (process.platform === 'win32') return; - const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-race-')); - const outside = await mkdtemp(join(tmpdir(), 'librechat-code-outside-')); - const descendant = join(root, 'locked'); - const retired = join(root, 'retired'); - const outsideChild = join(outside, 'child'); - t.after(() => rm(root, { recursive: true, force: true })); - t.after(() => rm(outside, { recursive: true, force: true })); - await mkdir(descendant); - await mkdir(outsideChild); - await chmod(outside, 0o711); - await chmod(outsideChild, 0o711); - const rootHandle = await open(root, 'r'); - t.after(() => rootHandle.close()); - let swapped = false; - - await restoreScratchTraversal(rootHandle, { - async afterEntryInspected(_directoryFd, name) { - if (name !== 'locked' || swapped) return; - swapped = true; - await rename(descendant, retired); - await symlink(outside, descendant, 'dir'); - }, - }); - - assert.equal(swapped, true); - assert.equal((await stat(outside)).mode & 0o777, 0o711); - assert.equal((await stat(outsideChild)).mode & 0o777, 0o711); +test('keeps concurrent sandbox scratch directories independent', async (t) => { + const firstRoot = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + const secondRoot = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(firstRoot, { recursive: true, force: true })); + t.after(() => rm(secondRoot, { recursive: true, force: true })); + let releaseWrap!: () => void; + let wrapStarted!: () => void; + const wrapStartedPromise = new Promise((resolve) => { + wrapStarted = resolve; + }); + const holdWrap = new Promise((resolve) => { + releaseWrap = resolve; + }); + const firstFake = fakeManager({ + async beforeWrap() { + wrapStarted(); + await holdWrap; + }, + }); + const secondFake = fakeManager(); + const firstSandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: firstRoot, + manager: firstFake.manager, + }); + const secondSandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: secondRoot, + manager: secondFake.manager, + }); + const firstExecution = firstSandbox.execute({ + ...request, + command: 'printf first', + }); + await wrapStartedPromise; + await secondSandbox.prepare(); + const firstScratch = firstFake.config?.filesystem.allowWrite[1]; + const secondScratch = secondFake.config?.filesystem.allowWrite[1]; + assert.equal(typeof firstScratch, 'string'); + assert.equal(typeof secondScratch, 'string'); + assert.notEqual(firstScratch, secondScratch); + assert.ok(!secondScratch!.startsWith(`${firstScratch}/`)); + releaseWrap(); + await firstExecution; + await firstSandbox.close(); + await access(secondScratch!); + await secondSandbox.close(); }); -test('scratch traversal removes command-created Darwin ACLs', async t => { - if (process.platform !== 'darwin') return; - 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, - }); - const result = await sandbox.execute({ - ...request, - command: - 'printf %s "$TMPDIR"; mkdir -p "$TMPDIR/locked/deeper"; touch "$TMPDIR/locked/deeper/file"; chmod +a "$USER deny list,search,delete_child" "$TMPDIR/locked" "$TMPDIR"; chmod 000 "$TMPDIR/locked" "$TMPDIR"', - }); - - assert.equal(result.exitCode, 0); - await sandbox.close(); - await assert.rejects(access(result.stdout)); +test('removes scratch storage after a command revokes traversal permissions', async (t) => { + if (process.platform === 'win32') return; + 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, + }); + + const result = await sandbox.execute({ + ...request, + command: + 'printf %s "$TMPDIR"; mkdir -p "$TMPDIR/locked/deeper"; touch "$TMPDIR/locked/deeper/file"; chmod 000 "$TMPDIR/locked/deeper" "$TMPDIR/locked" "$TMPDIR"', + }); + + assert.equal(result.exitCode, 0); + await sandbox.close(); + await assert.rejects(access(result.stdout)); }); -test('scratch traversal bounds descriptors and work across a deep tree', async t => { - if (process.platform === 'win32') return; - const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-depth-')); - t.after(() => rm(root, { recursive: true, force: true })); - const directories = [root]; - for (let depth = 0; depth < 100; depth += 1) { - directories.push(join(directories[directories.length - 1], 'd')); - await mkdir(directories[directories.length - 1]); - } - for (const directory of directories.slice(1).reverse()) { - await chmod(directory, 0o000); - } - const rootHandle = await open(root, 'r'); - t.after(() => rootHandle.close()); - - await restoreScratchTraversal(rootHandle); - - assert.equal( - (await stat(directories[directories.length - 1])).mode & 0o777, - 0o700, - ); +test('scratch traversal never follows a descendant replaced after inspection', async (t) => { + if (process.platform === 'win32') return; + const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-race-')); + const outside = await mkdtemp(join(tmpdir(), 'librechat-code-outside-')); + const descendant = join(root, 'locked'); + const retired = join(root, 'retired'); + const outsideChild = join(outside, 'child'); + t.after(() => rm(root, { recursive: true, force: true })); + t.after(() => rm(outside, { recursive: true, force: true })); + await mkdir(descendant); + await mkdir(outsideChild); + await chmod(outside, 0o711); + await chmod(outsideChild, 0o711); + const rootHandle = await open(root, 'r'); + t.after(() => rootHandle.close()); + let swapped = false; + + await restoreScratchTraversal(rootHandle, { + async afterEntryInspected(_directoryFd, name) { + if (name !== 'locked' || swapped) return; + swapped = true; + await rename(descendant, retired); + await symlink(outside, descendant, 'dir'); + }, + }); + + assert.equal(swapped, true); + assert.equal((await stat(outside)).mode & 0o777, 0o711); + assert.equal((await stat(outsideChild)).mode & 0o777, 0o711); }); -test('scratch traversal rejects trees beyond its recovery depth limit', async t => { - if (process.platform === 'win32') return; - const root = await mkdtemp( - join(tmpdir(), 'librechat-code-scratch-depth-limit-'), - ); - t.after(() => rm(root, { recursive: true, force: true })); - let directory = root; - for (let depth = 0; depth < 129; depth += 1) { - directory = join(directory, 'd'); - await mkdir(directory); - } - const rootHandle = await open(root, 'r'); - t.after(() => rootHandle.close()); - - await assert.rejects( - restoreScratchTraversal(rootHandle), - /scratch cleanup exceeded its depth limit/, - ); +test('scratch traversal removes command-created Darwin ACLs', async (t) => { + if (process.platform !== 'darwin') return; + 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, + }); + const result = await sandbox.execute({ + ...request, + command: + 'printf %s "$TMPDIR"; mkdir -p "$TMPDIR/locked/deeper"; touch "$TMPDIR/locked/deeper/file"; chmod +a "$USER deny list,search,delete_child" "$TMPDIR/locked" "$TMPDIR"; chmod 000 "$TMPDIR/locked" "$TMPDIR"', + }); + + assert.equal(result.exitCode, 0); + await sandbox.close(); + await assert.rejects(access(result.stdout)); }); -test('does not replace scratch state while cleanup remains pending', async t => { - if (process.platform === 'win32') return; - const workspace = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - const retained = await mkdtemp(join(tmpdir(), 'librechat-code-retained-')); - const retainedHandle = await open(retained, 'r'); - t.after(() => retainedHandle.close()); - t.after(() => rm(retained, { recursive: true, force: true })); - t.after(() => rm(workspace, { recursive: true, force: true })); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: workspace, - manager: fakeManager().manager, - }); - const mutable = sandbox as unknown as { - scratchDirectory?: string; - scratchHandle?: typeof retainedHandle; - createScratchDirectory(paths: string[]): Promise; - }; - mutable.scratchDirectory = retained; - mutable.scratchHandle = retainedHandle; - - await assert.rejects( - mutable.createScratchDirectory([]), - /scratch cleanup is still pending/, - ); - assert.equal(mutable.scratchDirectory, retained); - assert.equal(mutable.scratchHandle, retainedHandle); +test('scratch traversal bounds descriptors across a deep tree', async (t) => { + if (process.platform === 'win32') return; + const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-depth-')); + t.after(() => rm(root, { recursive: true, force: true })); + const directories = [root]; + for (let depth = 0; depth < 300; depth += 1) { + directories.push(join(directories[directories.length - 1], 'd')); + await mkdir(directories[directories.length - 1]); + } + for (const directory of directories.slice(1).reverse()) { + await chmod(directory, 0o000); + } + const rootHandle = await open(root, 'r'); + t.after(() => rootHandle.close()); + + await restoreScratchTraversal(rootHandle); + + assert.equal((await stat(directories[directories.length - 1])).mode & 0o777, 0o700); }); const proxyEnvironment = { - HTTP_PROXY: 'http://upstream.invalid:8080', - HTTPS_PROXY: 'http://upstream.invalid:8080', - ALL_PROXY: 'socks5://upstream.invalid:1080', - NO_PROXY: 'upstream.internal', - http_proxy: 'http://upstream.invalid:8080', - https_proxy: 'http://upstream.invalid:8080', - all_proxy: 'socks5://upstream.invalid:1080', - no_proxy: 'upstream.internal', + HTTP_PROXY: 'http://upstream.invalid:8080', + HTTPS_PROXY: 'http://upstream.invalid:8080', + ALL_PROXY: 'socks5://upstream.invalid:1080', + NO_PROXY: 'upstream.internal', + http_proxy: 'http://upstream.invalid:8080', + https_proxy: 'http://upstream.invalid:8080', + all_proxy: 'socks5://upstream.invalid:1080', + no_proxy: 'upstream.internal', }; const windowsEnvironment = { - SYSTEMROOT: 'C:\\Windows', - SystemRoot: 'C:\\Windows', - SYSTEMDRIVE: 'C:', - windir: 'C:\\Windows', - ComSpec: 'C:\\Windows\\System32\\cmd.exe', - PATHEXT: '.COM;.EXE;.BAT;.CMD', - TEMP: 'C:\\Temp', - Temp: 'C:\\Temp', - TMP: 'C:\\Temp', - USERPROFILE: 'C:\\Users\\sandbox', - HOMEDRIVE: 'C:', - HOMEPATH: '\\Users\\sandbox', - APPDATA: 'C:\\Users\\sandbox\\AppData\\Roaming', - LOCALAPPDATA: 'C:\\Users\\sandbox\\AppData\\Local', + SYSTEMROOT: 'C:\\Windows', + SystemRoot: 'C:\\Windows', + SYSTEMDRIVE: 'C:', + windir: 'C:\\Windows', + ComSpec: 'C:\\Windows\\System32\\cmd.exe', + PATHEXT: '.COM;.EXE;.BAT;.CMD', + TEMP: 'C:\\Temp', + Temp: 'C:\\Temp', + TMP: 'C:\\Temp', + USERPROFILE: 'C:\\Users\\sandbox', + HOMEDRIVE: 'C:', + HOMEPATH: '\\Users\\sandbox', + APPDATA: 'C:\\Users\\sandbox\\AppData\\Roaming', + LOCALAPPDATA: 'C:\\Users\\sandbox\\AppData\\Local', }; for (const platform of ['darwin', 'linux', 'win32'] as const) { - test(`preserves required ${platform} environment names without allowing credentials`, async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - const credentials = { - LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret', - LIBRECHAT_CODE_HTTP_PROXY: 'worker-secret', - AWS_SECRET_ACCESS_KEY: 'aws-secret', - GITHUB_TOKEN: 'github-secret', - HTTP_PROXY_TOKEN: 'proxy-secret', - CUSTOM_PROXY: 'proxy-secret', - SYSTEMROOT_TOKEN: 'runtime-secret', - NODE_OPTIONS: '--require /host/private.js', - LD_PRELOAD: '/host/private.so', - }; - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - platform, - allowedDomains: ['github.com'], - environment: { - ...proxyEnvironment, - ...windowsEnvironment, - ...credentials, - HtTp_PrOxY: 'http://mixed-case.invalid:8080', - PATH: '/usr/bin', - LC_ALL: 'C.UTF-8', - }, - manager: fake.manager, - }); - t.after(() => sandbox.close()); - await sandbox.prepare(); - const denied = new Set( - fake.config?.credentials?.envVars - ?.filter(({ mode }) => mode === 'deny') - .map(({ name }) => name), - ); - for (const name of [ - ...Object.keys(proxyEnvironment), - 'PATH', - 'LC_ALL', - ]) { - assert.equal( - denied.has(name), - false, - `${name} must remain available`, - ); - } - for (const name of Object.keys(windowsEnvironment)) { - assert.equal( - denied.has(name), - platform !== 'win32', - `${name} must be platform-specific`, - ); - } - assert.equal(denied.has('HtTp_PrOxY'), platform !== 'win32'); - for (const name of Object.keys(credentials)) { - assert.equal(denied.has(name), true, `${name} must remain denied`); - } - assert.deepEqual(fake.config?.network.allowedDomains, ['github.com']); - assert.equal(fake.config?.network.strictAllowlist, true); - }); -} - -test('uses SRT proxy values without restoring inherited proxies or credentials', async t => { + test(`preserves required ${platform} environment names without allowing credentials`, async (t) => { const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); t.after(() => rm(root, { recursive: true, force: true })); - const wrappedEnvironment = { - HTTP_PROXY: 'http://localhost:3128', - HTTPS_PROXY: 'http://localhost:3128', - ALL_PROXY: 'http://localhost:3128', - NO_PROXY: 'localhost', + const fake = fakeManager(); + const credentials = { + LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret', + LIBRECHAT_CODE_HTTP_PROXY: 'worker-secret', + AWS_SECRET_ACCESS_KEY: 'aws-secret', + GITHUB_TOKEN: 'github-secret', + HTTP_PROXY_TOKEN: 'proxy-secret', + CUSTOM_PROXY: 'proxy-secret', + SYSTEMROOT_TOKEN: 'runtime-secret', + NODE_OPTIONS: '--require /host/private.js', + LD_PRELOAD: '/host/private.so', }; - const fake = fakeManager({ wrappedEnvironment }); const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - environment: { ...proxyEnvironment, GITHUB_TOKEN: 'host-secret' }, - manager: fake.manager, + workspaceRoot: root, platform, allowedDomains: ['github.com'], + environment: { + ...proxyEnvironment, ...windowsEnvironment, ...credentials, + HtTp_PrOxY: 'http://mixed-case.invalid:8080', + PATH: '/usr/bin', LC_ALL: 'C.UTF-8', + }, + manager: fake.manager, }); t.after(() => sandbox.close()); - const result = await sandbox.execute({ - ...request, - maxOutputBytes: 256, - command: - 'printf "%s|%s|%s|%s|%s" "$HTTP_PROXY" "$HTTPS_PROXY" "$ALL_PROXY" "$NO_PROXY" "${GITHUB_TOKEN-unset}"', - }); - assert.equal(result.exitCode, 0); - assert.equal( - result.stdout, - `${Object.values(wrappedEnvironment).join('|')}|unset`, - ); + await sandbox.prepare(); + const denied = new Set(fake.config?.credentials?.envVars + ?.filter(({ mode }) => mode === 'deny').map(({ name }) => name)); + for (const name of [...Object.keys(proxyEnvironment), 'PATH', 'LC_ALL']) { + assert.equal(denied.has(name), false, `${name} must remain available`); + } + for (const name of Object.keys(windowsEnvironment)) { + assert.equal(denied.has(name), platform !== 'win32', `${name} must be platform-specific`); + } + assert.equal(denied.has('HtTp_PrOxY'), platform !== 'win32'); + for (const name of Object.keys(credentials)) { + assert.equal(denied.has(name), true, `${name} must remain denied`); + } + assert.deepEqual(fake.config?.network.allowedDomains, ['github.com']); + assert.equal(fake.config?.network.strictAllowlist, true); + }); +} + +test('uses SRT proxy values without restoring inherited proxies or credentials', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const wrappedEnvironment = { + HTTP_PROXY: 'http://localhost:3128', HTTPS_PROXY: 'http://localhost:3128', + ALL_PROXY: 'http://localhost:3128', NO_PROXY: 'localhost', + }; + const fake = fakeManager({ wrappedEnvironment }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + environment: { ...proxyEnvironment, GITHUB_TOKEN: 'host-secret' }, + manager: fake.manager, + }); + t.after(() => sandbox.close()); + const result = await sandbox.execute({ + ...request, maxOutputBytes: 256, + command: 'printf "%s|%s|%s|%s|%s" "$HTTP_PROXY" "$HTTPS_PROXY" "$ALL_PROXY" "$NO_PROXY" "${GITHUB_TOKEN-unset}"', + }); + assert.equal(result.exitCode, 0); + assert.equal(result.stdout, `${Object.values(wrappedEnvironment).join('|')}|unset`); }); -test('masks a host credential for only its injection host and restores the parent environment', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - const original = process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - t.after(() => { - if (original === undefined) - delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - else process.env.LIBRECHAT_CODE_TEST_CREDENTIAL = original; - }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - allowedDomains: ['github.com'], - maskedEnvironment: { - variables: [ - { - name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', - extract: '^Authorization: Bearer (.+)$', - injectHosts: ['github.com'], - }, - ], - async resolve() { - return { - LIBRECHAT_CODE_TEST_CREDENTIAL: - 'Authorization: Bearer real-secret', - }; - }, +test('masks a host credential for only its injection host and restores the parent environment', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + const original = process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + t.after(() => { + if (original === undefined) + delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + else process.env.LIBRECHAT_CODE_TEST_CREDENTIAL = original; + }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + allowedDomains: ['github.com'], + maskedEnvironment: { + variables: [ + { + name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', + extract: '^Authorization: Bearer (.+)$', + injectHosts: ['github.com'], }, - manager: fake.manager, - }); - - const result = await sandbox.execute({ - ...request, - command: 'printf %s "$LIBRECHAT_CODE_TEST_CREDENTIAL"', - }); - - assert.equal( - fake.credentialSeenDuringWrap, - 'Authorization: Bearer real-secret', - ); - assert.equal(result.stdout, 'Authorization: Bearer srt-sentinel'); - assert.equal(process.env.LIBRECHAT_CODE_TEST_CREDENTIAL, undefined); - assert.deepEqual(fake.config?.network.tlsTerminate, {}); - assert.deepEqual(fake.config?.credentials?.envVars?.at(-1), { - name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', - extract: '^Authorization: Bearer (.+)$', - injectHosts: ['github.com'], - mode: 'mask', - onExtractNoMatch: 'error', - }); + ], + async resolve() { + return { + LIBRECHAT_CODE_TEST_CREDENTIAL: 'Authorization: Bearer real-secret', + }; + }, + }, + manager: fake.manager, + }); + + const result = await sandbox.execute({ + ...request, + command: 'printf %s "$LIBRECHAT_CODE_TEST_CREDENTIAL"', + }); + + assert.equal( + fake.credentialSeenDuringWrap, + 'Authorization: Bearer real-secret', + ); + assert.equal(result.stdout, 'Authorization: Bearer srt-sentinel'); + assert.equal(process.env.LIBRECHAT_CODE_TEST_CREDENTIAL, undefined); + assert.deepEqual(fake.config?.network.tlsTerminate, {}); + assert.deepEqual(fake.config?.credentials?.envVars?.at(-1), { + name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', + extract: '^Authorization: Bearer (.+)$', + injectHosts: ['github.com'], + mode: 'mask', + onExtractNoMatch: 'error', + }); }); -test('serializes credential handoff across concurrent sandbox instances', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const original = process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - t.after(() => { - if (original === undefined) - delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; - else process.env.LIBRECHAT_CODE_TEST_CREDENTIAL = original; - }); - let firstEntered!: () => void; - const firstEnteredPromise = new Promise(resolve => { - firstEntered = resolve; - }); - let releaseFirst!: () => void; - const firstGate = new Promise(resolve => { - releaseFirst = resolve; - }); - let secondEntered = false; - const first = fakeManager({ - async beforeWrap() { - firstEntered(); - await firstGate; - }, - }); - const second = fakeManager({ - async beforeWrap() { - secondEntered = true; +test('serializes credential handoff across concurrent sandbox instances', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const original = process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + t.after(() => { + if (original === undefined) + delete process.env.LIBRECHAT_CODE_TEST_CREDENTIAL; + else process.env.LIBRECHAT_CODE_TEST_CREDENTIAL = original; + }); + let firstEntered!: () => void; + const firstEnteredPromise = new Promise((resolve) => { + firstEntered = resolve; + }); + let releaseFirst!: () => void; + const firstGate = new Promise((resolve) => { + releaseFirst = resolve; + }); + let secondEntered = false; + const first = fakeManager({ + async beforeWrap() { + firstEntered(); + await firstGate; + }, + }); + const second = fakeManager({ + async beforeWrap() { + secondEntered = true; + }, + }); + const sandbox = ( + manager: ReturnType['manager'], + value: string, + ) => + new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + allowedDomains: ['github.com'], + maskedEnvironment: { + variables: [ + { + name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', + injectHosts: ['github.com'], + }, + ], + async resolve() { + return { LIBRECHAT_CODE_TEST_CREDENTIAL: value }; }, + }, + manager, }); - const sandbox = ( - manager: ReturnType['manager'], - value: string, - ) => - new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - allowedDomains: ['github.com'], - maskedEnvironment: { - variables: [ - { - name: 'LIBRECHAT_CODE_TEST_CREDENTIAL', - injectHosts: ['github.com'], - }, - ], - async resolve() { - return { LIBRECHAT_CODE_TEST_CREDENTIAL: value }; - }, - }, - manager, - }); - const firstExecution = sandbox(first.manager, 'first-secret').execute( - request, - ); - await firstEnteredPromise; - const secondExecution = sandbox(second.manager, 'second-secret').execute( - request, - ); - await new Promise(resolve => setImmediate(resolve)); - assert.equal(secondEntered, false); - releaseFirst(); - await firstExecution; - await secondExecution; - - assert.equal(first.credentialSeenDuringWrap, 'first-secret'); - assert.equal(second.credentialSeenDuringWrap, 'second-secret'); - assert.equal(process.env.LIBRECHAT_CODE_TEST_CREDENTIAL, undefined); + const firstExecution = sandbox(first.manager, 'first-secret').execute( + request, + ); + await firstEnteredPromise; + const secondExecution = sandbox(second.manager, 'second-secret').execute( + request, + ); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(secondEntered, false); + releaseFirst(); + await firstExecution; + await secondExecution; + + assert.equal(first.credentialSeenDuringWrap, 'first-secret'); + assert.equal(second.credentialSeenDuringWrap, 'second-secret'); + assert.equal(process.env.LIBRECHAT_CODE_TEST_CREDENTIAL, undefined); }); -test('isolates Git from host-level global and system configuration', 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, - }); +test('isolates Git from host-level global and system configuration', 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, + }); - const result = await sandbox.execute({ - ...request, - command: 'printf "%s|%s" "$GIT_CONFIG_GLOBAL" "$GIT_CONFIG_NOSYSTEM"', - }); + const result = await sandbox.execute({ + ...request, + command: 'printf "%s|%s" "$GIT_CONFIG_GLOBAL" "$GIT_CONFIG_NOSYSTEM"', + }); - assert.equal(result.stdout, '/dev/null|1'); + assert.equal(result.stdout, '/dev/null|1'); }); -test('restores trusted Git LFS filters without reading host Git configuration', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager({ - appendGitSafeDirectory: true, - inheritedGitEnvironment: { - GIT_CONFIG_COUNT: '1', - GIT_CONFIG_KEY_0: 'include.path', - GIT_CONFIG_VALUE_0: '/untrusted/host-config', - }, - }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - environment: { - ...process.env, - GIT_CONFIG_COUNT: '1', - GIT_CONFIG_KEY_0: 'include.path', - GIT_CONFIG_VALUE_0: '/untrusted/host-config', - }, - manager: fake.manager, - }); - - const result = await sandbox.execute({ - ...request, - maxOutputBytes: 256, - command: - 'printf "%s|%s|%s|%s|%s|%s" "$(git config --get filter.lfs.clean)" "$(git config --get filter.lfs.smudge)" "$(git config --get filter.lfs.process)" "$(git config --get filter.lfs.required)" "$(git config --get safe.directory)" "$(git config --get include.path)"', - }); - - assert.equal( - result.stdout, - 'git-lfs clean -- %f|git-lfs smudge -- %f|git-lfs filter-process|true|/workspace|', - ); - assert.equal(fake.gitLfsRequiredSeenDuringWrap, 'true'); - const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); - assert.ok(!denied?.includes('GIT_CONFIG_COUNT')); - assert.ok(!denied?.includes('GIT_CONFIG_KEY_0')); - assert.ok(!denied?.includes('GIT_CONFIG_VALUE_0')); +test('restores trusted Git LFS filters without reading host Git configuration', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager({ + appendGitSafeDirectory: true, + inheritedGitEnvironment: { + GIT_CONFIG_COUNT: '1', + GIT_CONFIG_KEY_0: 'include.path', + GIT_CONFIG_VALUE_0: '/untrusted/host-config', + }, + }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + environment: { + ...process.env, + GIT_CONFIG_COUNT: '1', + GIT_CONFIG_KEY_0: 'include.path', + GIT_CONFIG_VALUE_0: '/untrusted/host-config', + }, + manager: fake.manager, + }); + + const result = await sandbox.execute({ + ...request, + maxOutputBytes: 256, + command: + 'printf "%s|%s|%s|%s|%s|%s" "$(git config --get filter.lfs.clean)" "$(git config --get filter.lfs.smudge)" "$(git config --get filter.lfs.process)" "$(git config --get filter.lfs.required)" "$(git config --get safe.directory)" "$(git config --get include.path)"', + }); + + assert.equal( + result.stdout, + 'git-lfs clean -- %f|git-lfs smudge -- %f|git-lfs filter-process|true|/workspace|', + ); + assert.equal(fake.gitLfsRequiredSeenDuringWrap, 'true'); + const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); + assert.ok(!denied?.includes('GIT_CONFIG_COUNT')); + assert.ok(!denied?.includes('GIT_CONFIG_KEY_0')); + assert.ok(!denied?.includes('GIT_CONFIG_VALUE_0')); }); -test('filters environment names case-insensitively only on Windows', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager(); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - platform: 'win32', - environment: { - PATH: '/usr/bin', - Path: 'C:\\Windows\\System32', - LC_API_TOKEN: 'secret', - librechat_code_worker_token: 'secret', - librechat_code_github_authorization: 'secret', - git_config_count: '1', - }, - maskedEnvironment: { - variables: [ - { - name: 'LIBRECHAT_CODE_GITHUB_AUTHORIZATION', - injectHosts: ['github.com'], - }, - ], - async resolve() { - return {}; - }, +test('filters environment names case-insensitively only on Windows', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager(); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + platform: 'win32', + environment: { + PATH: '/usr/bin', + Path: 'C:\\Windows\\System32', + LC_API_TOKEN: 'secret', + librechat_code_worker_token: 'secret', + librechat_code_github_authorization: 'secret', + git_config_count: '1', + }, + maskedEnvironment: { + variables: [ + { + name: 'LIBRECHAT_CODE_GITHUB_AUTHORIZATION', + injectHosts: ['github.com'], }, - manager: fake.manager, - }); - - await sandbox.prepare(); - const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); - assert.ok(!denied?.includes('PATH')); - assert.ok(!denied?.includes('Path')); - assert.ok(!denied?.includes('LC_API_TOKEN')); - assert.ok(denied?.includes('librechat_code_worker_token')); - assert.ok(!denied?.includes('librechat_code_github_authorization')); - assert.ok(!denied?.includes('git_config_count')); + ], + async resolve() { + return {}; + }, + }, + manager: fake.manager, + }); + + await sandbox.prepare(); + const denied = fake.config?.credentials?.envVars?.map(({ name }) => name); + assert.ok(!denied?.includes('PATH')); + assert.ok(!denied?.includes('Path')); + assert.ok(!denied?.includes('LC_API_TOKEN')); + assert.ok(denied?.includes('librechat_code_worker_token')); + assert.ok(!denied?.includes('librechat_code_github_authorization')); + assert.ok(!denied?.includes('git_config_count')); }); -test('fails closed when the configured POSIX shell is unavailable', 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, - platform: 'linux', - shellPath: join(root, 'missing-bash'), - manager: fakeManager().manager, - }); - - await assert.rejects( - sandbox.prepare(), - (error: unknown) => - error instanceof WorkspaceToolError && - error.code === 'COMMAND_UNAVAILABLE' && - /shell is unavailable/i.test(error.message), - ); +test('fails closed when the configured POSIX shell is unavailable', 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, + platform: 'linux', + shellPath: join(root, 'missing-bash'), + manager: fakeManager().manager, + }); + + await assert.rejects( + sandbox.prepare(), + (error: unknown) => + error instanceof WorkspaceToolError && + error.code === 'COMMAND_UNAVAILABLE' && + /shell is unavailable/i.test(error.message), + ); }); -test('fails closed when SRT dependencies are unavailable', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const fake = fakeManager({ dependencyErrors: ['bubblewrap missing'] }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - }); - - await assert.rejects( - sandbox.prepare(), - (error: unknown) => - error instanceof WorkspaceToolError && - error.code === 'COMMAND_UNAVAILABLE' && - /bubblewrap missing/.test(error.message), - ); +test('fails closed when SRT dependencies are unavailable', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const fake = fakeManager({ dependencyErrors: ['bubblewrap missing'] }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + }); + + await assert.rejects( + sandbox.prepare(), + (error: unknown) => + error instanceof WorkspaceToolError && + error.code === 'COMMAND_UNAVAILABLE' && + /bubblewrap missing/.test(error.message), + ); }); -test('refuses workspace roots that expose worker home or control files', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - const controlDirectory = join(root, '.control'); - await mkdir(controlDirectory); - const controlFile = join(controlDirectory, 'identity.json'); - await writeFile(controlFile, '{}'); - t.after(() => rm(root, { recursive: true, force: true })); - - await assert.rejects( - new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: homedir(), - manager: fakeManager().manager, - }).prepare(), - /cannot contain the worker home directory/i, - ); - await assert.rejects( - new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - protectedPaths: [controlFile], - manager: fakeManager().manager, - }).prepare(), - /cannot contain worker control files/i, - ); +test('refuses workspace roots that expose worker home or control files', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + const controlDirectory = join(root, '.control'); + await mkdir(controlDirectory); + const controlFile = join(controlDirectory, 'identity.json'); + await writeFile(controlFile, '{}'); + t.after(() => rm(root, { recursive: true, force: true })); + + await assert.rejects( + new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: homedir(), + manager: fakeManager().manager, + }).prepare(), + /cannot contain the worker home directory/i, + ); + await assert.rejects( + new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + protectedPaths: [controlFile], + manager: fakeManager().manager, + }).prepare(), + /cannot contain worker control files/i, + ); }); -test('executes in the canonical workspace and bounds aggregate output', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - await mkdir(join(root, 'src')); - t.after(() => rm(root, { recursive: true, force: true })); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fakeManager().manager, - }); - - assert.equal(sandbox.mutationFailuresAreAtomic, true); - assert.deepEqual( - await sandbox.execute({ - ...request, - command: "printf '1234567890'; printf 'abcdefghij' >&2", - cwd: 'src', - maxOutputBytes: 12, - }), - { - protocolVersion: 1, - operation: 'execute_command', - workspaceId: 'primary', - exitCode: 0, - stdout: '1234567890', - stderr: 'ab', - truncated: true, - timedOut: false, - }, - ); +test('executes in the canonical workspace and bounds aggregate output', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + await mkdir(join(root, 'src')); + t.after(() => rm(root, { recursive: true, force: true })); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fakeManager().manager, + }); + + assert.equal(sandbox.mutationFailuresAreAtomic, true); + assert.deepEqual( + await sandbox.execute({ + ...request, + command: "printf '1234567890'; printf 'abcdefghij' >&2", + cwd: 'src', + maxOutputBytes: 12, + }), + { + protocolVersion: 1, + operation: 'execute_command', + workspaceId: 'primary', + exitCode: 0, + stdout: '1234567890', + stderr: 'ab', + truncated: true, + timedOut: false, + }, + ); }); -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 })); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fakeManager().manager, - }); - - await assert.rejects( - sandbox.execute({ ...request, cwd: '..' }), - (error: unknown) => - error instanceof WorkspaceToolError && - error.code === 'INVALID_REQUEST', - ); +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 })); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fakeManager().manager, + }); + + await assert.rejects( + sandbox.execute({ ...request, cwd: '..' }), + (error: unknown) => + error instanceof WorkspaceToolError && error.code === 'INVALID_REQUEST', + ); }); -test('terminates detached command descendants before returning', 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, - }); - - const result = await sandbox.execute({ - ...request, - command: '(sleep 0.2; printf late > late.txt) >/dev/null 2>&1 &', - }); - assert.equal(result.exitCode, 0); - await new Promise(resolve => setTimeout(resolve, 350)); - await assert.rejects(access(join(root, 'late.txt'))); +test('terminates detached command descendants before returning', 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, + }); + + const result = await sandbox.execute({ + ...request, + command: '(sleep 0.2; printf late > late.txt) >/dev/null 2>&1 &', + }); + assert.equal(result.exitCode, 0); + await new Promise((resolve) => setTimeout(resolve, 350)); + await assert.rejects(access(join(root, 'late.txt'))); }); -test('reports cancellation after command start as a potentially committed mutation', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - let commandStarted!: () => void; - const commandStartedPromise = new Promise(resolve => { - commandStarted = resolve; - }); - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fakeManager().manager, - spawnCommand(command, args, options) { - const child = spawn(command, [...args], options); - commandStarted(); - return child; - }, - }); - const controller = new AbortController(); - const execution = sandbox.execute( - { ...request, command: 'sleep 30' }, - controller.signal, - ); - await commandStartedPromise; - controller.abort(); - - await assert.rejects( - execution, - (error: unknown) => - error instanceof WorkspaceToolError && - error.code === 'EXECUTION_ABORTED' && - error.mutationMayHaveCommitted === true, - ); +test('reports cancellation after command start as a potentially committed mutation', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + let commandStarted!: () => void; + const commandStartedPromise = new Promise((resolve) => { + commandStarted = resolve; + }); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fakeManager().manager, + spawnCommand(command, args, options) { + const child = spawn(command, [...args], options); + commandStarted(); + return child; + }, + }); + const controller = new AbortController(); + const execution = sandbox.execute( + { ...request, command: 'sleep 30' }, + controller.signal, + ); + await commandStartedPromise; + controller.abort(); + + await assert.rejects( + execution, + (error: unknown) => + error instanceof WorkspaceToolError && + error.code === 'EXECUTION_ABORTED' && + error.mutationMayHaveCommitted === true, + ); }); -test('closes stdin immediately when the command protocol provides no input', 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, - }); - - const result = await sandbox.execute({ - ...request, - command: 'cat', - timeoutMs: 250, - }); - assert.equal(result.exitCode, 0); - assert.equal(result.timedOut, false); +test('closes stdin immediately when the command protocol provides no input', 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, + }); + + const result = await sandbox.execute({ + ...request, + command: 'cat', + timeoutMs: 250, + }); + assert.equal(result.exitCode, 0); + assert.equal(result.timedOut, false); }); -test('maps platform-native exit statuses into the bridge protocol range', async t => { - const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); - t.after(() => rm(root, { recursive: true, force: true })); - const spawnCommand = () => { - const child = new EventEmitter() as ChildProcessWithoutNullStreams; - Object.assign(child, { - stdin: new PassThrough(), - stdout: new PassThrough(), - stderr: new PassThrough(), - pid: undefined, - kill: () => true, - }); - queueMicrotask(() => child.emit('close', 300, null)); - return child; - }; - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fakeManager().manager, - spawnCommand, +test('maps platform-native exit statuses into the bridge protocol range', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const spawnCommand = () => { + const child = new EventEmitter() as ChildProcessWithoutNullStreams; + Object.assign(child, { + stdin: new PassThrough(), + stdout: new PassThrough(), + stderr: new PassThrough(), + pid: undefined, + kill: () => true, }); - - const result = await sandbox.execute(request); - assert.equal(result.exitCode, 1); + queueMicrotask(() => child.emit('close', 300, null)); + return child; + }; + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fakeManager().manager, + spawnCommand, + }); + + const result = await sandbox.execute(request); + assert.equal(result.exitCode, 1); }); -test('cleans allocated command state exactly once on every execution exit', async t => { - for (const outcome of [ - 'abort-before-spawn', - 'spawn-throw', - 'close', - 'error', - 'abort-after-spawn', - 'timeout', - 'wrap-throw', - ] as const) { - for (const cleanupThrows of [false, true]) { - await t.test( - `${outcome}, cleanup throws: ${cleanupThrows}`, - async t => { - const root = await mkdtemp( - join(tmpdir(), 'librechat-code-native-'), - ); - t.after(() => rm(root, { recursive: true, force: true })); - const controller = new AbortController(); - let cleanupCalls = 0; - let spawnCalls = 0; - let allocated = false; - const fake = fakeManager({ - async beforeWrap() { - if (outcome === 'wrap-throw') - throw new Error('wrap failed'); - allocated = true; - if (outcome === 'abort-before-spawn') - controller.abort(); - }, - }); - fake.manager.cleanupAfterCommand = () => { - cleanupCalls += 1; - assert.equal(allocated, true); - allocated = false; - if (cleanupThrows) throw new Error('cleanup failed'); - }; - const sandbox = new NativeSrtWorkspaceCommandSandbox({ - workspaceRoot: root, - manager: fake.manager, - spawnCommand() { - spawnCalls += 1; - assert.equal(allocated, true); - if (outcome === 'spawn-throw') - throw new Error('spawn failed'); - const child = - new EventEmitter() as ChildProcessWithoutNullStreams; - let closeQueued = false; - const close = () => { - if (!closeQueued) { - closeQueued = true; - queueMicrotask(() => - child.emit('close', null, 'SIGKILL'), - ); - } - return true; - }; - Object.assign(child, { - stdin: new PassThrough(), - stdout: new PassThrough(), - stderr: new PassThrough(), - pid: undefined, - kill: close, - }); - queueMicrotask(() => { - assert.equal(cleanupCalls, 0); - if (outcome === 'error') { - child.emit( - 'error', - new Error('spawn failed'), - ); - } else if (outcome === 'abort-after-spawn') { - controller.abort(); - } else if (outcome === 'close') { - child.emit('close', 0, null); - } - }); - return child; - }, - }); - const execution = sandbox.execute( - { ...request, timeoutMs: 10 }, - controller.signal, - ); - if (outcome === 'close' || outcome === 'timeout') { - const result = await execution; - assert.equal( - result.exitCode, - outcome === 'close' ? 0 : null, - ); - assert.equal(result.timedOut, outcome === 'timeout'); - } else { - await assert.rejects( - execution, - (error: unknown) => - error instanceof WorkspaceToolError && - error.code === - (outcome.startsWith('abort') - ? 'EXECUTION_ABORTED' - : 'COMMAND_UNAVAILABLE') && - error.mutationMayHaveCommitted === - (outcome === 'abort-after-spawn'), - ); - } - assert.equal( - spawnCalls, - outcome === 'abort-before-spawn' || - outcome === 'wrap-throw' - ? 0 - : 1, - ); - assert.equal( - cleanupCalls, - outcome === 'wrap-throw' ? 0 : 1, - ); - assert.equal(allocated, false); - await sandbox.close(); - assert.equal(fake.reset, true); - }, - ); +test('cleans allocated command state exactly once on every execution exit', async (t) => { + for (const outcome of [ + 'abort-before-spawn', + 'spawn-throw', + 'close', + 'error', + 'abort-after-spawn', + 'timeout', + 'wrap-throw', + ] as const) { + for (const cleanupThrows of [false, true]) { + await t.test(`${outcome}, cleanup throws: ${cleanupThrows}`, async (t) => { + const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + t.after(() => rm(root, { recursive: true, force: true })); + const controller = new AbortController(); + let cleanupCalls = 0; + let spawnCalls = 0; + let allocated = false; + const fake = fakeManager({ + async beforeWrap() { + if (outcome === 'wrap-throw') throw new Error('wrap failed'); + allocated = true; + if (outcome === 'abort-before-spawn') controller.abort(); + }, + }); + fake.manager.cleanupAfterCommand = () => { + cleanupCalls += 1; + assert.equal(allocated, true); + allocated = false; + if (cleanupThrows) throw new Error('cleanup failed'); + }; + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: root, + manager: fake.manager, + spawnCommand() { + spawnCalls += 1; + assert.equal(allocated, true); + if (outcome === 'spawn-throw') throw new Error('spawn failed'); + const child = new EventEmitter() as ChildProcessWithoutNullStreams; + let closeQueued = false; + const close = () => { + if (!closeQueued) { + closeQueued = true; + queueMicrotask(() => child.emit('close', null, 'SIGKILL')); + } + return true; + }; + Object.assign(child, { + stdin: new PassThrough(), + stdout: new PassThrough(), + stderr: new PassThrough(), + pid: undefined, + kill: close, + }); + queueMicrotask(() => { + assert.equal(cleanupCalls, 0); + if (outcome === 'error') { + child.emit('error', new Error('spawn failed')); + } else if (outcome === 'abort-after-spawn') { + controller.abort(); + } else if (outcome === 'close') { + child.emit('close', 0, null); + } + }); + return child; + }, + }); + const execution = sandbox.execute( + { ...request, timeoutMs: 10 }, + controller.signal, + ); + if (outcome === 'close' || outcome === 'timeout') { + const result = await execution; + assert.equal(result.exitCode, outcome === 'close' ? 0 : null); + assert.equal(result.timedOut, outcome === 'timeout'); + } else { + await assert.rejects(execution, (error: unknown) => + error instanceof WorkspaceToolError && + error.code === (outcome.startsWith('abort') + ? 'EXECUTION_ABORTED' + : 'COMMAND_UNAVAILABLE') && + error.mutationMayHaveCommitted === (outcome === 'abort-after-spawn'), + ); } + assert.equal( + spawnCalls, + outcome === 'abort-before-spawn' || outcome === 'wrap-throw' ? 0 : 1, + ); + assert.equal(cleanupCalls, outcome === 'wrap-throw' ? 0 : 1); + assert.equal(allocated, false); + await sandbox.close(); + assert.equal(fake.reset, true); + }); } + } }); diff --git a/packages/code/src/native-sandbox.ts b/packages/code/src/native-sandbox.ts index 41c4a3f..1adfd35 100644 --- a/packages/code/src/native-sandbox.ts +++ b/packages/code/src/native-sandbox.ts @@ -2,105 +2,112 @@ import { spawn } from 'node:child_process'; import { randomUUID } from 'node:crypto'; import { homedir, tmpdir } from 'node:os'; import { - basename, - dirname, - isAbsolute, - join, - relative, - resolve, - sep, + basename, + dirname, + isAbsolute, + join, + relative, + resolve, + sep, } from 'node:path'; import { constants as fsConstants } from 'node:fs'; -import { access, mkdtemp, open, realpath, rm, stat } from 'node:fs/promises'; +import { + access, + mkdtemp, + open, + realpath, + rm, + stat, +} from 'node:fs/promises'; import type { FileHandle } from 'node:fs/promises'; import { SandboxManager } from '@anthropic-ai/sandbox-runtime'; import { - BRIDGE_PROTOCOL_VERSION, - BRIDGE_WORKSPACE_COMMAND_DEFAULT_OUTPUT_BYTES, - BRIDGE_WORKSPACE_COMMAND_DEFAULT_TIMEOUT_MS, - isWorkspaceToolRequest, + BRIDGE_PROTOCOL_VERSION, + BRIDGE_WORKSPACE_COMMAND_DEFAULT_OUTPUT_BYTES, + BRIDGE_WORKSPACE_COMMAND_DEFAULT_TIMEOUT_MS, + isWorkspaceToolRequest, } from './protocol.js'; import { - assertPrivateStorageAcl, - assertPrivateStorageAncestors, - removePrivateStorageAcl, + assertPrivateStorageAcl, + assertPrivateStorageAncestors, + removePrivateStorageAcl, } from './private-storage.js'; import { WorkspaceToolError } from './workspace.js'; import { restoreScratchTraversal } from './native-scratch.js'; import type { - ChildProcessWithoutNullStreams, - SpawnOptionsWithoutStdio, + ChildProcessWithoutNullStreams, + SpawnOptionsWithoutStdio, } from 'node:child_process'; import type { SandboxRuntimeConfig } from '@anthropic-ai/sandbox-runtime'; import type { - WorkspaceExecuteCommandRequest, - WorkspaceExecuteCommandResult, + WorkspaceExecuteCommandRequest, + WorkspaceExecuteCommandResult, } from './protocol.js'; import type { WorkspaceCommandSandbox } from './workspace.js'; const SAFE_CHILD_ENV_NAMES = new Set([ - 'COLORTERM', - 'HOME', - 'LANG', - 'LC_ALL', - 'LOGNAME', - 'NO_COLOR', - 'PATH', - 'SHELL', - 'TERM', - 'TMPDIR', - 'USER', + 'COLORTERM', + 'HOME', + 'LANG', + 'LC_ALL', + 'LOGNAME', + 'NO_COLOR', + 'PATH', + 'SHELL', + 'TERM', + 'TMPDIR', + 'USER', ]); // Preserve the conventional proxy names, not arbitrary *_PROXY variables. // SRT owns their final values and may replace them with its filtered proxy. const PROXY_CHILD_ENV_NAMES = new Set([ - 'HTTP_PROXY', - 'HTTPS_PROXY', - 'ALL_PROXY', - 'NO_PROXY', - 'http_proxy', - 'https_proxy', - 'all_proxy', - 'no_proxy', + 'HTTP_PROXY', + 'HTTPS_PROXY', + 'ALL_PROXY', + 'NO_PROXY', + 'http_proxy', + 'https_proxy', + 'all_proxy', + 'no_proxy', ]); // Windows resolves these names case-insensitively. Keep the exception // platform-specific so similarly named POSIX variables remain denied. const WINDOWS_CHILD_ENV_NAMES = new Set([ - 'SYSTEMROOT', - 'SYSTEMDRIVE', - 'WINDIR', - 'COMSPEC', - 'PATHEXT', - 'TEMP', - 'TMP', - 'USERPROFILE', - 'HOMEDRIVE', - 'HOMEPATH', - 'APPDATA', - 'LOCALAPPDATA', + 'SYSTEMROOT', + 'SYSTEMDRIVE', + 'WINDIR', + 'COMSPEC', + 'PATHEXT', + 'TEMP', + 'TMP', + 'USERPROFILE', + 'HOMEDRIVE', + 'HOMEPATH', + 'APPDATA', + 'LOCALAPPDATA', ]); let hostEnvironmentMutationQueue: Promise = Promise.resolve(); const TRUSTED_GIT_ENVIRONMENT = { - GIT_CONFIG_COUNT: '4', - GIT_CONFIG_KEY_0: 'filter.lfs.clean', - GIT_CONFIG_VALUE_0: 'git-lfs clean -- %f', - GIT_CONFIG_KEY_1: 'filter.lfs.smudge', - GIT_CONFIG_VALUE_1: 'git-lfs smudge -- %f', - GIT_CONFIG_KEY_2: 'filter.lfs.process', - GIT_CONFIG_VALUE_2: 'git-lfs filter-process', - GIT_CONFIG_KEY_3: 'filter.lfs.required', - GIT_CONFIG_VALUE_3: 'true', + GIT_CONFIG_COUNT: '4', + GIT_CONFIG_KEY_0: 'filter.lfs.clean', + GIT_CONFIG_VALUE_0: 'git-lfs clean -- %f', + GIT_CONFIG_KEY_1: 'filter.lfs.smudge', + GIT_CONFIG_VALUE_1: 'git-lfs smudge -- %f', + GIT_CONFIG_KEY_2: 'filter.lfs.process', + GIT_CONFIG_VALUE_2: 'git-lfs filter-process', + GIT_CONFIG_KEY_3: 'filter.lfs.required', + GIT_CONFIG_VALUE_3: 'true', } as const; const { - GIT_CONFIG_COUNT: TRUSTED_GIT_CONFIG_COUNT, - ...TRUSTED_GIT_CONFIG_ENTRIES + GIT_CONFIG_COUNT: TRUSTED_GIT_CONFIG_COUNT, + ...TRUSTED_GIT_CONFIG_ENTRIES } = TRUSTED_GIT_ENVIRONMENT; const NATIVE_SANDBOX_SCRATCH_PREFIX = 'librechat-code-srt-'; @@ -108,803 +115,745 @@ const NATIVE_SANDBOX_SCRATCH_PREFIX = 'librechat-code-srt-'; // TMPDIR must also deny them or separate worker processes can exchange files. const SRT_SHARED_SCRATCH_PATHS = ['/tmp/claude', '/private/tmp/claude']; const SRT_SCRATCH_SELECTOR_NAMES = [ - 'CLAUDE_CODE_TMPDIR', - 'CLAUDE_TMPDIR', + 'CLAUDE_CODE_TMPDIR', + 'CLAUDE_TMPDIR', ] as const; // Capture this before any command wrapper can temporarily mutate process.env. const HOST_TEMPORARY_ROOT = tmpdir(); interface NativeSandboxManager { - isSupportedPlatform(): boolean; - checkDependenciesAsync(): Promise<{ warnings: string[]; errors: string[] }>; - initialize(config: SandboxRuntimeConfig): Promise; - wrapWithSandboxArgv( - command: string, - binShell?: string, - customConfig?: Partial, - abortSignal?: AbortSignal, - cwd?: string, - options?: { commandId?: string; commandText?: string }, - ): Promise<{ argv: string[]; env: NodeJS.ProcessEnv }>; - annotateStderrWithSandboxFailures( - commandId: string, - stderr: string, - ): string; - cleanupAfterCommand(): void; - reset(): Promise; + isSupportedPlatform(): boolean; + checkDependenciesAsync(): Promise<{ warnings: string[]; errors: string[] }>; + initialize(config: SandboxRuntimeConfig): Promise; + wrapWithSandboxArgv( + command: string, + binShell?: string, + customConfig?: Partial, + abortSignal?: AbortSignal, + cwd?: string, + options?: { commandId?: string; commandText?: string }, + ): Promise<{ argv: string[]; env: NodeJS.ProcessEnv }>; + annotateStderrWithSandboxFailures(commandId: string, stderr: string): string; + cleanupAfterCommand(): void; + reset(): Promise; } // SRT's default manager is process-global, including its policy and cleanup // state. Distinct workspace objects must not reconfigure the same manager. const managerOwners = new WeakMap< - NativeSandboxManager, - NativeSrtWorkspaceCommandSandbox + NativeSandboxManager, + NativeSrtWorkspaceCommandSandbox >(); type SpawnCommand = ( - command: string, - args: readonly string[], - options: SpawnOptionsWithoutStdio, + command: string, + args: readonly string[], + options: SpawnOptionsWithoutStdio, ) => ChildProcessWithoutNullStreams; export interface NativeSrtWorkspaceCommandSandboxOptions { - workspaceRoot: string; - /** Trusted worker files that must never become workspace-readable or writable. */ - protectedPaths?: string[]; - allowedDomains?: string[]; - environment?: NodeJS.ProcessEnv; - manager?: NativeSandboxManager; - spawnCommand?: SpawnCommand; - homeDirectory?: string; - platform?: NodeJS.Platform; - /** Trusted shell path used by SRT on POSIX hosts. */ - shellPath?: string; - /** Host-owned credentials exposed only as SRT sentinels inside the sandbox. */ - maskedEnvironment?: { - variables: Array<{ - name: string; - injectHosts: string[]; - extract?: string; - }>; - resolve(signal?: AbortSignal): Promise>; - wrapCommand?(command: string, platform: NodeJS.Platform): string; - }; + workspaceRoot: string; + /** Trusted worker files that must never become workspace-readable or writable. */ + protectedPaths?: string[]; + allowedDomains?: string[]; + environment?: NodeJS.ProcessEnv; + manager?: NativeSandboxManager; + spawnCommand?: SpawnCommand; + homeDirectory?: string; + platform?: NodeJS.Platform; + /** Trusted shell path used by SRT on POSIX hosts. */ + shellPath?: string; + /** Host-owned credentials exposed only as SRT sentinels inside the sandbox. */ + maskedEnvironment?: { + variables: Array<{ + name: string; + injectHosts: string[]; + extract?: string; + }>; + resolve(signal?: AbortSignal): Promise>; + wrapCommand?(command: string, platform: NodeJS.Platform): string; + }; } function isWithin(root: string, candidate: string): boolean { - const path = relative(root, candidate); - return ( - path === '' || - (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path)) - ); + const path = relative(root, candidate); + return ( + path === '' || + (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path)) + ); } async function canonicalPath(path: string): Promise { - const absolute = resolve(path); - let cursor = absolute; - const missingSegments: string[] = []; - for (;;) { - try { - return join(await realpath(cursor), ...missingSegments); - } catch { - const parent = dirname(cursor); - if (parent === cursor) - throw new Error(`Cannot canonicalize protected path: ${path}`); - missingSegments.unshift(basename(cursor)); - cursor = parent; - } + const absolute = resolve(path); + let cursor = absolute; + const missingSegments: string[] = []; + for (;;) { + try { + return join(await realpath(cursor), ...missingSegments); + } catch { + const parent = dirname(cursor); + if (parent === cursor) + throw new Error(`Cannot canonicalize protected path: ${path}`); + missingSegments.unshift(basename(cursor)); + cursor = parent; } + } } function boundedUtf8(buffer: Buffer, budget: number): string { - let end = Math.min(buffer.byteLength, budget); - while (end > 0) { - const value = buffer.subarray(0, end).toString('utf8'); - if (Buffer.byteLength(value) <= budget) return value; - end -= 1; - } - return ''; + let end = Math.min(buffer.byteLength, budget); + while (end > 0) { + const value = buffer.subarray(0, end).toString('utf8'); + if (Buffer.byteLength(value) <= budget) return value; + end -= 1; + } + return ''; } function deniedEnvironmentNames( - environment: NodeJS.ProcessEnv, - platform: NodeJS.Platform, + environment: NodeJS.ProcessEnv, + platform: NodeJS.Platform, ): string[] { - return Object.keys(environment) - .filter(name => { - const normalized = platform === 'win32' ? name.toUpperCase() : name; - return ( - normalized.startsWith('LIBRECHAT_CODE_') || - (!SAFE_CHILD_ENV_NAMES.has(normalized) && - !PROXY_CHILD_ENV_NAMES.has(normalized) && - !( - platform === 'win32' && - WINDOWS_CHILD_ENV_NAMES.has(normalized) - ) && - !normalized.startsWith('LC_')) - ); - }) - .sort(); + return Object.keys(environment) + .filter((name) => { + const normalized = platform === 'win32' ? name.toUpperCase() : name; + return ( + normalized.startsWith('LIBRECHAT_CODE_') || + (!SAFE_CHILD_ENV_NAMES.has(normalized) && + !PROXY_CHILD_ENV_NAMES.has(normalized) && + !(platform === 'win32' && WINDOWS_CHILD_ENV_NAMES.has(normalized)) && + !normalized.startsWith('LC_')) + ); + }) + .sort(); } function normalizedEnvironmentName( - name: string, - platform: NodeJS.Platform, + name: string, + platform: NodeJS.Platform, ): string { - return platform === 'win32' ? name.toUpperCase() : name; + return platform === 'win32' ? name.toUpperCase() : name; } export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox { - readonly mutationFailuresAreAtomic = true as const; - private readonly manager: NativeSandboxManager; - private readonly spawnCommand: SpawnCommand; - private readonly environment: NodeJS.ProcessEnv; - private readonly platform: NodeJS.Platform; - private initialized?: Promise; - private canonicalRoot?: string; - private scratchDirectory?: string; - private scratchHandle?: FileHandle; - private execution?: Promise; - private closing?: Promise; - private resetFailed = false; - - constructor( - private readonly options: NativeSrtWorkspaceCommandSandboxOptions, - ) { - this.manager = options.manager ?? SandboxManager; - this.spawnCommand = options.spawnCommand ?? spawn; - this.environment = { ...(options.environment ?? process.env) }; - this.platform = options.platform ?? process.platform; + readonly mutationFailuresAreAtomic = true as const; + private readonly manager: NativeSandboxManager; + private readonly spawnCommand: SpawnCommand; + private readonly environment: NodeJS.ProcessEnv; + private readonly platform: NodeJS.Platform; + private initialized?: Promise; + private canonicalRoot?: string; + private scratchDirectory?: string; + private scratchHandle?: FileHandle; + private execution?: Promise; + private closing?: Promise; + private resetFailed = false; + + constructor( + private readonly options: NativeSrtWorkspaceCommandSandboxOptions, + ) { + this.manager = options.manager ?? SandboxManager; + this.spawnCommand = options.spawnCommand ?? spawn; + this.environment = { ...(options.environment ?? process.env) }; + this.platform = options.platform ?? process.platform; + } + + /** Fail closed before the worker advertises command execution. */ + async prepare(): Promise { + await this.initialize(); + } + + private async initialize(): Promise { + if (this.closing || this.resetFailed) { + throw new WorkspaceToolError( + 'Native sandbox is closing or requires cleanup', + 'COMMAND_UNAVAILABLE', + ); } - - /** Fail closed before the worker advertises command execution. */ - async prepare(): Promise { - await this.initialize(); + if (this.initialized) return this.initialized; + const owner = managerOwners.get(this.manager); + if (owner && owner !== this) { + throw new WorkspaceToolError( + 'Native sandbox manager already belongs to another workspace; use a separate worker process', + 'COMMAND_UNAVAILABLE', + ); } - - private async initialize(): Promise { - if (this.closing || this.resetFailed) { - throw new WorkspaceToolError( - 'Native sandbox is closing or requires cleanup', - 'COMMAND_UNAVAILABLE', - ); - } - if (this.initialized) return this.initialized; - const owner = managerOwners.get(this.manager); - if (owner && owner !== this) { - throw new WorkspaceToolError( - 'Native sandbox manager already belongs to another workspace; use a separate worker process', - 'COMMAND_UNAVAILABLE', - ); - } - managerOwners.set(this.manager, this); - this.initialized = this.initializeOnce().catch(async error => { - await this.manager.reset().catch(() => { - this.resetFailed = true; - }); - await this.removeScratchDirectory().catch(() => undefined); - if (!this.resetFailed) managerOwners.delete(this.manager); - this.initialized = undefined; - throw error; - }); - return this.initialized; + managerOwners.set(this.manager, this); + this.initialized = this.initializeOnce().catch(async (error) => { + await this.manager.reset().catch(() => { + this.resetFailed = true; + }); + await this.removeScratchDirectory().catch(() => undefined); + if (!this.resetFailed) managerOwners.delete(this.manager); + this.initialized = undefined; + throw error; + }); + return this.initialized; + } + + private async initializeOnce(): Promise { + if (!this.manager.isSupportedPlatform()) { + throw new WorkspaceToolError( + 'Native sandbox is unsupported on this platform', + 'COMMAND_UNAVAILABLE', + ); } - - private async initializeOnce(): Promise { - if (!this.manager.isSupportedPlatform()) { - throw new WorkspaceToolError( - 'Native sandbox is unsupported on this platform', - 'COMMAND_UNAVAILABLE', - ); - } - const root = await realpath(this.options.workspaceRoot); - if (!(await stat(root)).isDirectory()) { - throw new WorkspaceToolError( - 'Native sandbox workspace is unavailable', - 'COMMAND_UNAVAILABLE', - ); - } - const home = await canonicalPath( - this.options.homeDirectory ?? homedir(), - ); - if (isWithin(root, home)) { - throw new WorkspaceToolError( - 'Native sandbox workspace cannot contain the worker home directory', - 'REGISTRATION_INVALID', - ); - } - const protectedPaths = await Promise.all( - (this.options.protectedPaths ?? []).map(canonicalPath), - ); - if (protectedPaths.some(path => isWithin(root, path))) { - throw new WorkspaceToolError( - 'Native sandbox workspace cannot contain worker control files', - 'REGISTRATION_INVALID', - ); - } - const sharedScratchPaths = await Promise.all( - (this.platform === 'win32' ? [] : SRT_SHARED_SCRATCH_PATHS).map( - canonicalPath, - ), + const root = await realpath(this.options.workspaceRoot); + if (!(await stat(root)).isDirectory()) { + throw new WorkspaceToolError( + 'Native sandbox workspace is unavailable', + 'COMMAND_UNAVAILABLE', + ); + } + const home = await canonicalPath(this.options.homeDirectory ?? homedir()); + if (isWithin(root, home)) { + throw new WorkspaceToolError( + 'Native sandbox workspace cannot contain the worker home directory', + 'REGISTRATION_INVALID', + ); + } + const protectedPaths = await Promise.all( + (this.options.protectedPaths ?? []).map(canonicalPath), + ); + if (protectedPaths.some((path) => isWithin(root, path))) { + throw new WorkspaceToolError( + 'Native sandbox workspace cannot contain worker control files', + 'REGISTRATION_INVALID', + ); + } + const sharedScratchPaths = await Promise.all( + (this.platform === 'win32' ? [] : SRT_SHARED_SCRATCH_PATHS).map( + canonicalPath, + ), + ); + const inheritedWritablePaths = [ + ...sharedScratchPaths, + ...(await Promise.all( + [join(home, '.npm', '_logs'), join(home, '.claude', 'debug')].map( + canonicalPath, + ), + )), + ]; + const deniedInheritedWritablePaths = [...new Set(inheritedWritablePaths)]; + if (deniedInheritedWritablePaths.some((path) => isWithin(path, root))) { + throw new WorkspaceToolError( + 'Native sandbox workspace cannot be inside an inherited writable path', + 'REGISTRATION_INVALID', + ); + } + const dependencies = await this.manager.checkDependenciesAsync(); + if (dependencies.errors.length > 0) { + throw new WorkspaceToolError( + `Native sandbox dependencies are unavailable: ${dependencies.errors.join('; ')}`, + 'COMMAND_UNAVAILABLE', + ); + } + if (this.platform !== 'win32') { + try { + await access(this.options.shellPath ?? '/bin/bash', fsConstants.X_OK); + } catch { + throw new WorkspaceToolError( + `Native sandbox shell is unavailable: ${this.options.shellPath ?? '/bin/bash'}`, + 'COMMAND_UNAVAILABLE', ); - const inheritedWritablePaths = [ - ...sharedScratchPaths, - ...(await Promise.all( - [ - join(home, '.npm', '_logs'), - join(home, '.claude', 'debug'), - ].map(canonicalPath), - )), - ]; - const deniedInheritedWritablePaths = [ - ...new Set(inheritedWritablePaths), - ]; - if (deniedInheritedWritablePaths.some(path => isWithin(path, root))) { - throw new WorkspaceToolError( - 'Native sandbox workspace cannot be inside an inherited writable path', - 'REGISTRATION_INVALID', - ); - } - const dependencies = await this.manager.checkDependenciesAsync(); - if (dependencies.errors.length > 0) { - throw new WorkspaceToolError( - `Native sandbox dependencies are unavailable: ${dependencies.errors.join('; ')}`, - 'COMMAND_UNAVAILABLE', - ); - } - if (this.platform !== 'win32') { - try { - await access( - this.options.shellPath ?? '/bin/bash', - fsConstants.X_OK, - ); - } catch { - throw new WorkspaceToolError( - `Native sandbox shell is unavailable: ${this.options.shellPath ?? '/bin/bash'}`, - 'COMMAND_UNAVAILABLE', - ); - } - } - const canonicalScratchDirectory = - await this.createScratchDirectory(sharedScratchPaths); - if ( - canonicalScratchDirectory && - isWithin(root, canonicalScratchDirectory) - ) { - throw new WorkspaceToolError( - 'Native sandbox workspace cannot contain worker scratch storage', - 'REGISTRATION_INVALID', - ); - } - const config: SandboxRuntimeConfig = { - network: { - allowedDomains: [...(this.options.allowedDomains ?? [])], - deniedDomains: [], - strictAllowlist: true, - allowAllUnixSockets: false, - allowLocalBinding: false, - ...(this.options.maskedEnvironment ? { tlsTerminate: {} } : {}), - }, - filesystem: { - denyRead: [ - home, - ...sharedScratchPaths.filter(path => - deniedInheritedWritablePaths.includes(path), - ), - ], - allowRead: [ - root, - ...(canonicalScratchDirectory - ? [canonicalScratchDirectory] - : []), - ], - allowWrite: [ - root, - ...(canonicalScratchDirectory - ? [canonicalScratchDirectory] - : []), - ], - denyWrite: [...protectedPaths, ...deniedInheritedWritablePaths], - allowGitConfig: false, - }, - credentials: { - files: protectedPaths.map(path => ({ - path, - mode: 'deny' as const, - })), - envVars: [ - ...deniedEnvironmentNames( - { - ...this.environment, - CLAUDE_CODE_TMPDIR: '', - CLAUDE_TMPDIR: '', - }, - this.platform, - ) - .filter(name => { - const normalized = normalizedEnvironmentName( - name, - this.platform, - ); - return ( - !Object.hasOwn( - TRUSTED_GIT_ENVIRONMENT, - normalized, - ) && - !this.options.maskedEnvironment?.variables.some( - variable => - normalizedEnvironmentName( - variable.name, - this.platform, - ) === normalized, - ) - ); - }) - .map(name => ({ name, mode: 'deny' as const })), - ...(this.options.maskedEnvironment?.variables.map( - variable => ({ - ...variable, - mode: 'mask' as const, - ...(variable.extract - ? { onExtractNoMatch: 'error' as const } - : {}), - }), - ) ?? []), - ], + } + } + const canonicalScratchDirectory = + await this.createScratchDirectory(sharedScratchPaths); + if ( + canonicalScratchDirectory && + isWithin(root, canonicalScratchDirectory) + ) { + throw new WorkspaceToolError( + 'Native sandbox workspace cannot contain worker scratch storage', + 'REGISTRATION_INVALID', + ); + } + const config: SandboxRuntimeConfig = { + network: { + allowedDomains: [...(this.options.allowedDomains ?? [])], + deniedDomains: [], + strictAllowlist: true, + allowAllUnixSockets: false, + allowLocalBinding: false, + ...(this.options.maskedEnvironment ? { tlsTerminate: {} } : {}), + }, + filesystem: { + denyRead: [ + home, + ...sharedScratchPaths.filter((path) => + deniedInheritedWritablePaths.includes(path), + ), + ], + allowRead: [ + root, + ...(canonicalScratchDirectory ? [canonicalScratchDirectory] : []), + ], + allowWrite: [ + root, + ...(canonicalScratchDirectory ? [canonicalScratchDirectory] : []), + ], + denyWrite: [...protectedPaths, ...deniedInheritedWritablePaths], + allowGitConfig: false, + }, + credentials: { + files: protectedPaths.map((path) => ({ + path, + mode: 'deny' as const, + })), + envVars: [ + ...deniedEnvironmentNames( + { + ...this.environment, + CLAUDE_CODE_TMPDIR: '', + CLAUDE_TMPDIR: '', }, - allowAppleEvents: false, - enableWeakerNestedSandbox: false, - enableWeakerNetworkIsolation: false, - git: { safeDirectories: [root] }, - }; - await this.manager.initialize(config); - this.canonicalRoot = root; + this.platform, + ) + .filter((name) => { + const normalized = normalizedEnvironmentName( + name, + this.platform, + ); + return ( + !Object.hasOwn(TRUSTED_GIT_ENVIRONMENT, normalized) && + !this.options.maskedEnvironment?.variables.some( + (variable) => + normalizedEnvironmentName( + variable.name, + this.platform, + ) === normalized, + ) + ); + }) + .map((name) => ({ name, mode: 'deny' as const })), + ...(this.options.maskedEnvironment?.variables.map((variable) => ({ + ...variable, + mode: 'mask' as const, + ...(variable.extract ? { onExtractNoMatch: 'error' as const } : {}), + })) ?? []), + ], + }, + allowAppleEvents: false, + enableWeakerNestedSandbox: false, + enableWeakerNetworkIsolation: false, + git: { safeDirectories: [root] }, + }; + await this.manager.initialize(config); + this.canonicalRoot = root; + } + + async execute( + request: WorkspaceExecuteCommandRequest, + signal?: AbortSignal, + ): Promise { + if (this.execution || this.closing) { + throw new WorkspaceToolError( + 'Native sandbox already has an active command or is closing', + 'COMMAND_UNAVAILABLE', + ); } - - async execute( - request: WorkspaceExecuteCommandRequest, - signal?: AbortSignal, - ): Promise { - if (this.execution || this.closing) { - throw new WorkspaceToolError( - 'Native sandbox already has an active command or is closing', - 'COMMAND_UNAVAILABLE', - ); - } - const execution = this.executeExclusive(request, signal); - this.execution = execution; - try { - return await execution; - } finally { - this.execution = undefined; - } + const execution = this.executeExclusive(request, signal); + this.execution = execution; + try { + return await execution; + } finally { + this.execution = undefined; } - - private async executeExclusive( - request: WorkspaceExecuteCommandRequest, - signal?: AbortSignal, - ): Promise { - if ( - !isWorkspaceToolRequest(request) || - request.operation !== 'execute_command' - ) { - throw new WorkspaceToolError( - 'Invalid native sandbox command', - 'INVALID_REQUEST', - ); - } - if (signal?.aborted) { - throw new WorkspaceToolError( - 'Workspace command execution aborted', - 'EXECUTION_ABORTED', - ); - } - await this.initialize(); - const root = this.canonicalRoot!; - let cwd: string; - try { - cwd = await realpath(resolve(root, request.cwd ?? '.')); - if (!isWithin(root, cwd) || !(await stat(cwd)).isDirectory()) - throw new Error('invalid cwd'); - } catch { - throw new WorkspaceToolError( - 'Command working directory is unavailable', - 'INVALID_PATH', - ); - } - const commandId = `librechat-code-${randomUUID()}`; - const sandboxedCommand = this.options.maskedEnvironment?.wrapCommand - ? this.options.maskedEnvironment.wrapCommand( - request.command, - this.platform, - ) - : request.command; - let wrapped: Awaited< - ReturnType - >; - try { - const credentialEnvironment = - await this.options.maskedEnvironment?.resolve(signal); - wrapped = await this.withTemporaryHostEnvironment( - { - ...TRUSTED_GIT_ENVIRONMENT, - ...(credentialEnvironment ?? {}), - ...this.scratchSelectorEnvironment(), - }, - () => - this.manager.wrapWithSandboxArgv( - sandboxedCommand, - this.platform === 'win32' - ? undefined - : (this.options.shellPath ?? '/bin/bash'), - undefined, - signal, - cwd, - { commandId, commandText: request.command }, - ), - ); - } catch (error) { - if (signal?.aborted) { - throw new WorkspaceToolError( - 'Workspace command execution aborted', - 'EXECUTION_ABORTED', - ); - } - throw new WorkspaceToolError( - 'Native sandbox command could not start', - 'COMMAND_UNAVAILABLE', - ); - } - try { - if (signal?.aborted) { - throw new WorkspaceToolError( - 'Workspace command execution aborted', - 'EXECUTION_ABORTED', - ); - } - return await this.runWrapped( - request, - wrapped, - cwd, - commandId, - signal, - ); - } finally { - // A successful wrap owns command state even when no child is spawned. - try { - this.manager.cleanupAfterCommand(); - } catch { - // Cleanup is retried by close(); command settlement must still finish. - } - } + } + + private async executeExclusive( + request: WorkspaceExecuteCommandRequest, + signal?: AbortSignal, + ): Promise { + if ( + !isWorkspaceToolRequest(request) || + request.operation !== 'execute_command' + ) { + throw new WorkspaceToolError( + 'Invalid native sandbox command', + 'INVALID_REQUEST', + ); } - - private async withTemporaryHostEnvironment( - values: Record, - action: () => Promise, - ): Promise { - const previousMutation = hostEnvironmentMutationQueue; - let releaseMutation!: () => void; - hostEnvironmentMutationQueue = new Promise(resolve => { - releaseMutation = resolve; - }); - await previousMutation; - const previous = new Map(); - try { - for (const [name, value] of Object.entries(values)) { - previous.set(name, process.env[name]); - process.env[name] = value; - } - return await action(); - } finally { - for (const [name, value] of previous) { - if (value === undefined) delete process.env[name]; - else process.env[name] = value; - } - releaseMutation(); - } + if (signal?.aborted) { + throw new WorkspaceToolError( + 'Workspace command execution aborted', + 'EXECUTION_ABORTED', + ); } - - private async runWrapped( - request: WorkspaceExecuteCommandRequest, - wrapped: { argv: string[]; env: NodeJS.ProcessEnv }, - cwd: string, - commandId: string, - signal?: AbortSignal, - ): Promise { - const outputLimit = - request.maxOutputBytes ?? - BRIDGE_WORKSPACE_COMMAND_DEFAULT_OUTPUT_BYTES; - const timeoutMs = - request.timeoutMs ?? BRIDGE_WORKSPACE_COMMAND_DEFAULT_TIMEOUT_MS; - return await new Promise( - (resolvePromise, reject) => { - let child: ChildProcessWithoutNullStreams; - try { - child = this.spawnCommand( - wrapped.argv[0], - wrapped.argv.slice(1), - { - cwd, - env: { - ...wrapped.env, - ...this.scratchEnvironment(), - ...TRUSTED_GIT_CONFIG_ENTRIES, - GIT_CONFIG_COUNT: - wrapped.env.GIT_CONFIG_COUNT ?? - TRUSTED_GIT_CONFIG_COUNT, - GIT_CONFIG_GLOBAL: - this.platform === 'win32' - ? 'NUL' - : '/dev/null', - GIT_CONFIG_NOSYSTEM: '1', - }, - detached: this.platform !== 'win32', - shell: false, - windowsHide: true, - }, - ); - child.stdin.end(); - } catch { - reject( - new WorkspaceToolError( - 'Native sandbox command could not start', - 'COMMAND_UNAVAILABLE', - ), - ); - return; - } - 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 abort = (): void => { - if (settled) return; - this.killCommandTree(child); - }; - signal?.addEventListener('abort', abort, { once: true }); - if (signal?.aborted) abort(); - const timer = setTimeout(() => { - if (settled) return; - timedOut = true; - this.killCommandTree(child); - }, timeoutMs); - const cleanup = (): void => { - clearTimeout(timer); - signal?.removeEventListener('abort', abort); - }; - child.once('error', () => { - if (settled) return; - settled = true; - const mayHaveStarted = child.pid != null; - this.killCommandTree(child); - cleanup(); - reject( - new WorkspaceToolError( - 'Native sandbox command could not start', - 'COMMAND_UNAVAILABLE', - mayHaveStarted, - ), - ); - }); - child.once('close', (code, childSignal) => { - if (settled) return; - settled = true; - this.killCommandTree(child); - cleanup(); - if (signal?.aborted) { - reject( - new WorkspaceToolError( - 'Workspace command execution aborted', - 'EXECUTION_ABORTED', - true, - ), - ); - 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, - ); - } catch { - // Preserve the bounded child error if optional violation annotation fails. - } - const stderrValue = boundedUtf8( - Buffer.from(annotatedStderr), - stderrBudget, - ); - resolvePromise({ - protocolVersion: BRIDGE_PROTOCOL_VERSION, - operation: 'execute_command', - workspaceId: request.workspaceId, - exitCode: - timedOut || childSignal - ? null - : this.protocolExitCode(code), - ...(childSignal ? { signal: childSignal } : {}), - stdout: stdoutValue, - stderr: stderrValue, - truncated: - truncated || - Buffer.byteLength(annotatedStderr) > stderrBudget, - timedOut, - }); - }); - }, + await this.initialize(); + const root = this.canonicalRoot!; + let cwd: string; + try { + cwd = await realpath(resolve(root, request.cwd ?? '.')); + if (!isWithin(root, cwd) || !(await stat(cwd)).isDirectory()) + throw new Error('invalid cwd'); + } catch { + throw new WorkspaceToolError( + 'Command working directory is unavailable', + 'INVALID_PATH', + ); + } + const commandId = `librechat-code-${randomUUID()}`; + const sandboxedCommand = this.options.maskedEnvironment?.wrapCommand + ? this.options.maskedEnvironment.wrapCommand( + request.command, + this.platform, + ) + : request.command; + let wrapped: Awaited< + ReturnType + >; + try { + const credentialEnvironment = + await this.options.maskedEnvironment?.resolve(signal); + wrapped = await this.withTemporaryHostEnvironment( + { + ...TRUSTED_GIT_ENVIRONMENT, + ...(credentialEnvironment ?? {}), + ...this.scratchSelectorEnvironment(), + }, + () => + this.manager.wrapWithSandboxArgv( + sandboxedCommand, + this.platform === 'win32' + ? undefined + : (this.options.shellPath ?? '/bin/bash'), + undefined, + signal, + cwd, + { commandId, commandText: request.command }, + ), + ); + } catch (error) { + if (signal?.aborted) { + throw new WorkspaceToolError( + 'Workspace command execution aborted', + 'EXECUTION_ABORTED', ); + } + throw new WorkspaceToolError( + 'Native sandbox command could not start', + 'COMMAND_UNAVAILABLE', + ); } - - private killCommandTree(child: ChildProcessWithoutNullStreams): void { - try { - if (this.platform !== 'win32' && child.pid != null) { - process.kill(-child.pid, 'SIGKILL'); - } else { - child.kill('SIGKILL'); - } - } catch { - // The command group has already exited. - } + try { + if (signal?.aborted) { + throw new WorkspaceToolError( + 'Workspace command execution aborted', + 'EXECUTION_ABORTED', + ); + } + return await this.runWrapped(request, wrapped, cwd, commandId, signal); + } finally { + // A successful wrap owns command state even when no child is spawned. + try { + this.manager.cleanupAfterCommand(); + } catch { + // Cleanup is retried by close(); command settlement must still finish. + } } - - private protocolExitCode(code: number | null): number { - return Number.isSafeInteger(code) && - code != null && - code >= 0 && - code <= 255 - ? code - : 1; + } + + private async withTemporaryHostEnvironment( + values: Record, + action: () => Promise, + ): Promise { + const previousMutation = hostEnvironmentMutationQueue; + let releaseMutation!: () => void; + hostEnvironmentMutationQueue = new Promise((resolve) => { + releaseMutation = resolve; + }); + await previousMutation; + const previous = new Map(); + try { + for (const [name, value] of Object.entries(values)) { + previous.set(name, process.env[name]); + process.env[name] = value; + } + return await action(); + } finally { + for (const [name, value] of previous) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + releaseMutation(); } - - private async createScratchDirectory( - sharedScratchPaths: string[], - ): Promise { - // Windows SRT supplies the restricted account's private TEMP directory. - if (this.platform === 'win32') return undefined; - if (this.scratchDirectory || this.scratchHandle) { - throw new Error( - 'Native sandbox scratch cleanup is still pending; close the sandbox before reinitializing', - ); + } + + private async runWrapped( + request: WorkspaceExecuteCommandRequest, + wrapped: { argv: string[]; env: NodeJS.ProcessEnv }, + cwd: string, + commandId: string, + signal?: AbortSignal, + ): Promise { + const outputLimit = + request.maxOutputBytes ?? BRIDGE_WORKSPACE_COMMAND_DEFAULT_OUTPUT_BYTES; + const timeoutMs = + request.timeoutMs ?? BRIDGE_WORKSPACE_COMMAND_DEFAULT_TIMEOUT_MS; + return await new Promise( + (resolvePromise, reject) => { + let child: ChildProcessWithoutNullStreams; + try { + child = this.spawnCommand(wrapped.argv[0], wrapped.argv.slice(1), { + cwd, + env: { + ...wrapped.env, + ...this.scratchEnvironment(), + ...TRUSTED_GIT_CONFIG_ENTRIES, + GIT_CONFIG_COUNT: + wrapped.env.GIT_CONFIG_COUNT ?? TRUSTED_GIT_CONFIG_COUNT, + GIT_CONFIG_GLOBAL: + this.platform === 'win32' ? 'NUL' : '/dev/null', + GIT_CONFIG_NOSYSTEM: '1', + }, + detached: this.platform !== 'win32', + shell: false, + windowsHide: true, + }); + child.stdin.end(); + } catch { + reject( + new WorkspaceToolError( + 'Native sandbox command could not start', + 'COMMAND_UNAVAILABLE', + ), + ); + return; } - const canonicalTemporaryRoot = await canonicalPath(HOST_TEMPORARY_ROOT); - const sharedScratchRoot = sharedScratchPaths.find(path => - isWithin(path, canonicalTemporaryRoot), - ); - const scratchDirectory = await mkdtemp( - join( - sharedScratchRoot - ? dirname(sharedScratchRoot) - : canonicalTemporaryRoot, - NATIVE_SANDBOX_SCRATCH_PREFIX, + 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 abort = (): void => { + if (settled) return; + this.killCommandTree(child); + }; + signal?.addEventListener('abort', abort, { once: true }); + if (signal?.aborted) abort(); + const timer = setTimeout(() => { + if (settled) return; + timedOut = true; + this.killCommandTree(child); + }, timeoutMs); + const cleanup = (): void => { + clearTimeout(timer); + signal?.removeEventListener('abort', abort); + }; + child.once('error', () => { + if (settled) return; + settled = true; + const mayHaveStarted = child.pid != null; + this.killCommandTree(child); + cleanup(); + reject( + new WorkspaceToolError( + 'Native sandbox command could not start', + 'COMMAND_UNAVAILABLE', + mayHaveStarted, ), - ); - try { - await assertPrivateStorageAncestors(scratchDirectory); - const scratchHandle = await open(scratchDirectory, 'r'); - try { - await removePrivateStorageAcl(scratchHandle, scratchDirectory); - await scratchHandle.chmod(0o700); - await assertPrivateStorageAcl( - scratchHandle, - scratchDirectory, - true, - ); - if (((await scratchHandle.stat()).mode & 0o777) !== 0o700) { - throw new Error( - 'Native sandbox scratch directory is not private', - ); - } - this.scratchHandle = scratchHandle; - } catch (error) { - await scratchHandle.close(); - throw error; - } - this.scratchDirectory = await realpath(scratchDirectory); - return this.scratchDirectory; - } catch (error) { - await this.scratchHandle?.close().catch(() => undefined); - this.scratchHandle = undefined; - await rm(scratchDirectory, { recursive: true, force: true }).catch( - () => undefined, + ); + }); + child.once('close', (code, childSignal) => { + if (settled) return; + settled = true; + this.killCommandTree(child); + cleanup(); + if (signal?.aborted) { + reject( + new WorkspaceToolError( + 'Workspace command execution aborted', + 'EXECUTION_ABORTED', + true, + ), ); - throw error; - } - } - - private scratchEnvironment(): NodeJS.ProcessEnv { - const scratchDirectory = this.scratchDirectory; - if (!scratchDirectory) return {}; - return this.platform === 'win32' - ? { - TMPDIR: scratchDirectory, - TEMP: scratchDirectory, - TMP: scratchDirectory, - } - : { TMPDIR: scratchDirectory }; + 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, + ); + } catch { + // Preserve the bounded child error if optional violation annotation fails. + } + const stderrValue = boundedUtf8( + Buffer.from(annotatedStderr), + stderrBudget, + ); + resolvePromise({ + protocolVersion: BRIDGE_PROTOCOL_VERSION, + operation: 'execute_command', + workspaceId: request.workspaceId, + exitCode: + timedOut || childSignal ? null : this.protocolExitCode(code), + ...(childSignal ? { signal: childSignal } : {}), + stdout: stdoutValue, + stderr: stderrValue, + truncated: + truncated || Buffer.byteLength(annotatedStderr) > stderrBudget, + timedOut, + }); + }); + }, + ); + } + + private killCommandTree(child: ChildProcessWithoutNullStreams): void { + try { + if (this.platform !== 'win32' && child.pid != null) { + process.kill(-child.pid, 'SIGKILL'); + } else { + child.kill('SIGKILL'); + } + } catch { + // The command group has already exited. } - - private scratchSelectorEnvironment(): NodeJS.ProcessEnv { - const scratchDirectory = this.scratchDirectory; - if (!scratchDirectory) return {}; - return Object.fromEntries( - SRT_SCRATCH_SELECTOR_NAMES.map(name => [name, scratchDirectory]), + } + + private protocolExitCode(code: number | null): number { + return Number.isSafeInteger(code) && + code != null && + code >= 0 && + code <= 255 + ? code + : 1; + } + + private async createScratchDirectory( + sharedScratchPaths: string[], + ): Promise { + // Windows SRT supplies the restricted account's private TEMP directory. + if (this.platform === 'win32') return undefined; + const canonicalTemporaryRoot = await canonicalPath(HOST_TEMPORARY_ROOT); + const sharedScratchRoot = sharedScratchPaths.find((path) => + isWithin(path, canonicalTemporaryRoot), + ); + const scratchDirectory = await mkdtemp( + join( + sharedScratchRoot + ? dirname(sharedScratchRoot) + : canonicalTemporaryRoot, + NATIVE_SANDBOX_SCRATCH_PREFIX, + ), + ); + try { + await assertPrivateStorageAncestors(scratchDirectory); + const scratchHandle = await open(scratchDirectory, 'r'); + try { + await removePrivateStorageAcl(scratchHandle, scratchDirectory); + await scratchHandle.chmod(0o700); + await assertPrivateStorageAcl( + scratchHandle, + scratchDirectory, + true, ); - } - - private async removeScratchDirectory(): Promise { - const scratchDirectory = this.scratchDirectory; - if (!scratchDirectory) return; - const scratchHandle = this.scratchHandle; - if (!scratchHandle) { - throw new Error('Native sandbox scratch descriptor is unavailable'); - } - try { - await rm(scratchDirectory, { recursive: true, force: true }); - } catch { - await restoreScratchTraversal(scratchHandle); - await rm(scratchDirectory, { recursive: true, force: true }); + if (((await scratchHandle.stat()).mode & 0o777) !== 0o700) { + throw new Error('Native sandbox scratch directory is not private'); } - // Retain both the descriptor and path when cleanup fails so close() can - // retry without falling back to an attacker-replaceable ambient path. + this.scratchHandle = scratchHandle; + } catch (error) { await scratchHandle.close(); - this.scratchHandle = undefined; - this.scratchDirectory = undefined; + throw error; + } + this.scratchDirectory = await realpath(scratchDirectory); + return this.scratchDirectory; + } catch (error) { + await this.scratchHandle?.close().catch(() => undefined); + this.scratchHandle = undefined; + await rm(scratchDirectory, { recursive: true, force: true }).catch( + () => undefined, + ); + throw error; } - - async close(): Promise { - if (this.closing) return this.closing; - const closing = this.closeExclusive(); - this.closing = closing; - try { - await closing; - } finally { - this.closing = undefined; + } + + private scratchEnvironment(): NodeJS.ProcessEnv { + const scratchDirectory = this.scratchDirectory; + if (!scratchDirectory) return {}; + return this.platform === 'win32' + ? { + TMPDIR: scratchDirectory, + TEMP: scratchDirectory, + TMP: scratchDirectory, } + : { TMPDIR: scratchDirectory }; + } + + private scratchSelectorEnvironment(): NodeJS.ProcessEnv { + const scratchDirectory = this.scratchDirectory; + if (!scratchDirectory) return {}; + return Object.fromEntries( + SRT_SCRATCH_SELECTOR_NAMES.map((name) => [name, scratchDirectory]), + ); + } + + private async removeScratchDirectory(): Promise { + const scratchDirectory = this.scratchDirectory; + if (!scratchDirectory) return; + const scratchHandle = this.scratchHandle; + if (!scratchHandle) { + throw new Error('Native sandbox scratch descriptor is unavailable'); } - - private async closeExclusive(): Promise { - // Never reset proxy/credential state or remove scratch beneath a live child. - await this.execution?.catch(() => undefined); - await this.initialized?.catch(() => undefined); - if (managerOwners.get(this.manager) === this) { - this.resetFailed = true; - await this.manager.reset(); - this.resetFailed = false; - managerOwners.delete(this.manager); - } - this.initialized = undefined; - this.canonicalRoot = undefined; - await this.removeScratchDirectory(); + try { + await rm(scratchDirectory, { recursive: true, force: true }); + } catch { + await restoreScratchTraversal(scratchHandle); + await rm(scratchDirectory, { recursive: true, force: true }); + } + // Retain both the descriptor and path when cleanup fails so close() can + // retry without falling back to an attacker-replaceable ambient path. + await scratchHandle.close(); + this.scratchHandle = undefined; + this.scratchDirectory = undefined; + } + + async close(): Promise { + if (this.closing) return this.closing; + const closing = this.closeExclusive(); + this.closing = closing; + try { + await closing; + } finally { + this.closing = undefined; + } + } + + private async closeExclusive(): Promise { + // Never reset proxy/credential state or remove scratch beneath a live child. + await this.execution?.catch(() => undefined); + await this.initialized?.catch(() => undefined); + if (managerOwners.get(this.manager) === this) { + this.resetFailed = true; + await this.manager.reset(); + this.resetFailed = false; + managerOwners.delete(this.manager); } + this.initialized = undefined; + this.canonicalRoot = undefined; + await this.removeScratchDirectory(); + } } diff --git a/packages/code/src/native-scratch.ts b/packages/code/src/native-scratch.ts index ae8ef0f..59338a7 100644 --- a/packages/code/src/native-scratch.ts +++ b/packages/code/src/native-scratch.ts @@ -1,5 +1,5 @@ import { constants as fsConstants } from 'node:fs'; -import { opendir } from 'node:fs/promises'; +import { readdir } from 'node:fs/promises'; import type { FileHandle } from 'node:fs/promises'; import koffi from 'koffi'; @@ -7,137 +7,107 @@ import koffi from 'koffi'; import { removePrivateStorageAcl } from './private-storage.js'; const POSIX_PLATFORMS = new Set(['darwin', 'linux']); -const lib = POSIX_PLATFORMS.has(process.platform) - ? koffi.load(null) - : undefined; -const openat = lib?.func( - 'int openat(int dirfd, const char *path, int flags, uint32_t mode)', -); +const lib = POSIX_PLATFORMS.has(process.platform) ? koffi.load(null) : undefined; +const openat = lib?.func('int openat(int dirfd, const char *path, int flags, uint32_t mode)'); const closeFd = lib?.func('int close(int fd)'); const fchmod = lib?.func('int fchmod(int fd, uint32_t mode)'); const fchmodat = lib?.func( - 'int fchmodat(int dirfd, const char *path, uint32_t mode, int flags)', + 'int fchmodat(int dirfd, const char *path, uint32_t mode, int flags)', ); const AT_SYMLINK_NOFOLLOW = process.platform === 'darwin' ? 0x0020 : 0x0100; const O_EVTONLY = 0x8000; -// Recovery is a last-resort shutdown path over an attacker-controlled tree. -// Keep both its memory use and its descriptor-relative reopen work bounded. -const MAX_SCRATCH_DIRECTORIES = 10_000; -const MAX_SCRATCH_ENTRIES = 100_000; -const MAX_SCRATCH_DEPTH = 128; -const MAX_SCRATCH_COMPONENT_VISITS = 16_384; const IGNORED_ENTRY_ERRNOS = new Set([ - koffi.os.errno.ENOENT, - koffi.os.errno.ELOOP, - koffi.os.errno.ENOTDIR, - koffi.os.errno.ENOTSUP, + koffi.os.errno.ENOENT, + koffi.os.errno.ELOOP, + koffi.os.errno.ENOTDIR, + koffi.os.errno.ENOTSUP, ]); export interface ScratchTraversalHooks { - /** Test seam for deterministic replacement-race coverage. */ - afterEntryInspected?(directoryFd: number, name: string): Promise; + /** Test seam for deterministic replacement-race coverage. */ + afterEntryInspected?(directoryFd: number, name: string): Promise; } function descriptorPath(fd: number): string { - return process.platform === 'linux' - ? `/proc/self/fd/${fd}` - : `/dev/fd/${fd}`; + return process.platform === 'linux' ? `/proc/self/fd/${fd}` : `/dev/fd/${fd}`; } function requirePosixBindings(): void { - if (!openat || !closeFd || !fchmod || !fchmodat) { - throw new Error('Descriptor-relative scratch cleanup is unavailable'); - } + if (!openat || !closeFd || !fchmod || !fchmodat) { + throw new Error('Descriptor-relative scratch cleanup is unavailable'); + } } function ignoredEntryError(): boolean { - return IGNORED_ENTRY_ERRNOS.has(koffi.errno()); + return IGNORED_ENTRY_ERRNOS.has(koffi.errno()); } function restoreEntryMode(directoryFd: number, name: string): boolean { - requirePosixBindings(); - if (fchmodat!(directoryFd, name, 0o700, AT_SYMLINK_NOFOLLOW) === 0) - return true; - if (ignoredEntryError()) return false; - throw new Error( - `Descriptor-relative scratch chmod failed with errno ${koffi.errno()}`, - ); + requirePosixBindings(); + if (fchmodat!(directoryFd, name, 0o700, AT_SYMLINK_NOFOLLOW) === 0) return true; + if (ignoredEntryError()) return false; + throw new Error(`Descriptor-relative scratch chmod failed with errno ${koffi.errno()}`); } -function openDirectoryAt( - directoryFd: number, - name: string, -): number | undefined { - requirePosixBindings(); - const commonFlags = fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW; - const repairFlags = - process.platform === 'darwin' - ? commonFlags | O_EVTONLY - : commonFlags | fsConstants.O_RDONLY; - const fd = openat!(directoryFd, name, repairFlags, 0); - if (fd >= 0) return fd; - if (ignoredEntryError()) return undefined; - throw new Error( - `Descriptor-relative scratch open failed with errno ${koffi.errno()}`, - ); +function openDirectoryAt(directoryFd: number, name: string): number | undefined { + requirePosixBindings(); + const commonFlags = fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW; + const repairFlags = process.platform === 'darwin' + ? commonFlags | O_EVTONLY + : commonFlags | fsConstants.O_RDONLY; + const fd = openat!(directoryFd, name, repairFlags, 0); + if (fd >= 0) return fd; + if (ignoredEntryError()) return undefined; + throw new Error(`Descriptor-relative scratch open failed with errno ${koffi.errno()}`); } function closeDirectory(fd: number): void { - requirePosixBindings(); - if (closeFd!(fd) !== 0) { - throw new Error( - `Descriptor-relative scratch close failed with errno ${koffi.errno()}`, - ); - } + requirePosixBindings(); + if (closeFd!(fd) !== 0) { + throw new Error(`Descriptor-relative scratch close failed with errno ${koffi.errno()}`); + } } async function repairDirectory(fd: number, label: string): Promise { - requirePosixBindings(); - if (fchmod!(fd, 0o700) !== 0) { - throw new Error( - `Descriptor-relative scratch chmod failed with errno ${koffi.errno()}`, - ); - } - await removePrivateStorageAcl({ fd }, label); + requirePosixBindings(); + if (fchmod!(fd, 0o700) !== 0) { + throw new Error(`Descriptor-relative scratch chmod failed with errno ${koffi.errno()}`); + } + await removePrivateStorageAcl({ fd }, label); } async function openRelativeDirectory( - rootFd: number, - components: string[], - hooks: ScratchTraversalHooks, - consumeComponentVisit: () => void, + rootFd: number, + components: string[], + hooks: ScratchTraversalHooks, ): Promise { - let currentFd = rootFd; - try { - for (const component of components) { - consumeComponentVisit(); - await hooks.afterEntryInspected?.(currentFd, component); - if (!restoreEntryMode(currentFd, component)) { - if (currentFd !== rootFd) { - const closingFd = currentFd; - currentFd = rootFd; - closeDirectory(closingFd); - } - return undefined; - } - const childFd = openDirectoryAt(currentFd, component); - if (currentFd !== rootFd) { - const closingFd = currentFd; - currentFd = rootFd; - closeDirectory(closingFd); - } - if (childFd === undefined) return undefined; - currentFd = childFd; - await repairDirectory( - currentFd, - `scratch directory ${components.join('/')}`, - ); + let currentFd = rootFd; + try { + for (const component of components) { + await hooks.afterEntryInspected?.(currentFd, component); + if (!restoreEntryMode(currentFd, component)) { + if (currentFd !== rootFd) { + const closingFd = currentFd; + currentFd = rootFd; + closeDirectory(closingFd); } - return currentFd; - } catch (error) { - if (currentFd !== rootFd) closeDirectory(currentFd); - throw error; + return undefined; + } + const childFd = openDirectoryAt(currentFd, component); + if (currentFd !== rootFd) { + const closingFd = currentFd; + currentFd = rootFd; + closeDirectory(closingFd); + } + if (childFd === undefined) return undefined; + currentFd = childFd; + await repairDirectory(currentFd, `scratch directory ${components.join('/')}`); } + return currentFd; + } catch (error) { + if (currentFd !== rootFd) closeDirectory(currentFd); + throw error; + } } /** @@ -146,58 +116,25 @@ async function openRelativeDirectory( * path holds at most two descriptors and refuses replacement symlinks. */ export async function restoreScratchTraversal( - root: FileHandle, - hooks: ScratchTraversalHooks = {}, + root: FileHandle, + hooks: ScratchTraversalHooks = {}, ): Promise { - await root.chmod(0o700); - await removePrivateStorageAcl(root, 'native sandbox scratch root'); - const pending: string[][] = [[]]; - let componentVisits = 0; - const consumeComponentVisit = () => { - componentVisits += 1; - if (componentVisits > MAX_SCRATCH_COMPONENT_VISITS) { - throw new Error( - 'Native sandbox scratch cleanup exceeded its work limit', - ); - } - }; - let entriesInspected = 0; - for (let index = 0; index < pending.length; index += 1) { - const components = pending[index]; - const directoryFd = - components.length === 0 - ? root.fd - : await openRelativeDirectory( - root.fd, - components, - hooks, - consumeComponentVisit, - ); - if (directoryFd === undefined) continue; - try { - const directory = await opendir(descriptorPath(directoryFd)); - for await (const entry of directory) { - entriesInspected += 1; - if (entriesInspected > MAX_SCRATCH_ENTRIES) { - throw new Error( - 'Native sandbox scratch cleanup exceeded its entry limit', - ); - } - if (!entry.isDirectory()) continue; - if (components.length >= MAX_SCRATCH_DEPTH) { - throw new Error( - 'Native sandbox scratch cleanup exceeded its depth limit', - ); - } - if (pending.length >= MAX_SCRATCH_DIRECTORIES) { - throw new Error( - 'Native sandbox scratch cleanup exceeded its directory limit', - ); - } - pending.push([...components, entry.name]); - } - } finally { - if (directoryFd !== root.fd) closeDirectory(directoryFd); - } + await root.chmod(0o700); + await removePrivateStorageAcl(root, 'native sandbox scratch root'); + const pending: string[][] = [[]]; + for (let index = 0; index < pending.length; index += 1) { + const components = pending[index]; + const directoryFd = components.length === 0 + ? root.fd + : await openRelativeDirectory(root.fd, components, hooks); + if (directoryFd === undefined) continue; + try { + const entries = await readdir(descriptorPath(directoryFd), { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) pending.push([...components, entry.name]); + } + } finally { + if (directoryFd !== root.fd) closeDirectory(directoryFd); } + } } From 2c988044e95c6b64e4a3fe8ec95349ca660155e8 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 8 Sep 2026 23:43:50 -0400 Subject: [PATCH 6/6] fix: Bound Native Scratch Recovery Work --- packages/code/src/native-sandbox.test.ts | 50 +++++++++++++++++++++++- packages/code/src/native-sandbox.ts | 5 +++ packages/code/src/native-scratch.ts | 42 +++++++++++++++++--- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/packages/code/src/native-sandbox.test.ts b/packages/code/src/native-sandbox.test.ts index 9289271..a74f372 100644 --- a/packages/code/src/native-sandbox.test.ts +++ b/packages/code/src/native-sandbox.test.ts @@ -502,12 +502,12 @@ test('scratch traversal removes command-created Darwin ACLs', async (t) => { await assert.rejects(access(result.stdout)); }); -test('scratch traversal bounds descriptors across a deep tree', async (t) => { +test('scratch traversal bounds descriptors and work across a deep tree', async (t) => { if (process.platform === 'win32') return; const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-depth-')); t.after(() => rm(root, { recursive: true, force: true })); const directories = [root]; - for (let depth = 0; depth < 300; depth += 1) { + for (let depth = 0; depth < 100; depth += 1) { directories.push(join(directories[directories.length - 1], 'd')); await mkdir(directories[directories.length - 1]); } @@ -522,6 +522,52 @@ test('scratch traversal bounds descriptors across a deep tree', async (t) => { assert.equal((await stat(directories[directories.length - 1])).mode & 0o777, 0o700); }); +test('scratch traversal rejects trees beyond its recovery depth limit', async (t) => { + if (process.platform === 'win32') return; + const root = await mkdtemp(join(tmpdir(), 'librechat-code-scratch-depth-limit-')); + t.after(() => rm(root, { recursive: true, force: true })); + let directory = root; + for (let depth = 0; depth < 129; depth += 1) { + directory = join(directory, 'd'); + await mkdir(directory); + } + const rootHandle = await open(root, 'r'); + t.after(() => rootHandle.close()); + + await assert.rejects( + restoreScratchTraversal(rootHandle), + /scratch cleanup exceeded its depth limit/, + ); +}); + +test('does not replace scratch state while cleanup remains pending', async (t) => { + if (process.platform === 'win32') return; + const workspace = await mkdtemp(join(tmpdir(), 'librechat-code-native-')); + const retained = await mkdtemp(join(tmpdir(), 'librechat-code-retained-')); + const retainedHandle = await open(retained, 'r'); + t.after(() => retainedHandle.close()); + t.after(() => rm(retained, { recursive: true, force: true })); + t.after(() => rm(workspace, { recursive: true, force: true })); + const sandbox = new NativeSrtWorkspaceCommandSandbox({ + workspaceRoot: workspace, + manager: fakeManager().manager, + }); + const mutable = sandbox as unknown as { + scratchDirectory?: string; + scratchHandle?: typeof retainedHandle; + createScratchDirectory(paths: string[]): Promise; + }; + mutable.scratchDirectory = retained; + mutable.scratchHandle = retainedHandle; + + await assert.rejects( + mutable.createScratchDirectory([]), + /scratch cleanup is still pending/, + ); + assert.equal(mutable.scratchDirectory, retained); + assert.equal(mutable.scratchHandle, retainedHandle); +}); + const proxyEnvironment = { HTTP_PROXY: 'http://upstream.invalid:8080', HTTPS_PROXY: 'http://upstream.invalid:8080', diff --git a/packages/code/src/native-sandbox.ts b/packages/code/src/native-sandbox.ts index 1adfd35..49d269c 100644 --- a/packages/code/src/native-sandbox.ts +++ b/packages/code/src/native-sandbox.ts @@ -748,6 +748,11 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox ): Promise { // Windows SRT supplies the restricted account's private TEMP directory. if (this.platform === 'win32') return undefined; + if (this.scratchDirectory || this.scratchHandle) { + throw new Error( + 'Native sandbox scratch cleanup is still pending; close the sandbox before reinitializing', + ); + } const canonicalTemporaryRoot = await canonicalPath(HOST_TEMPORARY_ROOT); const sharedScratchRoot = sharedScratchPaths.find((path) => isWithin(path, canonicalTemporaryRoot), diff --git a/packages/code/src/native-scratch.ts b/packages/code/src/native-scratch.ts index 59338a7..0ecd330 100644 --- a/packages/code/src/native-scratch.ts +++ b/packages/code/src/native-scratch.ts @@ -1,5 +1,5 @@ import { constants as fsConstants } from 'node:fs'; -import { readdir } from 'node:fs/promises'; +import { opendir } from 'node:fs/promises'; import type { FileHandle } from 'node:fs/promises'; import koffi from 'koffi'; @@ -16,6 +16,12 @@ const fchmodat = lib?.func( ); const AT_SYMLINK_NOFOLLOW = process.platform === 'darwin' ? 0x0020 : 0x0100; const O_EVTONLY = 0x8000; +// Recovery is a last-resort shutdown path over an attacker-controlled tree. +// Keep both its memory use and its descriptor-relative reopen work bounded. +const MAX_SCRATCH_DIRECTORIES = 10_000; +const MAX_SCRATCH_ENTRIES = 100_000; +const MAX_SCRATCH_DEPTH = 128; +const MAX_SCRATCH_COMPONENT_VISITS = 16_384; const IGNORED_ENTRY_ERRNOS = new Set([ koffi.os.errno.ENOENT, koffi.os.errno.ELOOP, @@ -80,10 +86,12 @@ async function openRelativeDirectory( rootFd: number, components: string[], hooks: ScratchTraversalHooks, + consumeComponentVisit: () => void, ): Promise { let currentFd = rootFd; try { for (const component of components) { + consumeComponentVisit(); await hooks.afterEntryInspected?.(currentFd, component); if (!restoreEntryMode(currentFd, component)) { if (currentFd !== rootFd) { @@ -122,16 +130,40 @@ export async function restoreScratchTraversal( await root.chmod(0o700); await removePrivateStorageAcl(root, 'native sandbox scratch root'); const pending: string[][] = [[]]; + let componentVisits = 0; + const consumeComponentVisit = () => { + componentVisits += 1; + if (componentVisits > MAX_SCRATCH_COMPONENT_VISITS) { + throw new Error('Native sandbox scratch cleanup exceeded its work limit'); + } + }; + let entriesInspected = 0; for (let index = 0; index < pending.length; index += 1) { const components = pending[index]; const directoryFd = components.length === 0 ? root.fd - : await openRelativeDirectory(root.fd, components, hooks); + : await openRelativeDirectory( + root.fd, + components, + hooks, + consumeComponentVisit, + ); if (directoryFd === undefined) continue; try { - const entries = await readdir(descriptorPath(directoryFd), { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) pending.push([...components, entry.name]); + const directory = await opendir(descriptorPath(directoryFd)); + for await (const entry of directory) { + entriesInspected += 1; + if (entriesInspected > MAX_SCRATCH_ENTRIES) { + throw new Error('Native sandbox scratch cleanup exceeded its entry limit'); + } + if (!entry.isDirectory()) continue; + if (components.length >= MAX_SCRATCH_DEPTH) { + throw new Error('Native sandbox scratch cleanup exceeded its depth limit'); + } + if (pending.length >= MAX_SCRATCH_DIRECTORIES) { + throw new Error('Native sandbox scratch cleanup exceeded its directory limit'); + } + pending.push([...components, entry.name]); } } finally { if (directoryFd !== root.fd) closeDirectory(directoryFd);