@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
176 lines • 7.04 kB
JavaScript
import { spawn, spawnSync } from 'node:child_process';
import { accessSync, constants, realpathSync } from 'node:fs';
import { isAbsolute, join } from 'node:path';
import { UserError } from "../../../errors.mjs";
import { UNIX_LOCAL_FILE_WORKER } from "./unixLocalFileWorker.mjs";
// Transfer trusted preparation across setup without selecting a backend twice.
export const preparedFileIO = Symbol('preparedFileIO');
const fileAccessTails = new WeakMap();
/** Keep native file changes and snapshot capture separate from state inspection. */
export async function withUnixLocalFileAccess(state, operation) {
const previous = fileAccessTails.get(state) ?? Promise.resolve();
const current = previous.catch(() => { }).then(operation);
fileAccessTails.set(state, current);
try {
return await current;
}
finally {
if (fileAccessTails.get(state) === current)
fileAccessTails.delete(state);
}
}
// Resolve from trusted host configuration, never the manifest's command environment.
function pythonExecutable(mode) {
if (mode === 'off') {
throw new UserError('UnixLocal file I/O protection cannot be disabled. Use a trusted Python 3 installation with descriptor-relative filesystem support.');
}
if (process.platform === 'win32') {
throw new UserError('Required file I/O protection is supported only on Unix hosts.');
}
const configured = process.env.OPENAI_AGENTS_PYTHON;
const candidates = configured
? isAbsolute(configured)
? [configured]
: []
: ['/usr/bin', '/usr/local/bin', '/opt/homebrew/bin'].map((directory) => join(directory, 'python3'));
for (const candidate of candidates) {
try {
const executable = realpathSync(candidate);
accessSync(executable, constants.X_OK);
const probe = spawnSync(executable, [
'-I',
'-S',
'-c',
UNIX_LOCAL_FILE_WORKER,
JSON.stringify({ operation: 'probe' }),
], {
cwd: '/',
env: { PATH: '/usr/bin:/bin' },
timeout: 5000,
maxBuffer: 4096,
encoding: 'utf8',
});
if (!probe.error && probe.status === 0 && probe.stdout === 'ready')
return executable;
}
catch {
// Try the next trusted installation path.
}
}
throw new UserError('Required file I/O protection needs a trusted Python 3 installation with descriptor-relative filesystem support. Set the host OPENAI_AGENTS_PYTHON to an absolute executable path.');
}
/** Owns trusted file workers independently of sandbox shell processes. */
export class UnixLocalFiles {
executable;
constructor(mode = 'required') {
this.executable = pythonExecutable(mode);
}
active = new Set();
closed = false;
async close() {
this.closed = true;
await this.stop();
}
async stop() {
const active = [...this.active];
for (const worker of active)
worker.cancel();
await Promise.all(active.map((worker) => worker.done));
}
async run(request, options = {}) {
if (this.closed)
throw new UserError('UnixLocal file operations are closed.');
const child = spawn(this.executable, [
'-I',
'-S',
'-c',
UNIX_LOCAL_FILE_WORKER,
JSON.stringify({ ...request, identity: options.identity }),
], {
cwd: '/',
env: { PATH: '/usr/bin:/bin' },
stdio: ['pipe', 'pipe', 'pipe'],
});
let primaryError;
let cancelled = false;
const stdout = [];
const stderr = [];
let stderrBytes = 0;
let resolveDone;
const done = new Promise((resolve) => {
resolveDone = resolve;
});
const worker = {
done,
cancel: () => {
if (cancelled)
return;
cancelled = true;
primaryError ??= new UserError('UnixLocal file operation cancelled.');
child.kill('SIGTERM');
},
};
this.active.add(worker);
try {
return await new Promise((resolve, reject) => {
child.stdout.on('data', (chunk) => {
stdout.push(chunk);
});
child.stderr.on('data', (chunk) => {
const remaining = 8192 - stderrBytes;
if (remaining > 0) {
stderr.push(chunk.subarray(0, remaining));
stderrBytes += Math.min(chunk.length, remaining);
}
});
child.stdin.on('error', () => {
// The worker's exit/error owns failures such as a denied open or a closed pipe.
});
child.on('error', (error) => {
primaryError ??= error;
});
if (options.update) {
child.stdout.on('end', () => {
if (primaryError)
return;
const current = Buffer.concat(stdout);
if (!current.subarray(0, 6).equals(Buffer.from('READY\n')))
return;
try {
const next = options.update(current.subarray(6).toString('utf8'));
child.stdin.end(`W${next}`);
}
catch (error) {
primaryError = error;
child.stdin.end();
}
});
}
else {
child.stdin.end(options.input ?? '');
}
child.on('close', (code, signal) => {
if (!primaryError && (code !== 0 || signal)) {
let detail = {};
try {
detail = JSON.parse(Buffer.concat(stderr).toString('utf8'));
}
catch {
/* Preserve a bounded diagnostic below. */
}
primaryError = Object.assign(new Error(`${detail.code ?? 'EIO'}: ${detail.message ?? 'UnixLocal file worker failed. Python 3 with its standard library is required.'}`), { code: detail.code ?? 'EIO', path: request.path.path });
}
if (primaryError)
reject(primaryError);
else
resolve(Buffer.concat(stdout));
});
});
}
finally {
this.active.delete(worker);
resolveDone();
}
}
}
//# sourceMappingURL=unixLocalFiles.mjs.map