@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
JavaScript
;
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