UNPKG

@openai/agents-core

Version:

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

322 lines • 14.8 kB
"use strict"; 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