@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
322 lines • 14.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createWorkspaceArchive = createWorkspaceArchive;
exports.restoreWorkspaceArchive = restoreWorkspaceArchive;
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const errors_1 = require("../../../errors.js");
const client_1 = require("../../client.js");
const errors_2 = require("../../errors.js");
const hostPath_1 = require("../../shared/hostPath.js");
const typeGuards_1 = require("../../shared/typeGuards.js");
const snapshotFilesystem_1 = require("./snapshotFilesystem.js");
async function createWorkspaceArchive(sourceRoot, excludedLogicalPaths) {
// Remote SnapshotStore is provider-agnostic, so its portable fallback is a small
// JSON/base64 archive instead of relying on host tar availability.
const archive = {
version: 1,
directories: [],
files: [],
};
await appendWorkspaceArchiveEntries(archive, sourceRoot, excludedLogicalPaths);
return new TextEncoder().encode(JSON.stringify(archive));
}
async function appendWorkspaceArchiveEntries(archive, sourceRoot, excludedLogicalPaths, currentRelativePath = '', expectedSourceStats) {
const entries = await (0, snapshotFilesystem_1.readStableSnapshotDirectoryEntries)(sourceRoot, expectedSourceStats);
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
const relativePath = currentRelativePath
? `${currentRelativePath}/${entry.name}`
: entry.name;
if ((0, snapshotFilesystem_1.shouldSkipSnapshotPath)(relativePath, excludedLogicalPaths)) {
continue;
}
const sourcePath = (0, node_path_1.join)(sourceRoot, entry.name);
const sourceStats = await (0, promises_1.lstat)(sourcePath);
if (sourceStats.isDirectory()) {
archive.directories.push(relativePath);
await appendWorkspaceArchiveEntries(archive, sourcePath, excludedLogicalPaths, relativePath, sourceStats);
continue;
}
if (!sourceStats.isFile()) {
continue;
}
archive.files.push({
path: relativePath,
data: Buffer.from(await (0, snapshotFilesystem_1.readStableSnapshotFile)(sourcePath, sourceStats)).toString('base64'),
});
}
}
async function restoreWorkspaceArchive(data, destinationRoot, options = {}) {
const archive = await prepareWorkspaceArchive(data, destinationRoot, options.archiveLimits);
await (0, snapshotFilesystem_1.clearDirectory)(archive.root);
for (const directory of archive.directories) {
await writeSafeArchiveDirectory(archive.root, directory);
}
for (const file of archive.files) {
await writeSafeArchiveFile(archive.root, file.destination, file.base64);
}
}
async function prepareWorkspaceArchive(data, destinationRoot, limits) {
const bytes = typeof data === 'string'
? new TextEncoder().encode(data)
: workspaceArchiveDataToUint8Array(data);
const archiveLimits = (0, client_1.resolveSandboxArchiveLimits)(limits);
checkWorkspaceArchiveInputBytes(bytes.byteLength, archiveLimits);
const archive = parseWorkspaceArchive(JSON.parse(new TextDecoder().decode(bytes)), destinationRoot, archiveLimits);
await (0, promises_1.mkdir)(destinationRoot, { recursive: true });
await validateWorkspaceArchiveTree(destinationRoot, archive);
return { root: destinationRoot, ...archive };
}
function workspaceArchiveDataToUint8Array(data) {
if (data instanceof Uint8Array) {
return data;
}
return new Uint8Array(data);
}
function checkWorkspaceArchiveInputBytes(actual, limits) {
const limit = limits?.maxInputBytes;
if (limit != null && actual > limit) {
throw new errors_2.SandboxArchiveError('Workspace archive input size exceeds limit.', {
reason: 'archive input size exceeds limit',
limit,
actual,
});
}
}
function parseWorkspaceArchive(archive, destinationRoot, limits) {
if (!(0, typeGuards_1.isRecord)(archive) || archive.version !== 1) {
throw new errors_1.UserError('Unsupported remote snapshot archive version.');
}
if (!Array.isArray(archive.directories) || !Array.isArray(archive.files)) {
throw new errors_1.UserError('Invalid remote snapshot archive members.');
}
const parentSegment = node_path_1.sep === '/' ? /(?:^|\/)\.\.(?:\/|$)/u : /(?:^|[\\/])\.\.(?:[\\/]|$)/u;
let memberCount = 0;
function memberPath(path, kind) {
if (typeof path !== 'string') {
throw new errors_1.UserError('Invalid remote snapshot archive member path.');
}
memberCount += 1;
checkWorkspaceArchiveMemberCount(memberCount, path, limits);
// A directory's optional trailing separator must not lengthen the installation path.
const destination = (0, node_path_1.join)(destinationRoot, path, '.');
if ((0, node_path_1.isAbsolute)(path) ||
path.includes('\0') ||
parentSegment.test(path) ||
(kind === 'file' && (path.endsWith('/') || path.endsWith(node_path_1.sep))) ||
!(0, hostPath_1.isHostPathStrictlyWithinRoot)(destinationRoot, destination)) {
throw new errors_1.UserError(`Invalid remote snapshot archive path: ${path}`);
}
return destination;
}
const directories = archive.directories.map((path) => memberPath(path, 'directory'));
let extractedBytes = 0;
const files = archive.files.map((file) => {
if (!(0, typeGuards_1.isRecord)(file) ||
typeof file.path !== 'string' ||
typeof file.data !== 'string') {
throw new errors_1.UserError('Invalid remote snapshot archive file.');
}
const path = memberPath(file.path, 'file');
if (limits?.maxExtractedBytes != null) {
extractedBytes += Buffer.byteLength(file.data, 'base64');
checkWorkspaceArchiveExtractedBytes(extractedBytes, file.path, limits);
}
return { destination: path, base64: file.data };
});
return { directories, files };
}
async function validateWorkspaceArchiveTree(destinationRoot, archive) {
if (!archive.files.length && !archive.directories.length)
return;
const members = [
...archive.directories.map((path) => ({
path: (0, node_path_1.relative)(destinationRoot, path),
offset: 0,
file: false,
})),
...archive.files.map(({ destination }) => ({
path: (0, node_path_1.relative)(destinationRoot, destination),
offset: 0,
file: true,
})),
];
const groups = [{ members, prefixLength: 0 }];
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRoot, destinationRoot);
const longest = members.reduce((length, member) => Math.max(length, nativePathLength(member.path)), 0);
const scratch = await createArchiveProbeDirectory(destinationRoot, longest);
try {
if (scratch)
await checkArchivePathLength(destinationRoot, scratch, longest);
let longestStored = 0;
while (groups.length) {
const { members, prefixLength } = groups.pop();
let entries;
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRoot, destinationRoot);
try {
entries = await probeSiblingNames(scratch ?? destinationRoot, members);
}
catch (error) {
if (!scratch ||
error.code !== 'ENAMETOOLONG')
throw error;
// Even one extra component can exceed PATH_MAX for a top-level name.
// Use the granted directory itself and remove only markers we created.
entries = await probeSiblingNames(destinationRoot, members);
}
for (const entry of entries) {
const length = prefixLength + nativePathLength(entry.name);
longestStored = Math.max(longestStored, length);
if (entry.children?.length)
groups.push({ members: entry.children, prefixLength: length + 1 });
}
}
// An earlier directory spelling can occupy more bytes than its alias in a later file path.
if (longestStored > longest) {
if (!scratch)
throw new errors_1.UserError('Workspace archive destination exceeds the pathname limit.');
await checkArchivePathLength(destinationRoot, scratch, longestStored);
}
}
finally {
if (scratch)
await (0, promises_1.rmdir)(scratch);
}
}
async function probeSiblingNames(root, members) {
const aliases = new Map();
const names = new Map();
const created = [];
async function probe(name, directory) {
const path = (0, node_path_1.join)(root, name);
let storedName = name;
try {
await (0, promises_1.mkdir)(path, { mode: 0o700 });
created.push(path);
storedName = (0, node_path_1.basename)(await (0, promises_1.realpath)(path));
}
catch (error) {
if (error.code !== 'EEXIST')
throw error;
}
const key = await archiveEntryKey(path);
const existing = aliases.get(key);
if (existing)
return existing;
const entry = { name: storedName, children: directory ? [] : undefined };
aliases.set(key, entry);
return entry;
}
try {
// Process parents in installation order to retain the first native spelling.
for (const member of members) {
const end = member.path.indexOf(node_path_1.sep, member.offset);
const name = member.path.slice(member.offset, end < 0 ? undefined : end);
const directory = !member.file || end >= 0;
let entry = names.get(name);
if (!entry) {
entry = await probe(name, directory);
names.set(name, entry);
}
if (directory !== Boolean(entry.children))
throw new errors_1.UserError(`Conflicting remote snapshot archive path: ${name}`);
if (end >= 0)
entry.children.push({ ...member, offset: end + 1 });
}
return [...aliases.values()];
}
finally {
for (const path of created)
await (0, promises_1.rmdir)(path);
}
}
async function archiveEntryKey(path) {
const info = await (0, promises_1.lstat)(path, { bigint: true });
// Existing hard links have separate names when the workspace is replaced.
const id = `${info.dev}:${info.ino}`;
if (info.isDirectory() || info.nlink === 1n)
return id;
const entries = await (0, promises_1.readdir)((0, node_path_1.dirname)(path));
const name = (0, node_path_1.basename)(path);
if (entries.includes(name))
return `${id}/${name}`;
if (info.isFile()) {
const canonical = (0, node_path_1.basename)(await (0, promises_1.realpath)(path));
if (entries.includes(canonical))
return `${id}/${canonical}`;
}
throw new errors_1.UserError(`Cannot distinguish hard-linked workspace archive paths: ${name}`);
}
function nativePathLength(path) {
return node_path_1.sep === '/' ? Buffer.byteLength(path) : path.length;
}
async function checkArchivePathLength(root, scratch, longest) {
// Probe the longest complete pathname without resolving links in the old workspace.
// The empty directory replaces the first part of the path, so the probe has
// exactly the same native length as the destination even at the host limit.
const remaining = longest - (0, node_path_1.relative)(root, scratch).length;
if (remaining <= 0)
return;
const physical = await (0, promises_1.realpath)(scratch);
const prefix = nativePathLength(physical) > nativePathLength(scratch) ? physical : scratch;
const suffix = (node_path_1.sep + 'x').repeat(Math.floor(remaining / 2)) + (remaining % 2 ? 'x' : '');
try {
await (0, promises_1.lstat)(prefix + suffix);
}
catch (error) {
if (error.code !== 'ENOENT')
throw error;
}
}
async function createArchiveProbeDirectory(root, longest) {
// A single component keeps the probe usable even for deep workspace roots.
for (let index = 0;; index++) {
const path = (0, node_path_1.join)(root, index.toString(36));
try {
await (0, promises_1.mkdir)(path, { mode: 0o700 });
return path;
}
catch (error) {
// All shorter probe names already exist and fit at this root.
if (error.code === 'ENAMETOOLONG' &&
index.toString(36).length > longest)
return undefined;
if (error.code !== 'EEXIST')
throw error;
}
}
}
function checkWorkspaceArchiveMemberCount(actual, member, limits) {
const limit = limits?.maxMembers;
if (limit != null && actual > limit) {
throw new errors_2.SandboxArchiveError('Workspace archive member count exceeds limit.', {
reason: 'archive member count exceeds limit',
limit,
actual,
member,
});
}
}
function checkWorkspaceArchiveExtractedBytes(actual, member, limits) {
const limit = limits?.maxExtractedBytes;
if (limit != null && actual > limit) {
throw new errors_2.SandboxArchiveError('Workspace archive extracted size exceeds limit.', {
reason: 'archive extracted size exceeds limit',
limit,
actual,
member,
});
}
}
async function writeSafeArchiveDirectory(destinationRoot, destinationPath) {
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRoot, destinationPath);
await (0, promises_1.mkdir)(destinationPath, { recursive: true });
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRoot, destinationPath);
}
async function writeSafeArchiveFile(destinationRoot, destinationPath, base64Data) {
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRoot, (0, node_path_1.dirname)(destinationPath));
await (0, promises_1.mkdir)((0, node_path_1.dirname)(destinationPath), { recursive: true });
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRoot, (0, node_path_1.dirname)(destinationPath));
await (0, snapshotFilesystem_1.writeStableSnapshotFile)(destinationRoot, destinationPath, Buffer.from(base64Data, 'base64'));
}
//# sourceMappingURL=workspaceArchive.js.map