UNPKG

@openai/agents-core

Version:

The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.

1,108 lines • 49.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UnixLocalSandboxClient = exports.UnixLocalSandboxSession = void 0; const sessionStateTrust_1 = require("../internal/sessionStateTrust.js"); const errors_1 = require("../../errors.js"); const applyDiff_1 = require("../../utils/applyDiff.js"); const promises_1 = require("node:fs/promises"); const node_fs_1 = require("node:fs"); const node_child_process_1 = require("node:child_process"); const node_path_1 = require("node:path"); const node_os_1 = require("node:os"); const client_1 = require("../client.js"); const session_1 = require("../session.js"); const manifest_1 = require("../manifest.js"); const errors_2 = require("../errors.js"); const workspacePaths_1 = require("../workspacePaths.js"); const localWorkspace_1 = require("./shared/localWorkspace.js"); const manifestPersistence_1 = require("./shared/manifestPersistence.js"); const mountSecurity_1 = require("../mountSecurity.js"); const hostPath_1 = require("../shared/hostPath.js"); const media_1 = require("../shared/media.js"); const unixLocalFiles_1 = require("./shared/unixLocalFiles.js"); const output_1 = require("../shared/output.js"); const localSnapshots_1 = require("./shared/localSnapshots.js"); const workspaceArchive_1 = require("./shared/workspaceArchive.js"); const pty_1 = require("./shared/pty.js"); const runProcess_1 = require("./shared/runProcess.js"); const shellCommand_1 = require("./shared/shellCommand.js"); const sessionStateValues_1 = require("./shared/sessionStateValues.js"); const DEFAULT_EXEC_YIELD_TIME_MS = 10_000; const DEFAULT_WRITE_STDIN_YIELD_TIME_MS = 250; const MAX_ACTIVE_PROCESS_OUTPUT_CHARS = 1024 * 1024; const RUN_AS_LOOKUP_TIMEOUT_MS = 10_000; const DEFAULT_SANDBOX_COMMAND_PATH = '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'; class UnixLocalSandboxSession { state; defaultShell; archiveLimits; activeProcesses = new Map(); nextSessionId = 1; closePromise; stopPromise; fileOperationGeneration = 0; hostFiles; get files() { if (!this.hostFiles) { throw new errors_1.UserError('Host file operations are unavailable for this sandbox.'); } return this.hostFiles; } filesystemRoots = new Map(); constructor(args) { this.hostFiles = args[unixLocalFiles_1.preparedFileIO] === null ? null : (args[unixLocalFiles_1.preparedFileIO] ?? new unixLocalFiles_1.UnixLocalFiles(args.fileIOProtection)); this.state = args.state; this.defaultShell = args.defaultShell; this.setArchiveLimits(args.archiveLimits); this.captureFilesystemRoots(); } /** Selected once at creation or resume; Docker subclasses use the container backend. */ get fileIOBackend() { return 'python'; } setArchiveLimits(limits) { (0, client_1.validateSandboxArchiveLimits)(limits); this.archiveLimits = limits; } createEditor(runAs) { this.assertSessionUsable(); return new UnixLocalSandboxEditor(this, (operation) => this.applyFileOperation(operation, runAs)); } supportsPty() { return true; } async resolveExposedPort(port) { this.assertSessionUsable(); const exposedPort = (0, session_1.normalizeExposedPort)(port); const configuredPorts = this.state.configuredExposedPorts ?? []; if (configuredPorts.length > 0 && !configuredPorts.includes(exposedPort)) { throw new errors_2.SandboxConfigurationError(`UnixLocalSandboxClient was not configured to expose port ${exposedPort}.`, { provider: 'UnixLocalSandboxClient', port: exposedPort, configuredPorts, }); } return (0, session_1.recordExposedPortEndpoint)(this.state, { host: '127.0.0.1', port: exposedPort, tls: false, }); } async execCommand(args) { return (0, output_1.formatExecResponse)(await this.exec(args)); } async exec(args) { this.assertSessionUsable(); const start = Date.now(); const cwd = this.resolveCommandWorkdir(args.workdir); const logicalCwd = this.logicalWorkdirForPath(args.workdir); const child = await this.spawnShellCommand(this.translateCommandInput(args.cmd), { cwd, logicalCwd, shell: args.shell, login: args.login ?? true, runAs: args.runAs, tty: args.tty ?? false, }); const activeProcess = this.trackChildProcess(child, { tty: args.tty ?? false, }); if (!args.tty) { await waitForProcessOrTimeout(activeProcess, args.yieldTimeMs ?? DEFAULT_EXEC_YIELD_TIME_MS); const processOutput = consumeActiveProcessOutput(activeProcess); if (activeProcess.done) { const output = (0, output_1.truncateOutput)(this.translateCommandOutput(processOutput.output), args.maxOutputTokens); return { output: output.text, stdout: this.translateCommandOutput(processOutput.stdout), stderr: this.translateCommandOutput(processOutput.stderr), wallTimeSeconds: (0, output_1.elapsedSeconds)(start), exitCode: activeProcess.exitCode ?? 1, originalTokenCount: output.originalTokenCount, }; } const output = (0, output_1.truncateOutput)(this.translateCommandOutput(processOutput.output), args.maxOutputTokens); const sessionId = this.allocateProcessId(activeProcess); return { output: output.text, stdout: this.translateCommandOutput(processOutput.stdout), stderr: this.translateCommandOutput(processOutput.stderr), wallTimeSeconds: (0, output_1.elapsedSeconds)(start), sessionId, originalTokenCount: output.originalTokenCount, }; } const yieldTimeMs = args.yieldTimeMs ?? DEFAULT_EXEC_YIELD_TIME_MS; await waitForProcessOrTimeout(activeProcess, yieldTimeMs); if (activeProcess.done) { const processOutput = consumeActiveProcessOutput(activeProcess); const output = (0, output_1.truncateOutput)(this.translateCommandOutput(processOutput.output), args.maxOutputTokens); return { output: output.text, stdout: this.translateCommandOutput(processOutput.stdout), stderr: this.translateCommandOutput(processOutput.stderr), wallTimeSeconds: (0, output_1.elapsedSeconds)(start), exitCode: activeProcess.exitCode ?? 1, originalTokenCount: output.originalTokenCount, }; } const sessionId = this.allocateProcessId(activeProcess); const processOutput = consumeActiveProcessOutput(activeProcess); const output = (0, output_1.truncateOutput)(this.translateCommandOutput(processOutput.output), args.maxOutputTokens); return { output: output.text, stdout: this.translateCommandOutput(processOutput.stdout), stderr: this.translateCommandOutput(processOutput.stderr), wallTimeSeconds: (0, output_1.elapsedSeconds)(start), sessionId, originalTokenCount: output.originalTokenCount, }; } async writeStdin(args) { this.assertSessionUsable(); const activeProcess = this.activeProcesses.get(args.sessionId); if (!activeProcess) { return (0, output_1.formatExecResponse)({ output: `write_stdin failed: session not found: ${args.sessionId}`, wallTimeSeconds: 0, exitCode: 1, }); } const start = Date.now(); const chars = args.chars ?? ''; if (!activeProcess.tty && chars.includes('\u0003')) { activeProcess.child.kill('SIGINT'); } const remainingChars = activeProcess.tty ? chars : chars.split('\u0003').join(''); if (remainingChars.length > 0) { activeProcess.child.stdin.write(remainingChars); } await waitForProcessOrTimeout(activeProcess, args.yieldTimeMs ?? DEFAULT_WRITE_STDIN_YIELD_TIME_MS); const processOutput = consumeActiveProcessOutput(activeProcess); const output = (0, output_1.truncateOutput)(this.translateCommandOutput(processOutput.output), args.maxOutputTokens); if (activeProcess.done) { this.activeProcesses.delete(args.sessionId); return (0, output_1.formatExecResponse)({ output: output.text, wallTimeSeconds: (0, output_1.elapsedSeconds)(start), exitCode: activeProcess.exitCode ?? 1, originalTokenCount: output.originalTokenCount, }); } return (0, output_1.formatExecResponse)({ output: output.text, wallTimeSeconds: (0, output_1.elapsedSeconds)(start), sessionId: args.sessionId, originalTokenCount: output.originalTokenCount, }); } async viewImage(args) { this.assertSessionUsable(); const identity = await this.resolveFilesystemRunAs(args.runAs); const filePath = this.resolveFilesystemPath(args.path); const bytes = await this.files .run({ operation: 'image', path: filePath, limit: media_1.MAX_VIEW_IMAGE_BYTES }, { identity }) .catch((error) => { if (error.code === 'ENOENT') throw new errors_1.UserError(`Image file not found: ${args.path}`); if (error.code === 'EINVAL') throw new errors_1.UserError(`Image path is not a file: ${args.path}`); if (error.code === 'EFBIG') throw new errors_1.UserError(`Image file exceeds the 10 MB limit: ${args.path}`); throw error; }); return (0, media_1.imageOutputFromBytes)(args.path, bytes); } async pathExists(path, runAs) { this.assertSessionUsable(); const identity = await this.resolveFilesystemRunAs(runAs); return JSON.parse((await this.files.run({ operation: 'exists', path: this.resolveFilesystemPath(path), }, { identity })).toString('utf8')); } async directoryExists(path, runAs) { this.assertSessionUsable(); const identity = await this.resolveCommandRunAs(runAs); const resolvedPath = this.resolveFilesystemPath(path); return JSON.parse((await this.files.run({ operation: 'directoryExists', path: resolvedPath }, { identity })).toString('utf8')); } async readFile(args) { this.assertSessionUsable(); const identity = await this.resolveFilesystemRunAs(args.runAs); const bytes = await this.files.run({ operation: 'read', path: this.resolveFilesystemPath(args.path), }, { identity }); if (typeof args.maxBytes === 'number' && bytes.byteLength > args.maxBytes) { return bytes.subarray(0, args.maxBytes); } return bytes; } async listDir(args) { this.assertSessionUsable(); const identity = await this.resolveFilesystemRunAs(args.runAs); const resolved = this.resolveSandboxPathTarget(args.path); const logicalPath = resolved.workspaceRelativePath ?? resolved.path; const entries = JSON.parse((await this.files.run({ operation: 'list', path: this.resolveFilesystemPath(args.path), }, { identity })).toString('utf8')); return entries.map((entry) => ({ name: entry.name, path: logicalPath ? `${logicalPath}/${entry.name}` : entry.name, type: entry.type, })); } async materializeEntry(args) { const entry = structuredClone(args.entry); return await (0, mountSecurity_1.withExclusiveSandboxManifestMutation)(this.state, async () => this.materializeEntryExclusive({ ...args, entry })); } async materializeEntryExclusive(args) { const logicalPath = this.resolveLogicalPath(args.path); const nextManifest = (0, manifestPersistence_1.mergeManifestEntryDelta)(this.state.manifest, logicalPath, args.entry); (0, mountSecurity_1.assertExistingMountTopologyPreserved)(this.state.manifest, nextManifest); (0, mountSecurity_1.validateMountCredentialBoundaries)(nextManifest); const runAs = await this.resolveFilesystemRunAs(args.runAs); await (0, localWorkspace_1.materializeLocalWorkspaceManifestEntry)(this.state.workspaceRootPath, logicalPath, args.entry, { localSourceGrants: this.state.manifest.extraPathGrants, }); if (runAs) { await (0, localWorkspace_1.applyOwnershipRecursive)(this.resolveSandboxPath(args.path), runAs.uid, runAs.gid); } this.state.manifest = (0, manifestPersistence_1.mergeManifestEntryDelta)(this.state.manifest, logicalPath, args.entry); this.captureFilesystemRoots(); (0, mountSecurity_1.captureLiveMountCredentialAuthority)(this.state.manifest); } async applyManifest(manifest, runAs) { const manifestSnapshot = (0, manifest_1.cloneManifest)(manifest); return await (0, mountSecurity_1.withExclusiveSandboxManifestMutation)(this.state, async () => this.applyManifestExclusive(manifestSnapshot, runAs)); } async applyManifestExclusive(manifest, runAs) { const nextManifest = (0, manifestPersistence_1.mergeManifestDelta)(this.state.manifest, manifest); (0, mountSecurity_1.assertExistingMountTopologyPreserved)(this.state.manifest, nextManifest); (0, mountSecurity_1.validateMountCredentialBoundaries)(nextManifest); assertUnixLocalHostPathGrantsUnsupported(manifest); (0, localWorkspace_1.assertLocalWorkspaceManifestMetadataSupported)('UnixLocalSandboxClient', manifest); const identity = await this.resolveFilesystemRunAs(runAs); await (0, localWorkspace_1.materializeLocalWorkspaceManifest)(manifest, this.state.workspaceRootPath); const environment = await manifest.resolveEnvironment(); if (identity) { for (const path of Object.keys(manifest.entries)) { await (0, localWorkspace_1.applyOwnershipRecursive)(this.resolveSandboxPath(path), identity.uid, identity.gid); } } this.state.environment = { ...this.state.environment, ...environment, }; this.state.manifest = (0, manifestPersistence_1.mergeManifestDelta)(this.state.manifest, manifest); this.captureFilesystemRoots(); (0, mountSecurity_1.captureLiveMountCredentialAuthority)(this.state.manifest); } async persistWorkspace() { return await (0, unixLocalFiles_1.withUnixLocalFileAccess)(this.state, async () => { this.assertSessionUsable(); return await (0, workspaceArchive_1.createWorkspaceArchive)(this.state.workspaceRootPath, this.state.manifest.ephemeralPersistencePaths()); }); } async hydrateWorkspace(data, options = {}) { this.assertSessionUsable(); await (0, workspaceArchive_1.restoreWorkspaceArchive)(data, this.state.workspaceRootPath, { archiveLimits: options.archiveLimits === undefined ? this.archiveLimits : options.archiveLimits, }); await this.materializeRestoredWorkspaceMounts(); } async stop() { this.stopPromise ??= this.stopResources().finally(() => { this.stopPromise = undefined; }); await this.stopPromise; } async stopResources() { this.fileOperationGeneration++; if (this.hostFiles) { await this.hostFiles.stop(); await (0, unixLocalFiles_1.withUnixLocalFileAccess)(this.state, async () => { }); } await this.stopActiveProcesses(); } async shutdown() { await this.close(); } async delete() { await this.close(); } async close() { this.closePromise ??= this.closeResources(); await this.closePromise; } async closeResources() { await this.hostFiles?.close(); await this.stop(); if (this.state.workspaceRootOwned) { await (0, promises_1.rm)(this.state.workspaceRootPath, { recursive: true, force: true }); } } async stopActiveProcesses() { await Promise.all([...this.activeProcesses.values()].map(async (activeProcess) => { if (!activeProcess.done) { activeProcess.child.kill('SIGTERM'); await waitForProcessOrTimeout(activeProcess, 250); } if (!activeProcess.done) { activeProcess.child.kill('SIGKILL'); await activeProcess.donePromise.catch(() => { }); } })); this.activeProcesses.clear(); } resolveSandboxPath(path, options = {}) { return this.resolveHostPath(path, options, 'content').accessPath; } resolveFilesystemPath(path, options = {}, mode = 'content') { // Preserve provider overrides that reject container-only paths before host I/O. if (mode === 'content') { this.resolveSandboxPath(path, options); } return this.resolveHostPath(path, options, mode); } resolveHostPath(path, options, mode) { this.assertSessionUsable(); const resolved = this.resolveSandboxPathTarget(path, options); const workspaceRelativePath = resolved.workspaceRelativePath ?? ''; if (resolved.grant) { const authority = this.filesystemRoot(resolved.grant.path); const root = authority.content; return validateResolvedHostPath({ path, accessPath: resolved.path, resolvedPath: (0, node_path_1.resolve)(root, (0, node_path_1.relative)(resolved.grant.path, resolved.path)), allowedRoot: root, entryRoot: authority.entry, preserveLeaf: mode === 'entry', }); } const mountPath = this.resolveLocalBindMountPath(workspaceRelativePath, path, options, mode); if (mountPath) { return mountPath; } const authority = this.filesystemRoot(this.state.workspaceRootPath); const root = authority.content; const resolvedPath = (0, node_path_1.resolve)(root, workspaceRelativePath); const relativeToRoot = (0, node_path_1.relative)(root, resolvedPath); if ((0, hostPath_1.relativeHostPathEscapesRoot)(relativeToRoot)) { throw new errors_1.UserError(`Sandbox path "${path}" escapes the workspace root.`); } return validateResolvedHostPath({ path, accessPath: (0, node_path_1.resolve)(this.state.workspaceRootPath, workspaceRelativePath), resolvedPath, allowedRoot: root, entryRoot: authority.entry, preserveLeaf: mode === 'entry', }); } resolveCommandWorkdir(path) { return this.resolveSandboxPath(path); } assertSessionUsable() { (0, mountSecurity_1.assertSandboxSessionStateUsable)(this.state); } captureFilesystemRoots() { if (!this.hostFiles) return; const paths = [ this.state.workspaceRootPath, ...this.state.manifest.extraPathGrants.map((grant) => grant.path), ...this.state.manifest.mountTargets().flatMap(({ entry }) => { const source = localBindMountSource(entry); return source ? [source] : []; }), ]; for (const path of paths) { if (!this.filesystemRoots.has(path)) { try { this.filesystemRoots.set(path, { content: (0, node_fs_1.realpathSync)(path), entry: (0, node_path_1.resolve)((0, node_fs_1.realpathSync)((0, node_path_1.dirname)(path)), (0, node_path_1.basename)(path)), }); } catch { this.filesystemRoots.set(path, new errors_1.UserError(`Sandbox filesystem root is unavailable: ${path}`)); } } } } filesystemRoot(path) { if (!this.hostFiles) { return { content: realpathForValidation(path), entry: path }; } this.captureFilesystemRoots(); const root = this.filesystemRoots.get(path); if (root instanceof Error) throw root; return root; } async applyFileOperation(operation, runAs) { const generation = this.fileOperationGeneration; const assertCurrent = () => { if (this.closePromise) { throw new errors_1.UserError('UnixLocal file operations are closed.'); } if (this.stopPromise || generation !== this.fileOperationGeneration) { throw new errors_1.UserError('UnixLocal file operation cancelled.'); } }; assertCurrent(); return await (0, unixLocalFiles_1.withUnixLocalFileAccess)(this.state, async () => { assertCurrent(); this.assertSessionUsable(); const identity = await this.resolveFilesystemRunAs(runAs); assertCurrent(); const path = this.resolveFilesystemPath(operation.path, { forWrite: true }, operation.type === 'delete_file' ? 'entry' : 'content'); if (operation.type === 'create_file') { const input = (0, applyDiff_1.applyDiff)('', operation.diff, 'create'); await this.files.run({ operation: 'create', path }, { input, identity }); } else if (operation.type === 'update_file') { const destination = operation.moveTo ? this.resolveFilesystemPath(operation.moveTo, { forWrite: true }) : path; const unlinkPath = operation.moveTo && this.resolveSandboxPath(operation.moveTo, { forWrite: true }) !== this.resolveSandboxPath(operation.path, { forWrite: true }) ? this.resolveFilesystemPath(operation.path, { forWrite: true }, 'entry') : undefined; await this.files.run({ operation: 'update', path, destination, unlinkPath }, { identity, update: (current) => (0, applyDiff_1.applyDiff)(current, operation.diff) }); } else { await this.files.run({ operation: 'delete', path }, { identity }); } }); } resolveSandboxPathTarget(path, options = {}) { return new workspacePaths_1.WorkspacePathPolicy({ root: this.state.manifest.root, extraPathGrants: this.state.manifest.extraPathGrants, }).resolve(path, options); } resolveLocalBindMountPath(workspaceRelativePath, path, options, mode) { for (const { entry, mountPath } of this.state.manifest.mountTargets()) { const source = localBindMountSource(entry); if (!source) { continue; } const mountRelativePath = new workspacePaths_1.WorkspacePathPolicy({ root: this.state.manifest.root, }).resolve(mountPath).workspaceRelativePath; if (typeof mountRelativePath !== 'string') { continue; } if (!pathWithinLogicalRoot(workspaceRelativePath, mountRelativePath)) { continue; } if (options.forWrite && entry.readOnly !== false) { throw new errors_1.UserError(`Sandbox path "${path}" is inside a read-only local bind mount.`); } const childPath = workspaceRelativePath === mountRelativePath ? '' : workspaceRelativePath.slice(mountRelativePath.length + 1); const authority = this.filesystemRoot(source); const root = authority.content; const resolvedPath = childPath ? (0, node_path_1.resolve)(root, childPath) : root; return validateResolvedHostPath({ path, accessPath: childPath ? (0, node_path_1.resolve)(source, childPath) : source, resolvedPath, allowedRoot: root, entryRoot: authority.entry, preserveLeaf: mode === 'entry', }); } return undefined; } resolveLogicalPath(path) { if (!path || path.trim().length === 0) { return ''; } const logicalRoot = this.state.manifest.root; const trimmed = path.trim(); if (trimmed === logicalRoot) { return ''; } if (logicalRoot === '/') { return (0, manifest_1.normalizeRelativePath)(trimmed.startsWith('/') ? trimmed.slice(1) : trimmed); } if (trimmed.startsWith(`${logicalRoot}/`)) { return (0, manifest_1.normalizeRelativePath)(trimmed.slice(logicalRoot.length + 1)); } if (trimmed.startsWith('/')) { throw new errors_1.UserError(`Sandbox path "${path}" must stay within ${logicalRoot}.`); } return (0, manifest_1.normalizeRelativePath)(trimmed); } async spawnShellCommand(command, args) { const { shellPath, flag } = (0, shellCommand_1.resolveLocalShellCommand)({ shell: args.shell, defaultShell: this.defaultShell, envShell: process.env.SHELL, login: args.login, }); const runAs = await this.resolveCommandRunAs(args.runAs); const env = buildCommandEnvironment({ environment: this.state.environment, shellPath, home: this.state.workspaceRootPath, pwd: args.logicalCwd, }); if (args.tty) { return (0, pty_1.spawnInPseudoTerminal)(shellPath, [flag, command], { cwd: args.cwd, env, ...(runAs ? { uid: runAs.uid, gid: runAs.gid, } : {}), }); } return (0, node_child_process_1.spawn)(shellPath, [flag, command], { cwd: args.cwd, env, ...(runAs ? { uid: runAs.uid, gid: runAs.gid, } : {}), stdio: 'pipe', }); } translateCommandInput(command) { // Unix-local is not a chroot; command rewriting preserves manifest-root UX while // filesystem APIs continue to enforce host-path containment separately. if (this.state.manifest.root === '/') { return translateRootManifestCommandInput(command, this.state.workspaceRootPath); } return translateManifestRootCommandInput(command, this.state.manifest.root, this.state.workspaceRootPath); } translateCommandOutput(output) { if (this.state.manifest.root === '/') { return translateRootManifestCommandOutput(output, this.state.workspaceRootPath); } return translateManifestRootCommandOutput(output, this.state.manifest.root, this.state.workspaceRootPath); } async materializeRestoredWorkspaceMounts() { await (0, localWorkspace_1.materializeLocalWorkspaceManifestMounts)(this.state.manifest, this.state.workspaceRootPath); } logicalWorkdirForPath(path) { return this.resolveSandboxPathTarget(path).path; } async resolveCommandRunAs(runAs) { const identity = await this.resolveRunAsIdentity(runAs); if (!identity || identity.isCurrentUser) { return identity; } if (typeof process.getuid !== 'function' || process.getuid() !== 0) { throw new errors_1.UserError(`Unix-local sandbox runAs="${runAs}" requires the host process to run as root or the requested user.`); } return identity; } async resolveFilesystemRunAs(runAs) { const identity = await this.resolveRunAsIdentity(runAs); if (!identity || identity.isCurrentUser) { return identity; } if (typeof process.getuid !== 'function' || process.getuid() !== 0) { throw new errors_1.UserError(`Unix-local sandbox filesystem operations cannot honor runAs="${runAs}" without root privileges.`); } return identity; } async resolveRunAsIdentity(runAs) { if (!runAs || runAs.trim().length === 0) { return undefined; } const requestedUser = runAs.trim(); const currentUser = (0, node_os_1.userInfo)().username; const currentUid = typeof process.getuid === 'function' ? process.getuid() : undefined; const currentGid = typeof process.getgid === 'function' ? process.getgid() : undefined; if (requestedUser === currentUser && currentUid !== undefined && currentGid !== undefined) { return { username: currentUser, uid: currentUid, gid: currentGid, isCurrentUser: true, }; } const [uidResult, gidResult] = await Promise.all([ (0, runProcess_1.runSandboxProcess)('id', ['-u', requestedUser], { timeoutMs: RUN_AS_LOOKUP_TIMEOUT_MS, }), (0, runProcess_1.runSandboxProcess)('id', ['-g', requestedUser], { timeoutMs: RUN_AS_LOOKUP_TIMEOUT_MS, }), ]); if (uidResult.status !== 0 || gidResult.status !== 0) { throw new errors_1.UserError(`Sandbox runAs user "${requestedUser}" could not be resolved on this host.`); } const uid = Number(uidResult.stdout.trim()); const gid = Number(gidResult.stdout.trim()); return { username: requestedUser, uid, gid, isCurrentUser: currentUid !== undefined && currentGid !== undefined && uid === currentUid && gid === currentGid, }; } trackChildProcess(child, options = {}) { let resolveDone = () => { }; const donePromise = new Promise((resolve) => { resolveDone = resolve; }); let resolveOutputClosed = () => { }; const outputClosedPromise = new Promise((resolve) => { resolveOutputClosed = resolve; }); const activeProcess = { child, tty: options.tty ?? false, output: '', stdout: '', stderr: '', droppedOutputChars: 0, droppedStdoutChars: 0, droppedStderrChars: 0, exitCode: null, done: false, donePromise, resolveDone, outputClosedPromise, resolveOutputClosed, }; let openOutputStreams = 2; const markOutputStreamClosed = () => { openOutputStreams -= 1; if (openOutputStreams === 0) { activeProcess.resolveOutputClosed(); } }; child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8'); child.stdout.on('data', (chunk) => { appendActiveProcessOutput(activeProcess, chunk, 'stdout'); }); child.stderr.on('data', (chunk) => { appendActiveProcessOutput(activeProcess, chunk, 'stderr'); }); child.stdout.once('close', markOutputStreamClosed); child.stderr.once('close', markOutputStreamClosed); child.on('close', (code, signal) => { activeProcess.exitCode = code ?? signalToExitCode(signal); activeProcess.done = true; activeProcess.resolveDone(); }); child.on('error', (error) => { appendActiveProcessOutput(activeProcess, `${error.message}\n`, 'stderr'); activeProcess.exitCode = 1; activeProcess.done = true; activeProcess.resolveOutputClosed(); activeProcess.resolveDone(); }); return activeProcess; } allocateProcessId(activeProcess) { const sessionId = this.nextSessionId++; this.activeProcesses.set(sessionId, activeProcess); return sessionId; } } exports.UnixLocalSandboxSession = UnixLocalSandboxSession; /** * Local sandbox client for Unix hosts. * Host file operations require trusted Python 3 with descriptor-relative * filesystem support. Set the host OPENAI_AGENTS_PYTHON to an absolute trusted * executable to override discovery. Unavailable protection and fileIOProtection: * 'off' fail during preparation. Resumed sessions use current client configuration. * File tools honor runAs using host permissions. Editor updates replace files, * require writable parent directories, and leave other hardlinks unchanged. */ class UnixLocalSandboxClient { backendId = 'unix_local'; supportsDefaultOptions = true; options; constructor(options = {}) { this.options = options; } async create(args, manifestOptions) { const createArgs = (0, client_1.normalizeSandboxClientCreateArgs)(args, manifestOptions); const manifest = createArgs.manifest; const resolvedOptions = { ...this.options, ...createArgs.options, ...(createArgs.snapshot ? { snapshot: createArgs.snapshot } : {}), ...(createArgs.concurrencyLimits ? { concurrencyLimits: createArgs.concurrencyLimits } : {}), ...(createArgs.archiveLimits !== undefined ? { archiveLimits: createArgs.archiveLimits } : {}), }; (0, mountSecurity_1.validateMountCredentialBoundaries)(manifest); assertUnixLocalHostPathGrantsUnsupported(manifest); (0, localWorkspace_1.assertLocalWorkspaceManifestMetadataSupported)('UnixLocalSandboxClient', manifest); const files = new unixLocalFiles_1.UnixLocalFiles(resolvedOptions.fileIOProtection); const workspaceRootPath = await (0, promises_1.mkdtemp)((0, node_path_1.join)(resolvedOptions.workspaceBaseDir ?? (0, node_os_1.tmpdir)(), 'openai-agents-sandbox-')); await (0, localWorkspace_1.materializeLocalWorkspaceManifest)(manifest, workspaceRootPath, { concurrencyLimits: resolvedOptions.concurrencyLimits, }); const environment = await manifest.resolveEnvironment(); const configuredExposedPorts = (0, sessionStateValues_1.normalizeExposedPorts)(resolvedOptions.exposedPorts); return new UnixLocalSandboxSession({ [unixLocalFiles_1.preparedFileIO]: files, state: { manifest, workspaceRootPath, workspaceRootOwned: true, environment, snapshotSpec: resolvedOptions.snapshot ?? null, snapshot: null, configuredExposedPorts, }, defaultShell: resolvedOptions.defaultShell, archiveLimits: resolvedOptions.archiveLimits, }); } async resume(state, options = {}) { (0, manifestPersistence_1.assertMountCredentialsRebound)(state); (0, mountSecurity_1.validateMountCredentialBoundaries)(state.manifest); (0, manifestPersistence_1.assertHostPathGrantsRebound)(state); assertUnixLocalHostPathGrantsUnsupported(state.manifest); const archiveLimits = options.archiveLimits === undefined ? this.options.archiveLimits : options.archiveLimits; const currentOptions = ((0, sessionStateTrust_1.getRunStateSessionTrustedConfig)(state) ?.clientOptions ?? options.clientOptions); const files = new unixLocalFiles_1.UnixLocalFiles(currentOptions?.fileIOProtection ?? this.options.fileIOProtection); const restoredState = await this.restoreIfNeeded(state, archiveLimits); return new UnixLocalSandboxSession({ [unixLocalFiles_1.preparedFileIO]: files, state: restoredState, defaultShell: this.options.defaultShell, archiveLimits, }); } async serializeSessionState(state) { (0, mountSecurity_1.assertSandboxSessionStateUsable)(state); const stateGeneration = (0, mountSecurity_1.captureSandboxStateGeneration)(state); assertUnixLocalHostPathGrantsUnsupported(state.manifest); const snapshotSpec = state.snapshotSpec ?? this.options.snapshot ?? null; const snapshot = await (0, localSnapshots_1.persistLocalSnapshot)('UnixLocalSandboxClient', state, snapshotSpec, (capture) => (0, unixLocalFiles_1.withUnixLocalFileAccess)(state, capture)); state.snapshotSpec = snapshotSpec; const serialized = { ...(0, manifestPersistence_1.serializeMountCredentialRedactionMetadata)(state), manifest: (0, manifestPersistence_1.serializeManifest)(state.manifest), workspaceRootPath: state.workspaceRootPath, workspaceRootOwned: state.workspaceRootOwned, environment: (0, manifestPersistence_1.sanitizeEnvironmentForPersistence)(state), snapshotSpec: (0, localSnapshots_1.serializeLocalSnapshotSpec)(snapshotSpec), snapshot, snapshotFingerprint: state.snapshotFingerprint ?? null, snapshotFingerprintVersion: state.snapshotFingerprintVersion ?? null, configuredExposedPorts: state.configuredExposedPorts ?? [], exposedPorts: state.exposedPorts ?? null, }; (0, mountSecurity_1.assertSandboxStateGenerationUnchanged)(state, stateGeneration); return serialized; } async deserializeSessionState(state) { return (0, sessionStateValues_1.deserializeLocalSandboxSessionStateValues)(state, this.options.snapshot); } async restoreIfNeeded(state, archiveLimits) { if (await (0, localWorkspace_1.pathExists)(state.workspaceRootPath)) { if (await (0, localSnapshots_1.canReuseLocalSnapshotWorkspace)(state)) { return state; } if (await (0, localSnapshots_1.localSnapshotIsRestorable)(state)) { return await restoreSnapshotAndMounts(state, state.workspaceRootPath, archiveLimits); } return state; } if (!(await (0, localSnapshots_1.localSnapshotIsRestorable)(state))) { throw new errors_1.UserError('UnixLocal sandbox workspace is unavailable and no local snapshot could be restored.'); } const workspaceRootPath = await (0, promises_1.mkdtemp)((0, node_path_1.join)(this.options.workspaceBaseDir ?? (0, node_os_1.tmpdir)(), 'openai-agents-sandbox-')); return await restoreSnapshotAndMounts({ ...state, workspaceRootPath, workspaceRootOwned: true, }, workspaceRootPath, archiveLimits); } } exports.UnixLocalSandboxClient = UnixLocalSandboxClient; async function restoreSnapshotAndMounts(state, workspaceRootPath, archiveLimits) { const restoredState = await (0, localSnapshots_1.restoreLocalSnapshotToWorkspace)(state, workspaceRootPath, { archiveLimits }); // Restore sticky intent before mounts can expose host-owned directories. await (0, localWorkspace_1.restoreLocalWorkspaceManifestStickyPermissions)(restoredState.manifest, restoredState.workspaceRootPath); await (0, localWorkspace_1.materializeLocalWorkspaceManifestMounts)(restoredState.manifest, restoredState.workspaceRootPath); return restoredState; } function localBindMountSource(entry) { if (entry.type !== 'mount' || typeof entry.source !== 'string') { return undefined; } if (entry.mountStrategy !== undefined && entry.mountStrategy.type !== 'local_bind') { return undefined; } return (0, node_path_1.isAbsolute)(entry.source) ? entry.source : undefined; } function pathWithinLogicalRoot(path, root) { return path === root || path.startsWith(`${root}/`); } function assertUnixLocalHostPathGrantsUnsupported(manifest) { const grant = manifest.extraPathGrants.find(({ hostPath }) => hostPath !== undefined); if (!grant) { return; } throw new errors_2.SandboxUnsupportedFeatureError(`UnixLocalSandboxClient does not support path grant hostPath for "${grant.path}". Omit hostPath when the host and sandbox paths are the same, or use DockerSandboxClient when they differ.`, { provider: 'UnixLocalSandboxClient', feature: 'manifest.extraPathGrants.hostPath', path: grant.path, }); } function validateResolvedHostPath(args) { const allowedRootRealPath = args.allowedRoot; if (args.preserveLeaf && args.resolvedPath !== allowedRootRealPath) { // Unlink owns the leaf entry, so only its parent needs target validation. const parent = realpathForValidation((0, node_path_1.dirname)(args.resolvedPath), args.path); if (!(0, hostPath_1.isHostPathWithinRoot)(allowedRootRealPath, parent)) { throw new errors_1.UserError(`Sandbox path "${args.path}" escapes the workspace root.`); } return { path: (0, node_path_1.resolve)(parent, (0, node_path_1.basename)(args.resolvedPath)), accessPath: args.accessPath, preserveLeaf: true, }; } const existingPath = nearestExistingPath(args.resolvedPath); if (!existingPath) { throw new errors_1.UserError(`Sandbox path "${args.path}" escapes the workspace root.`); } const realPath = realpathForValidation(existingPath, args.path); if (!(0, hostPath_1.isHostPathWithinRoot)(allowedRootRealPath, realPath)) { throw new errors_1.UserError(`Sandbox path "${args.path}" escapes the workspace root.`); } if (args.preserveLeaf) { return { path: args.entryRoot, accessPath: args.accessPath, preserveLeaf: true, rootTarget: allowedRootRealPath, }; } return { path: (0, node_path_1.resolve)(realPath, (0, node_path_1.relative)(existingPath, args.resolvedPath)), accessPath: args.accessPath, preserveLeaf: false, }; } function nearestExistingPath(path) { let current = path; while (true) { try { (0, node_fs_1.lstatSync)(current); return current; } catch (error) { if (error.code !== 'ENOENT') { throw error; } } const parent = (0, node_path_1.dirname)(current); if (parent === current) { return undefined; } current = parent; } } function realpathForValidation(path, originalPath) { try { return (0, node_fs_1.realpathSync)(path); } catch { throw new errors_1.UserError(`Sandbox path "${originalPath}" escapes the workspace root.`); } } class UnixLocalSandboxEditor { session; apply; constructor(session, apply) { this.session = session; this.apply = apply; } canAccessPathForEdit(path) { try { this.session.resolveSandboxPath(path, { forWrite: true }); return true; } catch { return false; } } async createFile(operation) { await this.apply(operation); return {}; } async updateFile(operation) { await this.apply(operation); return {}; } async deleteFile(operation) { await this.apply(operation); return {}; } } function buildCommandEnvironment(args) { return { PATH: DEFAULT_SANDBOX_COMMAND_PATH, HOME: args.home, USER: 'sandbox', LOGNAME: 'sandbox', SHELL: args.shellPath, TMPDIR: '/tmp', ...args.environment, PWD: args.pwd, }; } function translateRootManifestCommandInput(command, workspaceRootPath) { return command.replace(/(^|[\s"'=<>])\/([^\s"'|&;<>(){}]*)/g, (_match, prefix, pathSuffix) => `${prefix}${workspaceRootPath}/${pathSuffix}`); } function translateManifestRootCommandInput(command, manifestRoot, workspaceRootPath) { const escapedManifestRoot = escapeRegExp(manifestRoot); const pathPrefix = String.raw `(^|[\s"'=<>])`; const pathSuffix = String.raw `(?=$|[\/\s"'|&;<>(){}])`; return command.replace(new RegExp(`${pathPrefix}${escapedManifestRoot}${pathSuffix}`, 'g'), (_match, prefix) => `${prefix}${workspaceRootPath}`); } function translateRootManifestCommandOutput(output, workspaceRootPath) { return translateWorkspaceRootCommandOutput(output, '/', workspaceRootPath); } function translateManifestRootCommandOutput(output, manifestRoot, workspaceRootPath) { return translateWorkspaceRootCommandOutput(output, manifestRoot, workspaceRootPath); } function translateWorkspaceRootCommandOutput(output, manifestRoot, workspaceRootPath) { const nestedPathReplacement = manifestRoot === '/' ? '/' : `${manifestRoot}/`; let translated = output; for (const path of workspaceRootOutputPaths(workspaceRootPath)) { translated = translated .split(`${path}/`) .join(nestedPathReplacement) .split(path) .join(manifestRoot); } return translated; } function workspaceRootOutputPaths(workspaceRootPath) { const paths = [workspaceRootPath]; try { paths.push((0, node_fs_1.realpathSync)(workspaceRootPath)); } catch { return paths; } return Array.from(new Set(paths)).sort((a, b) => b.length - a.length); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function appendActiveProcessOutput(activeProcess, chunk, stream) { if (chunk.length === 0) { return; } const combined = appendBoundedOutput(activeProcess.output, activeProcess.droppedOutputChars, chunk); activeProcess.output = combined.output; activeProcess.droppedOutputChars = combined.droppedChars; if (stream === 'stdout') { const stdout = appendBoundedOutput(activeProcess.stdout, activeProcess.droppedStdoutChars, chunk); activeProcess.stdout = stdout.output; activeProcess.droppedStdoutChars = stdout.droppedChars; return; } const stderr = appendBoundedOutput(activeProcess.stderr, activeProcess.droppedStderrChars, chunk); activeProcess.stderr = stderr.output; activeProcess.droppedStderrChars = stderr.droppedChars; } function appendBoundedOutput(output, droppedChars, chunk) { const nextLength = output.length + chunk.length; if (nextLength <= MAX_ACTIVE_PROCESS_OUTPUT_CHARS) { return { output: output + chunk, droppedChars, }; } const nextDroppedChars = droppedChars + nextLength - MAX_ACTIVE_PROCESS_OUTPUT_CHARS; if (chunk.length >= MAX_ACTIVE_PROCESS_OUTPUT_CHARS) { return { output: chunk.slice(-MAX_ACTIVE_PROCESS_OUTPUT_CHARS), droppedChars: nextDroppedChars, }; } const existingCharsToKeep = MAX_ACTIVE_PROCESS_OUTPUT_CHARS - chunk.length; return { output: output.slice(-existingCharsToKeep) + chunk, droppedChars: nextDroppedChars, }; } function consumeActiveProcessOutput(activeProcess) { const output = withDroppedOutputPrefix(activeProcess.output, activeProcess.droppedOutputChars); const stdout = withDroppedOutputPrefix(activeProcess.stdout, activeProcess.droppedStdoutChars); const stderr = withDroppedOutputPrefix(activeProcess.stderr, activeProcess.droppedStderrChars); activeProcess.output = ''; activeProcess.stdout = ''; activeProcess.stderr = ''; activeProcess.droppedOutputChars = 0; activeProcess.droppedStdoutChars = 0; activeProcess.droppedStderrChars = 0; return { output, stdout, stderr }; } function withDroppedOutputPrefix(output, droppedChars) { if (droppedChars === 0) { return output; } return `[...${droppedChars} characters truncated from process output...]\n${output}`; } async function waitForProcessOrTimeout(activeProcess, timeoutMs) { if (activeProcess.done) { await activeProcess.outputClosedPromise; return; } await Promise.race([ activeProcess.donePromise, new Promise((resolve) => setTimeout(resolve, timeoutMs)), ]); if (activeProcess.done) { await activeProcess.outputClosedPromise; } } function signalToExitCode(signal) { if (signal === 'SIGINT') { return 130; } return 1; } //# sourceMappingURL=unixLocal.js.map