UNPKG

@openai/agents-core

Version:

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

940 lines • 42.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.materializeLocalWorkspaceManifest = materializeLocalWorkspaceManifest; exports.assertLocalWorkspaceManifestMetadataSupported = assertLocalWorkspaceManifestMetadataSupported; exports.materializeLocalWorkspaceManifestEntry = materializeLocalWorkspaceManifestEntry; exports.materializeLocalWorkspaceManifestMounts = materializeLocalWorkspaceManifestMounts; exports.restoreLocalWorkspaceManifestStickyPermissions = restoreLocalWorkspaceManifestStickyPermissions; exports.applyOwnershipRecursive = applyOwnershipRecursive; exports.pathExists = pathExists; exports.joinSandboxLogicalPath = joinSandboxLogicalPath; exports.readLocalWorkspaceEntry = readLocalWorkspaceEntry; const errors_1 = require("../../../errors.js"); const errors_2 = require("../../errors.js"); const entries_1 = require("../../entries/index.js"); const manifest_1 = require("../../manifest.js"); const mountSecurity_1 = require("../../mountSecurity.js"); const permissions_1 = require("../../permissions.js"); const workspacePaths_1 = require("../../workspacePaths.js"); const runProcess_1 = require("./runProcess.js"); const promises_1 = require("node:fs/promises"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const node_os_1 = require("node:os"); const hostPath_1 = require("../../shared/hostPath.js"); const pathProbe_1 = require("../../shared/pathProbe.js"); const GIT_VERSION_TIMEOUT_MS = 10_000; const GIT_CLONE_TIMEOUT_MS = 5 * 60_000; const COMMIT_REF_PATTERN = /^[0-9a-fA-F]{7,40}$/; const MATERIALIZATION_FILE_WRITE_FLAGS = node_fs_1.constants.O_WRONLY | node_fs_1.constants.O_CREAT | node_fs_1.constants.O_TRUNC | node_fs_1.constants.O_NOFOLLOW; const LOCAL_SOURCE_FILE_READ_FLAGS = node_fs_1.constants.O_RDONLY | node_fs_1.constants.O_NOFOLLOW; const LOCAL_SOURCE_DIRECTORY_READ_FLAGS = node_fs_1.constants.O_RDONLY | node_fs_1.constants.O_NOFOLLOW | node_fs_1.constants.O_DIRECTORY; async function materializeLocalWorkspaceManifest(manifest, workspaceRootPath, options = {}) { (0, mountSecurity_1.validateMountCredentialBoundaries)(manifest); assertLocalWorkspaceManifestMetadataSupported('Local sandbox materialization', manifest, { supportsMount: options.supportsMount, allowLocalBindMounts: options.allowLocalBindMounts, allowIdentityMetadata: options.allowIdentityMetadata, }); await (0, promises_1.mkdir)(workspaceRootPath, { recursive: true }); await materializeEntries(workspaceRootPath, manifest.entries, '', { ...options, manifestRoot: manifest.root, localSourceGrants: manifest.extraPathGrants, skipMountEntries: true, }); await materializeLocalWorkspaceManifestMounts(manifest, workspaceRootPath, options); } function assertLocalWorkspaceManifestMetadataSupported(providerName, manifest, options = {}) { if (!options.allowIdentityMetadata && manifest.users.length > 0) { throw new errors_2.SandboxUnsupportedFeatureError(`${providerName} does not support manifest users yet.`); } if (!options.allowIdentityMetadata && manifest.groups.length > 0) { throw new errors_2.SandboxUnsupportedFeatureError(`${providerName} does not support manifest groups yet.`); } for (const [logicalPath, entry] of Object.entries(manifest.entries)) { assertLocalWorkspaceEntryMetadataSupported(providerName, logicalPath, entry, options); } } async function materializeLocalWorkspaceManifestEntry(workspaceRootPath, logicalPath, entry, options = {}) { assertLocalWorkspaceEntryMetadataSupported('Local sandbox materialization', logicalPath, entry, { supportsMount: options.supportsMount, allowLocalBindMounts: options.allowLocalBindMounts, allowIdentityMetadata: options.allowIdentityMetadata, }); if ((0, entries_1.isMount)(entry)) { if (options.materializeMount) { await options.materializeMount({ logicalPath, entry }); return; } await materializeLocalBindMountEntry(workspaceRootPath, logicalPath, entry, options); return; } const destination = logicalPath ? (0, node_path_1.resolve)(workspaceRootPath, logicalPath) : workspaceRootPath; switch (entry.type) { case 'dir': await materializeDirEntry(workspaceRootPath, destination, entry, logicalPath, options); break; case 'file': await materializeFileEntry(workspaceRootPath, destination, entry, logicalPath); break; case 'local_file': await materializeLocalFileEntry(workspaceRootPath, destination, entry, logicalPath, options); break; case 'local_dir': await materializeLocalDirEntry(workspaceRootPath, destination, entry, logicalPath, options); break; case 'git_repo': await materializeGitRepoEntry(workspaceRootPath, destination, entry, logicalPath); break; default: throw new errors_1.UserError(`Unsupported sandbox entry type: ${entry.type}`); } await applyEntryPermissions(workspaceRootPath, destination, entry, logicalPath); } async function materializeLocalWorkspaceManifestMounts(manifest, workspaceRootPath, options = {}) { (0, mountSecurity_1.validateMountCredentialBoundaries)(manifest); assertLocalWorkspaceManifestMetadataSupported('Local sandbox materialization', manifest, { supportsMount: options.supportsMount, allowLocalBindMounts: options.allowLocalBindMounts, allowIdentityMetadata: options.allowIdentityMetadata, }); for (const { logicalPath, entry, } of manifest.mountTargetsForMaterialization()) { await materializeLocalWorkspaceManifestEntry(workspaceRootPath, logicalPath, entry, { ...options, manifestRoot: manifest.root, localSourceGrants: manifest.extraPathGrants, }); } } /** Reapply manifest sticky intent without replacing snapshot contents or modes. */ async function restoreLocalWorkspaceManifestStickyPermissions(manifest, workspaceRootPath) { for (const { logicalPath, entry } of manifest.iterEntries()) { if ((0, entries_1.isMount)(entry) || !(0, permissions_1.permissionsForSandboxEntry)(entry.permissions).sticky) { continue; } const destination = (0, node_path_1.resolve)(workspaceRootPath, logicalPath); const info = await (0, promises_1.lstat)(destination).catch((error) => { // A snapshot may have replaced a manifest ancestor with a file. if ((0, pathProbe_1.isSandboxPathNotFoundError)(error) || error.code === 'ENOTDIR') { return null; } throw error; }); if (info?.isDirectory()) { await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); const handle = await (0, promises_1.open)(destination, LOCAL_SOURCE_DIRECTORY_READ_FLAGS); try { const openedInfo = await handle.stat(); if (!openedInfo.isDirectory() || !sameFilesystemEntry(info, openedInfo)) { throw new errors_1.UserError(`Sandbox sticky permission target changed during restore: ${logicalPath || '.'}`); } await handle.chmod((openedInfo.mode & 0o7777) | 0o1000); } finally { await handle.close(); } } } } async function applyOwnershipRecursive(targetPath, uid, gid) { const info = await (0, promises_1.lstat)(targetPath).catch((error) => { if ((0, pathProbe_1.isSandboxPathNotFoundError)(error)) { return null; } throw error; }); if (!info) { return; } if (info.isSymbolicLink()) { return; } await (0, promises_1.chown)(targetPath, uid, gid); if (!info.isDirectory()) { return; } const children = await (0, promises_1.readdir)(targetPath); await Promise.all(children.map(async (child) => { await applyOwnershipRecursive((0, node_path_1.join)(targetPath, child), uid, gid); })); } async function pathExists(path) { try { await (0, promises_1.stat)(path); return true; } catch (error) { if (!(0, pathProbe_1.isSandboxPathNotFoundError)(error)) { throw error; } let info; try { info = await (0, promises_1.lstat)(path); } catch (probeError) { if ((0, pathProbe_1.isSandboxPathNotFoundError)(probeError)) { return false; } throw probeError; } if (info.isSymbolicLink()) { try { await (0, promises_1.stat)(path); return true; } catch (retryError) { if ((0, pathProbe_1.isSandboxPathNotFoundError)(retryError)) { return false; } throw retryError; } } return true; } } function joinSandboxLogicalPath(root, logicalPath) { if (!logicalPath) { return root; } return root === '/' ? `/${logicalPath}` : `${root}/${logicalPath}`; } function assertLocalWorkspaceEntryMetadataSupported(providerName, logicalPath, entry, options = {}) { const displayPath = logicalPath || '.'; if ((0, entries_1.isMount)(entry)) { if (options.supportsMount?.(entry) || (options.allowLocalBindMounts !== false && isSupportedLocalBindMount(entry))) { return; } throw new errors_2.SandboxUnsupportedFeatureError(`${providerName} does not support this mount entry: ${displayPath}`); } if (entry.group !== undefined) { throw new errors_2.SandboxUnsupportedFeatureError(`${providerName} does not support sandbox entry group ownership yet: ${displayPath}`); } if (entry.type !== 'dir' || !entry.children) { return; } for (const [childPath, childEntry] of Object.entries(entry.children)) { assertLocalWorkspaceEntryMetadataSupported(providerName, joinLogicalPath(logicalPath, childPath), childEntry, options); } } async function materializeEntries(workspaceRootPath, entries, prefix = '', options = {}) { await runLimited(Object.entries(entries), resolveManifestEntryConcurrency(options.concurrencyLimits), async ([path, entry]) => { if (options.skipMountEntries && (0, entries_1.isMount)(entry)) { return; } const logicalPath = prefix ? `${prefix}/${(0, manifest_1.normalizeRelativePath)(path)}` : (0, manifest_1.normalizeRelativePath)(path); await materializeLocalWorkspaceManifestEntry(workspaceRootPath, logicalPath, entry, options); }); } async function materializeLocalBindMountEntry(workspaceRootPath, logicalPath, entry, options) { if (!isSupportedLocalBindMount(entry)) { throw new errors_2.SandboxUnsupportedFeatureError(`Local sandbox materialization only supports mount entries with an absolute source and localBindMountStrategy(): ${logicalPath || '.'}`); } if (entry.readOnly !== false) { throw new errors_2.SandboxUnsupportedFeatureError(`Local sandbox materialization cannot enforce read-only local bind mounts: ${logicalPath || '.'}`); } // A local bind mount is implemented as a symlink, so read-only semantics cannot be // enforced by this materializer itself. const source = await (0, promises_1.realpath)(entry.source).catch(() => { throw new errors_1.UserError(`Local bind mount source does not exist: ${entry.source}`); }); const sourceInfo = await (0, promises_1.lstat)(source); if (!sourceInfo.isDirectory()) { throw new errors_1.UserError(`Local bind mount source must be a directory: ${source}`); } const mountLogicalPath = resolveMountLogicalPath(options.manifestRoot ?? '/workspace', logicalPath, entry.mountPath); if (!mountLogicalPath) { throw new errors_2.SandboxUnsupportedFeatureError('Local sandbox materialization does not support mounting over the workspace root.'); } const destination = (0, node_path_1.resolve)(workspaceRootPath, mountLogicalPath); await createMaterializationParentDirectory(workspaceRootPath, destination, mountLogicalPath); await (0, promises_1.rm)(destination, { recursive: true, force: true }); await (0, promises_1.symlink)(source, destination, 'dir'); } function isSupportedLocalBindMount(entry) { return (entry.type === 'mount' && typeof entry.source === 'string' && (0, node_path_1.isAbsolute)(entry.source) && (entry.mountStrategy === undefined || entry.mountStrategy.type === 'local_bind')); } function resolveMountLogicalPath(root, logicalPath, mountPath) { if (mountPath === undefined) { return (0, manifest_1.normalizeRelativePath)(logicalPath); } const resolved = new workspacePaths_1.WorkspacePathPolicy({ root }).resolve(mountPath, { forWrite: true, }); if (typeof resolved.workspaceRelativePath !== 'string') { throw new errors_1.UserError(`Mount path "${mountPath}" escapes the workspace root.`); } return (0, manifest_1.normalizeRelativePath)(resolved.workspaceRelativePath); } async function createMaterializationDirectory(workspaceRootPath, destination, logicalPath) { await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); await (0, promises_1.mkdir)(destination, { recursive: true }); await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); } async function createMaterializationParentDirectory(workspaceRootPath, destination, logicalPath) { const parent = (0, node_path_1.dirname)(destination); await assertSafeMaterializationPath(workspaceRootPath, parent, logicalPath); await (0, promises_1.mkdir)(parent, { recursive: true }); await assertSafeMaterializationPath(workspaceRootPath, parent, logicalPath); await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); } async function assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath) { // Check after directory creation and again before writes so a concurrent symlink swap // cannot redirect manifest materialization outside the workspace root. const workspaceRootRealPath = await (0, promises_1.realpath)(workspaceRootPath).catch(() => { throw materializationEscapesWorkspaceError(logicalPath); }); const relativeDestination = (0, node_path_1.relative)(workspaceRootPath, destination); if ((0, hostPath_1.relativeHostPathEscapesRoot)(relativeDestination)) { throw materializationEscapesWorkspaceError(logicalPath); } if (relativeDestination === '') { return; } let current = workspaceRootPath; for (const segment of relativeDestination.split(node_path_1.sep)) { if (!segment) { continue; } current = (0, node_path_1.join)(current, segment); const info = await (0, promises_1.lstat)(current).catch((error) => { if (error.code === 'ENOENT') { return undefined; } throw error; }); if (!info) { return; } if (info.isSymbolicLink()) { throw new errors_1.UserError(`Sandbox materialization path "${logicalPath || '.'}" escapes the workspace root through a symbolic link.`); } const currentRealPath = await (0, promises_1.realpath)(current).catch(() => { throw materializationEscapesWorkspaceError(logicalPath); }); if (!(0, hostPath_1.isHostPathWithinRoot)(workspaceRootRealPath, currentRealPath)) { throw materializationEscapesWorkspaceError(logicalPath); } } } async function materializeDirEntry(workspaceRootPath, destination, entry, logicalPath, options) { await createMaterializationDirectory(workspaceRootPath, destination, logicalPath); if (entry.children) { await materializeEntries(workspaceRootPath, entry.children, logicalPath, options); } } async function materializeFileEntry(workspaceRootPath, destination, entry, logicalPath) { await createMaterializationParentDirectory(workspaceRootPath, destination, logicalPath); await writeMaterializationFile(workspaceRootPath, destination, logicalPath, entry.content); } async function materializeLocalFileEntry(workspaceRootPath, destination, entry, logicalPath, options) { await createMaterializationParentDirectory(workspaceRootPath, destination, logicalPath); await writeMaterializationFile(workspaceRootPath, destination, logicalPath, await readStableLocalFile(resolveLocalSourcePath('local_file', entry.src, options))); } async function materializeLocalDirEntry(workspaceRootPath, destination, entry, logicalPath, options) { await copyLocalDirectory(resolveLocalSourcePath('local_dir', entry.src, options), destination, options, workspaceRootPath, logicalPath); } function resolveLocalSourcePath(entryType, sourcePath, options) { const base = (0, node_path_1.resolve)(options.localSourceBaseDir ?? process.cwd()); const resolvedSourcePath = (0, node_path_1.resolve)(base, sourcePath); if ((0, hostPath_1.isHostPathWithinRoot)(base, resolvedSourcePath) || (options.localSourceGrants ?? []).some((grant) => (0, hostPath_1.isHostPathWithinRoot)((0, node_path_1.resolve)((0, hostPath_1.sandboxPathGrantHostPath)(grant)), resolvedSourcePath))) { return resolvedSourcePath; } throw new errors_1.UserError(`${entryType} source must stay within the local source base directory or manifest.extraPathGrants: ${resolvedSourcePath} (base: ${base})`); } /** Read configured resources without opening any live workspace destination. */ async function* readLocalWorkspaceEntry(entry, localSourceGrants, privateGitRoot) { assertLocalWorkspaceEntryMetadataSupported('Local sandbox materialization', '', entry, { allowLocalBindMounts: false }); yield* readLocalWorkspaceEntryContents(entry, '', localSourceGrants, privateGitRoot); } async function* readLocalWorkspaceEntryContents(entry, path, localSourceGrants, privateGitRoot) { const permissions = (0, permissions_1.permissionsForSandboxEntry)(entry.permissions).toMode() & 0o1777; switch (entry.type) { case 'dir': yield { type: 'dir', path, permissions }; for (const [childPath, child] of Object.entries(entry.children ?? {})) { yield* readLocalWorkspaceEntryContents(child, node_path_1.posix.join(path, (0, manifest_1.normalizeRelativePath)(childPath)), localSourceGrants, privateGitRoot); } return; case 'file': yield { type: 'file', path, permissions, content: Buffer.from(entry.content), }; return; case 'local_file': yield { type: 'file', path, permissions, content: await readStableLocalFile(resolveLocalSourcePath('local_file', entry.src, { localSourceGrants, })), }; return; case 'local_dir': for await (const item of readLocalDirectoryContents(resolveLocalSourcePath('local_dir', entry.src, { localSourceGrants }), '')) { yield { ...item, path: node_path_1.posix.join(path, item.path), ...(item.path === '' ? { permissions } : {}), }; } return; case 'git_repo': { if (privateGitRoot === undefined) { throw new errors_1.UserError('Git resources require a private temporary directory.'); } await assertGitAvailable(); const temporary = await (0, promises_1.mkdtemp)((0, node_path_1.join)(privateGitRoot, 'checkout-')); try { const { result, commitFetchError } = await cloneGitRepo(normalizeGitRepository(entry), temporary, entry.ref); if (result.status !== 0) { throw new errors_1.UserError(`Failed to materialize git_repo entry ${entry.repo}: ${(0, runProcess_1.formatSandboxProcessError)(commitFetchError ?? result)}`); } const source = await resolveGitRepoSource(temporary, entry); const copiesTree = Boolean(entry.subpath || (entry.ref && looksLikeCommitRef(entry.ref))); const info = await (0, promises_1.lstat)(source); if (copiesTree && info.isFile()) { yield { type: 'file', path, permissions, content: await (0, promises_1.readFile)(source), }; return; } if (!info.isDirectory()) { throw new errors_1.UserError(`git_repo subpath must resolve to a file or directory: ${entry.subpath}`); } // SHA/subpath copies retain existing target modes; direct clones retain Git modes. const contents = copiesTree ? readLocalDirectoryContents(source, '') : readClonedGitContent(source, ''); for await (const item of contents) { yield { ...item, path: node_path_1.posix.join(path, item.path), ...(item.path === '' ? { permissions } : {}), ...(item.path === '' && item.type === 'dir' && !copiesTree ? { mustBeEmpty: true } : {}), }; } } finally { await (0, promises_1.rm)(temporary, { recursive: true, force: true }); } return; } default: throw new errors_2.SandboxUnsupportedFeatureError('Mount entries must be configured through the sandbox mount APIs.'); } } async function* readClonedGitContent(source, path) { const info = await (0, promises_1.lstat)(source); const permissions = info.mode & 0o1777; if (info.isSymbolicLink()) { yield { type: 'symlink', path, target: await (0, promises_1.readlink)(source) }; } else if (info.isFile()) { const handle = await (0, promises_1.open)(source, LOCAL_SOURCE_FILE_READ_FLAGS); try { const opened = await handle.stat(); if (!opened.isFile() || !sameFilesystemEntry(opened, info)) { throw new errors_1.UserError('git_repo file changed while materializing.'); } yield { type: 'file', path, permissions, size: opened.size, chunks: readClonedGitFileChunks(handle, opened.size), }; } finally { await handle.close(); } } else if (info.isDirectory()) { yield { type: 'dir', path, permissions }; for await (const child of await (0, promises_1.opendir)(source)) { yield* readClonedGitContent((0, node_path_1.join)(source, child.name), node_path_1.posix.join(path, child.name)); } } } async function* readClonedGitFileChunks(handle, remaining) { while (remaining > 0) { const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, remaining)); const { bytesRead } = await handle.read(buffer, 0, buffer.length, null); if (bytesRead === 0) { throw new errors_1.UserError('git_repo file changed while materializing.'); } remaining -= bytesRead; yield buffer.subarray(0, bytesRead); } } async function* readLocalDirectoryContents(sourceDir, relativePath, expectedSourceStat) { const source = await resolveStableLocalDirSource(sourceDir, expectedSourceStat); yield { type: 'dir', path: relativePath }; for await (const child of await (0, promises_1.opendir)(sourceDir)) { await assertStableLocalDirPath(sourceDir, source.stat); const sourcePath = (0, node_path_1.join)(sourceDir, child.name); const path = relativePath ? `${relativePath}/${child.name}` : child.name; if (child.isDirectory()) { const childStat = await assertStableLocalDirChild(source.root, sourcePath); yield* readLocalDirectoryContents(sourcePath, path, childStat); } else if (child.isFile()) { yield { type: 'file', path, content: await readStableLocalDirFile(source.root, sourcePath), }; } else if (child.isSymbolicLink()) { throw new errors_1.UserError(`local_dir entries do not support symbolic links: ${sourcePath}`); } else { throw new errors_1.UserError(`local_dir entries do not support this file type: ${sourcePath}`); } } await assertStableLocalDirPath(sourceDir, source.stat); } async function copyLocalDirectory(sourceDir, destination, options, workspaceRootPath, logicalPath, expectedSourceStat) { const source = await resolveStableLocalDirSource(sourceDir, expectedSourceStat); await createMaterializationDirectory(workspaceRootPath, destination, logicalPath); const children = await readStableLocalDirEntries(sourceDir, source.stat); await runLimited(children, resolveLocalDirEntryConcurrency(options.concurrencyLimits), async (child) => { const sourcePath = (0, node_path_1.join)(sourceDir, child.name); const destinationPath = (0, node_path_1.join)(destination, child.name); const childLogicalPath = joinLogicalPath(logicalPath, child.name); if (child.isDirectory()) { const childSourceStat = await assertStableLocalDirChild(source.root, sourcePath); await copyLocalDirectory(sourcePath, destinationPath, options, workspaceRootPath, childLogicalPath, childSourceStat); return; } if (child.isFile()) { await writeMaterializationFile(workspaceRootPath, destinationPath, childLogicalPath, await readStableLocalDirFile(source.root, sourcePath)); return; } if (child.isSymbolicLink()) { throw new errors_1.UserError(`local_dir entries do not support symbolic links: ${sourcePath}`); } }); } async function writeMaterializationFile(workspaceRootPath, destination, logicalPath, content) { await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); let handle; try { handle = await (0, promises_1.open)(destination, MATERIALIZATION_FILE_WRITE_FLAGS); } catch (error) { if (isPathChangedError(error)) { throw materializationEscapesWorkspaceError(logicalPath); } throw error; } try { await handle.writeFile(content); } finally { await handle.close(); } await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); } async function resolveStableLocalDirSource(sourceDir, expectedSourceStat) { await assertNoLocalSourceSymlinkAncestors(sourceDir, 'local_dir', localDirPathChangedError); const sourceStat = await statStableLocalSourcePath(sourceDir); if (sourceStat.isSymbolicLink()) { throw new errors_1.UserError(`local_dir entries do not support symbolic links: ${sourceDir}`); } if (!sourceStat.isDirectory()) { throw new errors_1.UserError(`local_dir source must be a directory: ${sourceDir}`); } if (expectedSourceStat && !sameFilesystemEntry(sourceStat, expectedSourceStat)) { throw localDirPathChangedError(sourceDir); } const sourceRoot = await realpathStableLocalSourcePath(sourceDir); let handle; try { handle = await (0, promises_1.open)(sourceDir, LOCAL_SOURCE_DIRECTORY_READ_FLAGS); } catch (error) { if (isPathChangedError(error)) { throw localDirPathChangedError(sourceDir); } throw error; } try { const openedStat = await handle.stat(); if (!openedStat.isDirectory() || !sameFilesystemEntry(openedStat, sourceStat)) { throw localDirPathChangedError(sourceDir); } return { root: sourceRoot, stat: openedStat }; } finally { await handle.close(); } } async function readStableLocalDirEntries(sourceDir, expectedStat) { let children; try { children = await (0, promises_1.readdir)(sourceDir, { withFileTypes: true }); } catch (error) { if (isPathChangedError(error)) { throw localDirPathChangedError(sourceDir); } throw error; } await assertStableLocalDirPath(sourceDir, expectedStat); return children; } async function assertStableLocalDirPath(sourcePath, expectedStat) { const currentStat = await statStableLocalSourcePath(sourcePath); if (currentStat.isSymbolicLink() || !currentStat.isDirectory() || !sameFilesystemEntry(currentStat, expectedStat)) { throw localDirPathChangedError(sourcePath); } } async function readStableLocalFile(sourcePath) { await assertNoLocalSourceSymlinkAncestors(sourcePath, 'local_file', localFilePathChangedError); const sourceStat = await statStableLocalSourcePath(sourcePath, localFilePathChangedError); if (sourceStat.isSymbolicLink()) { throw new errors_1.UserError(`local_file entries do not support symbolic links: ${sourcePath}`); } if (!sourceStat.isFile()) { throw localFilePathChangedError(sourcePath); } let handle; try { handle = await (0, promises_1.open)(sourcePath, LOCAL_SOURCE_FILE_READ_FLAGS); } catch (error) { if (isPathChangedError(error)) { throw localFilePathChangedError(sourcePath); } throw error; } try { const openedStat = await handle.stat(); if (!openedStat.isFile() || !sameFilesystemEntry(openedStat, sourceStat)) { throw localFilePathChangedError(sourcePath); } return await handle.readFile(); } finally { await handle.close(); } } async function assertNoLocalSourceSymlinkAncestors(sourcePath, entryType, pathChangedError) { const resolvedPath = (0, node_path_1.resolve)(sourcePath); let current = (0, node_path_1.dirname)(resolvedPath); while (current !== (0, node_path_1.dirname)(current)) { const parent = (0, node_path_1.dirname)(current); if (parent === (0, node_path_1.dirname)(parent)) { break; } let currentStat; try { currentStat = await (0, promises_1.lstat)(current); } catch (error) { if (isPathChangedError(error)) { throw pathChangedError(sourcePath); } throw error; } if (currentStat.isSymbolicLink()) { throw new errors_1.UserError(`${entryType} entries do not support symbolic link ancestors: ${sourcePath}`); } current = parent; } } async function readStableLocalDirFile(sourceRoot, sourcePath) { const sourceStat = await statStableLocalSourcePath(sourcePath); if (sourceStat.isSymbolicLink()) { throw new errors_1.UserError(`local_dir entries do not support symbolic links: ${sourcePath}`); } if (!sourceStat.isFile()) { throw localDirPathChangedError(sourcePath); } const resolvedSourcePath = await realpathStableLocalSourcePath(sourcePath); if (!(0, hostPath_1.isHostPathStrictlyWithinRoot)(sourceRoot, resolvedSourcePath)) { throw localDirPathChangedError(sourcePath); } let handle; try { handle = await (0, promises_1.open)(sourcePath, LOCAL_SOURCE_FILE_READ_FLAGS); } catch (error) { if (isPathChangedError(error)) { throw localDirPathChangedError(sourcePath); } throw error; } try { const openedStat = await handle.stat(); if (!openedStat.isFile() || !sameFilesystemEntry(openedStat, sourceStat)) { throw localDirPathChangedError(sourcePath); } return await handle.readFile(); } finally { await handle.close(); } } async function statStableLocalSourcePath(sourcePath, pathChangedError = localDirPathChangedError) { try { return await (0, promises_1.lstat)(sourcePath); } catch (error) { if (isPathChangedError(error)) { throw pathChangedError(sourcePath); } throw error; } } async function realpathStableLocalSourcePath(sourcePath, pathChangedError = localDirPathChangedError) { try { return await (0, promises_1.realpath)(sourcePath); } catch (error) { if (isPathChangedError(error)) { throw pathChangedError(sourcePath); } throw error; } } function localDirPathChangedError(sourcePath) { return new errors_1.UserError(`local_dir entry path changed while materializing: ${sourcePath}`); } function localFilePathChangedError(sourcePath) { return new errors_1.UserError(`local_file entry path changed while materializing: ${sourcePath}`); } function isPathChangedError(error) { return (typeof error === 'object' && error !== null && 'code' in error && (error.code === 'ELOOP' || error.code === 'ENOENT' || error.code === 'ENOTDIR')); } async function assertStableLocalDirChild(sourceRoot, sourcePath) { // local_dir deliberately rejects symlinks so a host-side source cannot smuggle // files outside the declared directory into the sandbox. const sourceStat = await statStableLocalSourcePath(sourcePath); if (sourceStat.isSymbolicLink()) { throw new errors_1.UserError(`local_dir entries do not support symbolic links: ${sourcePath}`); } if (!sourceStat.isDirectory()) { throw localDirPathChangedError(sourcePath); } const resolvedSourcePath = await realpathStableLocalSourcePath(sourcePath); if (!(0, hostPath_1.isHostPathStrictlyWithinRoot)(sourceRoot, resolvedSourcePath)) { throw localDirPathChangedError(sourcePath); } return sourceStat; } function sameFilesystemEntry(left, right) { return left.dev === right.dev && left.ino === right.ino; } async function materializeGitRepoEntry(workspaceRootPath, destination, entry, logicalPath) { await assertGitAvailable(); const repository = normalizeGitRepository(entry); const cloneIntoTemporaryDirectory = Boolean(entry.subpath) || Boolean(entry.ref && looksLikeCommitRef(entry.ref)); const cloneDestination = cloneIntoTemporaryDirectory ? await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'openai-agents-git-repo-')) : destination; try { if (cloneIntoTemporaryDirectory) { await (0, promises_1.mkdir)((0, node_path_1.dirname)(cloneDestination), { recursive: true }); } else { await createMaterializationParentDirectory(workspaceRootPath, cloneDestination, logicalPath); } const { result: cloneResult, commitFetchError } = await cloneGitRepo(repository, cloneDestination, entry.ref); if (cloneResult.status !== 0) { throw new errors_1.UserError(`Failed to materialize git_repo entry ${entry.repo}: ${(0, runProcess_1.formatSandboxProcessError)(commitFetchError ?? cloneResult)}`); } if (!cloneIntoTemporaryDirectory) { await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); return; } const sourcePath = await resolveGitRepoSource(cloneDestination, entry); const sourceInfo = await (0, promises_1.lstat)(sourcePath); if (sourceInfo.isDirectory()) { await copyLocalDirectory(sourcePath, destination, {}, workspaceRootPath, logicalPath); return; } if (sourceInfo.isFile()) { await createMaterializationParentDirectory(workspaceRootPath, destination, logicalPath); await writeMaterializationFile(workspaceRootPath, destination, logicalPath, await (0, promises_1.readFile)(sourcePath)); return; } throw new errors_1.UserError(`git_repo subpath must resolve to a file or directory: ${entry.subpath}`); } finally { if (cloneIntoTemporaryDirectory) { await (0, promises_1.rm)(cloneDestination, { recursive: true, force: true }); } } } async function resolveGitRepoSource(cloneRoot, entry) { const source = entry.subpath ? (0, node_path_1.resolve)(cloneRoot, entry.subpath) : cloneRoot; if (entry.subpath && !(0, hostPath_1.isHostPathStrictlyWithinRoot)(await (0, promises_1.realpath)(cloneRoot), await (0, promises_1.realpath)(source))) { throw new errors_1.UserError(`git_repo subpath escapes the cloned repository: ${entry.subpath}`); } return source; } async function assertGitAvailable() { const gitVersion = await (0, runProcess_1.runSandboxProcess)('git', ['--version'], { timeoutMs: GIT_VERSION_TIMEOUT_MS, }); if (gitVersion.status !== 0) { throw new errors_1.UserError('git_repo entries require a local `git` executable, but `git` was not found.'); } } async function cloneGitRepo(repository, destination, ref) { if (!ref || !looksLikeCommitRef(ref)) { return { result: await cloneNamedRef(repository, destination, ref) }; } // Branch/tag shallow clone does not reliably fetch arbitrary SHAs, so commit-looking // refs take the explicit init/fetch/checkout path first. const result = await fetchCommitRef(repository, destination, ref); if (result.status === 0) { return { result }; } await (0, promises_1.rm)(destination, { recursive: true, force: true }); return { result: await cloneNamedRef(repository, destination, ref), commitFetchError: result, }; } async function cloneNamedRef(repository, destination, ref) { const args = ['clone', '--depth', '1']; if (ref) { args.push('--branch', ref); } args.push('--', repository, destination); return await (0, runProcess_1.runSandboxProcess)('git', args, { timeoutMs: GIT_CLONE_TIMEOUT_MS, }); } async function fetchCommitRef(repository, destination, ref) { const steps = [ ['init', destination], ['-C', destination, 'remote', 'add', 'origin', repository], ['-C', destination, 'fetch', '--depth', '1', '--no-tags', 'origin', ref], ['-C', destination, 'checkout', '--detach', 'FETCH_HEAD'], ]; for (const args of steps) { const result = await (0, runProcess_1.runSandboxProcess)('git', args, { timeoutMs: GIT_CLONE_TIMEOUT_MS, }); if (result.status !== 0) { return result; } } return { status: 0, signal: null, stdout: '', stderr: '', timedOut: false, }; } function looksLikeCommitRef(ref) { return COMMIT_REF_PATTERN.test(ref); } async function runLimited(items, limit, fn) { if (items.length === 0) { return; } if (limit <= 1) { for (const item of items) { await fn(item); } return; } let nextIndex = 0; // This intentionally stays as a tiny work queue rather than adding a dependency for // the few manifest and local_dir concurrency limits used here. const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { while (nextIndex < items.length) { const item = items[nextIndex++]; await fn(item); } }); await Promise.all(workers); } function resolveManifestEntryConcurrency(limits) { return normalizeConcurrencyLimit(limits?.manifestEntries, 4); } function resolveLocalDirEntryConcurrency(limits) { return normalizeConcurrencyLimit(limits?.localDirFiles, 4); } function normalizeConcurrencyLimit(value, fallback) { if (value === undefined) { return fallback; } if (!Number.isFinite(value) || value < 1) { throw new errors_1.UserError('Sandbox concurrency limits must be positive numbers.'); } return Math.floor(value); } function joinLogicalPath(parent, child) { const normalizedChild = (0, manifest_1.normalizeRelativePath)(child); if (!parent || parent === '.') { return normalizedChild; } let parentEnd = parent.length; while (parentEnd > 0 && parent[parentEnd - 1] === '/') { parentEnd--; } return `${parent.slice(0, parentEnd)}/${normalizedChild}`; } async function applyEntryPermissions(workspaceRootPath, destination, entry, logicalPath) { await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); const permissions = (0, permissions_1.permissionsForSandboxEntry)(entry.permissions); await (0, promises_1.chmod)(destination, permissions.toMode() & 0o1777); } function materializationEscapesWorkspaceError(logicalPath) { return new errors_1.UserError(`Sandbox materialization path "${logicalPath || '.'}" escapes the workspace root.`); } function normalizeGitRepository(entry) { const repository = entry.repo; if (!repository) { throw new errors_1.UserError('git_repo entries require a repo.'); } if (repository.includes('://') || repository.startsWith('git@')) { return repository; } return `https://${entry.host ?? 'github.com'}/${repository}.git`; } //# sourceMappingURL=localWorkspace.js.map