@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
940 lines • 42.1 kB
JavaScript
;
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