UNPKG

@openai/agents-core

Version:

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

929 lines • 39.6 kB
import { UserError } from "../../../errors.mjs"; import { SandboxUnsupportedFeatureError } from "../../errors.mjs"; import { isMount } from "../../entries/index.mjs"; import { normalizeRelativePath } from "../../manifest.mjs"; import { validateMountCredentialBoundaries } from "../../mountSecurity.mjs"; import { permissionsForSandboxEntry } from "../../permissions.mjs"; import { WorkspacePathPolicy } from "../../workspacePaths.mjs"; import { formatSandboxProcessError, runSandboxProcess } from "./runProcess.mjs"; import { chmod, chown, lstat, mkdir, mkdtemp, open, opendir, readdir, readFile, readlink, realpath, rm, stat, symlink, } from 'node:fs/promises'; import { constants } from 'node:fs'; import { dirname, isAbsolute, join, posix, relative, resolve, sep, } from 'node:path'; import { tmpdir } from 'node:os'; import { isHostPathWithinRoot, isHostPathStrictlyWithinRoot, relativeHostPathEscapesRoot, sandboxPathGrantHostPath, } from "../../shared/hostPath.mjs"; import { isSandboxPathNotFoundError } from "../../shared/pathProbe.mjs"; 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 = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW; const LOCAL_SOURCE_FILE_READ_FLAGS = constants.O_RDONLY | constants.O_NOFOLLOW; const LOCAL_SOURCE_DIRECTORY_READ_FLAGS = constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_DIRECTORY; export async function materializeLocalWorkspaceManifest(manifest, workspaceRootPath, options = {}) { validateMountCredentialBoundaries(manifest); assertLocalWorkspaceManifestMetadataSupported('Local sandbox materialization', manifest, { supportsMount: options.supportsMount, allowLocalBindMounts: options.allowLocalBindMounts, allowIdentityMetadata: options.allowIdentityMetadata, }); await mkdir(workspaceRootPath, { recursive: true }); await materializeEntries(workspaceRootPath, manifest.entries, '', { ...options, manifestRoot: manifest.root, localSourceGrants: manifest.extraPathGrants, skipMountEntries: true, }); await materializeLocalWorkspaceManifestMounts(manifest, workspaceRootPath, options); } export function assertLocalWorkspaceManifestMetadataSupported(providerName, manifest, options = {}) { if (!options.allowIdentityMetadata && manifest.users.length > 0) { throw new SandboxUnsupportedFeatureError(`${providerName} does not support manifest users yet.`); } if (!options.allowIdentityMetadata && manifest.groups.length > 0) { throw new SandboxUnsupportedFeatureError(`${providerName} does not support manifest groups yet.`); } for (const [logicalPath, entry] of Object.entries(manifest.entries)) { assertLocalWorkspaceEntryMetadataSupported(providerName, logicalPath, entry, options); } } export async function materializeLocalWorkspaceManifestEntry(workspaceRootPath, logicalPath, entry, options = {}) { assertLocalWorkspaceEntryMetadataSupported('Local sandbox materialization', logicalPath, entry, { supportsMount: options.supportsMount, allowLocalBindMounts: options.allowLocalBindMounts, allowIdentityMetadata: options.allowIdentityMetadata, }); if (isMount(entry)) { if (options.materializeMount) { await options.materializeMount({ logicalPath, entry }); return; } await materializeLocalBindMountEntry(workspaceRootPath, logicalPath, entry, options); return; } const destination = logicalPath ? 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 UserError(`Unsupported sandbox entry type: ${entry.type}`); } await applyEntryPermissions(workspaceRootPath, destination, entry, logicalPath); } export async function materializeLocalWorkspaceManifestMounts(manifest, workspaceRootPath, options = {}) { 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. */ export async function restoreLocalWorkspaceManifestStickyPermissions(manifest, workspaceRootPath) { for (const { logicalPath, entry } of manifest.iterEntries()) { if (isMount(entry) || !permissionsForSandboxEntry(entry.permissions).sticky) { continue; } const destination = resolve(workspaceRootPath, logicalPath); const info = await lstat(destination).catch((error) => { // A snapshot may have replaced a manifest ancestor with a file. if (isSandboxPathNotFoundError(error) || error.code === 'ENOTDIR') { return null; } throw error; }); if (info?.isDirectory()) { await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); const handle = await open(destination, LOCAL_SOURCE_DIRECTORY_READ_FLAGS); try { const openedInfo = await handle.stat(); if (!openedInfo.isDirectory() || !sameFilesystemEntry(info, openedInfo)) { throw new UserError(`Sandbox sticky permission target changed during restore: ${logicalPath || '.'}`); } await handle.chmod((openedInfo.mode & 0o7777) | 0o1000); } finally { await handle.close(); } } } } export async function applyOwnershipRecursive(targetPath, uid, gid) { const info = await lstat(targetPath).catch((error) => { if (isSandboxPathNotFoundError(error)) { return null; } throw error; }); if (!info) { return; } if (info.isSymbolicLink()) { return; } await chown(targetPath, uid, gid); if (!info.isDirectory()) { return; } const children = await readdir(targetPath); await Promise.all(children.map(async (child) => { await applyOwnershipRecursive(join(targetPath, child), uid, gid); })); } export async function pathExists(path) { try { await stat(path); return true; } catch (error) { if (!isSandboxPathNotFoundError(error)) { throw error; } let info; try { info = await lstat(path); } catch (probeError) { if (isSandboxPathNotFoundError(probeError)) { return false; } throw probeError; } if (info.isSymbolicLink()) { try { await stat(path); return true; } catch (retryError) { if (isSandboxPathNotFoundError(retryError)) { return false; } throw retryError; } } return true; } } export function joinSandboxLogicalPath(root, logicalPath) { if (!logicalPath) { return root; } return root === '/' ? `/${logicalPath}` : `${root}/${logicalPath}`; } function assertLocalWorkspaceEntryMetadataSupported(providerName, logicalPath, entry, options = {}) { const displayPath = logicalPath || '.'; if (isMount(entry)) { if (options.supportsMount?.(entry) || (options.allowLocalBindMounts !== false && isSupportedLocalBindMount(entry))) { return; } throw new SandboxUnsupportedFeatureError(`${providerName} does not support this mount entry: ${displayPath}`); } if (entry.group !== undefined) { throw new 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 && isMount(entry)) { return; } const logicalPath = prefix ? `${prefix}/${normalizeRelativePath(path)}` : normalizeRelativePath(path); await materializeLocalWorkspaceManifestEntry(workspaceRootPath, logicalPath, entry, options); }); } async function materializeLocalBindMountEntry(workspaceRootPath, logicalPath, entry, options) { if (!isSupportedLocalBindMount(entry)) { throw new SandboxUnsupportedFeatureError(`Local sandbox materialization only supports mount entries with an absolute source and localBindMountStrategy(): ${logicalPath || '.'}`); } if (entry.readOnly !== false) { throw new 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 realpath(entry.source).catch(() => { throw new UserError(`Local bind mount source does not exist: ${entry.source}`); }); const sourceInfo = await lstat(source); if (!sourceInfo.isDirectory()) { throw new UserError(`Local bind mount source must be a directory: ${source}`); } const mountLogicalPath = resolveMountLogicalPath(options.manifestRoot ?? '/workspace', logicalPath, entry.mountPath); if (!mountLogicalPath) { throw new SandboxUnsupportedFeatureError('Local sandbox materialization does not support mounting over the workspace root.'); } const destination = resolve(workspaceRootPath, mountLogicalPath); await createMaterializationParentDirectory(workspaceRootPath, destination, mountLogicalPath); await rm(destination, { recursive: true, force: true }); await symlink(source, destination, 'dir'); } function isSupportedLocalBindMount(entry) { return (entry.type === 'mount' && typeof entry.source === 'string' && isAbsolute(entry.source) && (entry.mountStrategy === undefined || entry.mountStrategy.type === 'local_bind')); } function resolveMountLogicalPath(root, logicalPath, mountPath) { if (mountPath === undefined) { return normalizeRelativePath(logicalPath); } const resolved = new WorkspacePathPolicy({ root }).resolve(mountPath, { forWrite: true, }); if (typeof resolved.workspaceRelativePath !== 'string') { throw new UserError(`Mount path "${mountPath}" escapes the workspace root.`); } return normalizeRelativePath(resolved.workspaceRelativePath); } async function createMaterializationDirectory(workspaceRootPath, destination, logicalPath) { await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); await mkdir(destination, { recursive: true }); await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); } async function createMaterializationParentDirectory(workspaceRootPath, destination, logicalPath) { const parent = dirname(destination); await assertSafeMaterializationPath(workspaceRootPath, parent, logicalPath); await 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 realpath(workspaceRootPath).catch(() => { throw materializationEscapesWorkspaceError(logicalPath); }); const relativeDestination = relative(workspaceRootPath, destination); if (relativeHostPathEscapesRoot(relativeDestination)) { throw materializationEscapesWorkspaceError(logicalPath); } if (relativeDestination === '') { return; } let current = workspaceRootPath; for (const segment of relativeDestination.split(sep)) { if (!segment) { continue; } current = join(current, segment); const info = await lstat(current).catch((error) => { if (error.code === 'ENOENT') { return undefined; } throw error; }); if (!info) { return; } if (info.isSymbolicLink()) { throw new UserError(`Sandbox materialization path "${logicalPath || '.'}" escapes the workspace root through a symbolic link.`); } const currentRealPath = await realpath(current).catch(() => { throw materializationEscapesWorkspaceError(logicalPath); }); if (!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 = resolve(options.localSourceBaseDir ?? process.cwd()); const resolvedSourcePath = resolve(base, sourcePath); if (isHostPathWithinRoot(base, resolvedSourcePath) || (options.localSourceGrants ?? []).some((grant) => isHostPathWithinRoot(resolve(sandboxPathGrantHostPath(grant)), resolvedSourcePath))) { return resolvedSourcePath; } throw new 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. */ export 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 = 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, posix.join(path, 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: posix.join(path, item.path), ...(item.path === '' ? { permissions } : {}), }; } return; case 'git_repo': { if (privateGitRoot === undefined) { throw new UserError('Git resources require a private temporary directory.'); } await assertGitAvailable(); const temporary = await mkdtemp(join(privateGitRoot, 'checkout-')); try { const { result, commitFetchError } = await cloneGitRepo(normalizeGitRepository(entry), temporary, entry.ref); if (result.status !== 0) { throw new UserError(`Failed to materialize git_repo entry ${entry.repo}: ${formatSandboxProcessError(commitFetchError ?? result)}`); } const source = await resolveGitRepoSource(temporary, entry); const copiesTree = Boolean(entry.subpath || (entry.ref && looksLikeCommitRef(entry.ref))); const info = await lstat(source); if (copiesTree && info.isFile()) { yield { type: 'file', path, permissions, content: await readFile(source), }; return; } if (!info.isDirectory()) { throw new 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: posix.join(path, item.path), ...(item.path === '' ? { permissions } : {}), ...(item.path === '' && item.type === 'dir' && !copiesTree ? { mustBeEmpty: true } : {}), }; } } finally { await rm(temporary, { recursive: true, force: true }); } return; } default: throw new SandboxUnsupportedFeatureError('Mount entries must be configured through the sandbox mount APIs.'); } } async function* readClonedGitContent(source, path) { const info = await lstat(source); const permissions = info.mode & 0o1777; if (info.isSymbolicLink()) { yield { type: 'symlink', path, target: await readlink(source) }; } else if (info.isFile()) { const handle = await open(source, LOCAL_SOURCE_FILE_READ_FLAGS); try { const opened = await handle.stat(); if (!opened.isFile() || !sameFilesystemEntry(opened, info)) { throw new 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 opendir(source)) { yield* readClonedGitContent(join(source, child.name), 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 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 opendir(sourceDir)) { await assertStableLocalDirPath(sourceDir, source.stat); const sourcePath = 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 UserError(`local_dir entries do not support symbolic links: ${sourcePath}`); } else { throw new 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 = join(sourceDir, child.name); const destinationPath = 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 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 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 UserError(`local_dir entries do not support symbolic links: ${sourceDir}`); } if (!sourceStat.isDirectory()) { throw new 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 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 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 UserError(`local_file entries do not support symbolic links: ${sourcePath}`); } if (!sourceStat.isFile()) { throw localFilePathChangedError(sourcePath); } let handle; try { handle = await 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 = resolve(sourcePath); let current = dirname(resolvedPath); while (current !== dirname(current)) { const parent = dirname(current); if (parent === dirname(parent)) { break; } let currentStat; try { currentStat = await lstat(current); } catch (error) { if (isPathChangedError(error)) { throw pathChangedError(sourcePath); } throw error; } if (currentStat.isSymbolicLink()) { throw new 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 UserError(`local_dir entries do not support symbolic links: ${sourcePath}`); } if (!sourceStat.isFile()) { throw localDirPathChangedError(sourcePath); } const resolvedSourcePath = await realpathStableLocalSourcePath(sourcePath); if (!isHostPathStrictlyWithinRoot(sourceRoot, resolvedSourcePath)) { throw localDirPathChangedError(sourcePath); } let handle; try { handle = await 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 lstat(sourcePath); } catch (error) { if (isPathChangedError(error)) { throw pathChangedError(sourcePath); } throw error; } } async function realpathStableLocalSourcePath(sourcePath, pathChangedError = localDirPathChangedError) { try { return await realpath(sourcePath); } catch (error) { if (isPathChangedError(error)) { throw pathChangedError(sourcePath); } throw error; } } function localDirPathChangedError(sourcePath) { return new UserError(`local_dir entry path changed while materializing: ${sourcePath}`); } function localFilePathChangedError(sourcePath) { return new 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 UserError(`local_dir entries do not support symbolic links: ${sourcePath}`); } if (!sourceStat.isDirectory()) { throw localDirPathChangedError(sourcePath); } const resolvedSourcePath = await realpathStableLocalSourcePath(sourcePath); if (!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 mkdtemp(join(tmpdir(), 'openai-agents-git-repo-')) : destination; try { if (cloneIntoTemporaryDirectory) { await mkdir(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 UserError(`Failed to materialize git_repo entry ${entry.repo}: ${formatSandboxProcessError(commitFetchError ?? cloneResult)}`); } if (!cloneIntoTemporaryDirectory) { await assertSafeMaterializationPath(workspaceRootPath, destination, logicalPath); return; } const sourcePath = await resolveGitRepoSource(cloneDestination, entry); const sourceInfo = await 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 readFile(sourcePath)); return; } throw new UserError(`git_repo subpath must resolve to a file or directory: ${entry.subpath}`); } finally { if (cloneIntoTemporaryDirectory) { await rm(cloneDestination, { recursive: true, force: true }); } } } async function resolveGitRepoSource(cloneRoot, entry) { const source = entry.subpath ? resolve(cloneRoot, entry.subpath) : cloneRoot; if (entry.subpath && !isHostPathStrictlyWithinRoot(await realpath(cloneRoot), await realpath(source))) { throw new UserError(`git_repo subpath escapes the cloned repository: ${entry.subpath}`); } return source; } async function assertGitAvailable() { const gitVersion = await runSandboxProcess('git', ['--version'], { timeoutMs: GIT_VERSION_TIMEOUT_MS, }); if (gitVersion.status !== 0) { throw new 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 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 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 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 UserError('Sandbox concurrency limits must be positive numbers.'); } return Math.floor(value); } function joinLogicalPath(parent, child) { const normalizedChild = 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 = permissionsForSandboxEntry(entry.permissions); await chmod(destination, permissions.toMode() & 0o1777); } function materializationEscapesWorkspaceError(logicalPath) { return new UserError(`Sandbox materialization path "${logicalPath || '.'}" escapes the workspace root.`); } function normalizeGitRepository(entry) { const repository = entry.repo; if (!repository) { throw new 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.mjs.map