@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
3,604 lines • 165 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DockerSandboxClient = exports.DockerSandboxSession = void 0;
const errors_1 = require("../../errors.js");
const applyDiff_1 = require("../../utils/applyDiff.js");
const errors_2 = require("../errors.js");
const promises_1 = require("node:fs/promises");
const node_crypto_1 = require("node:crypto");
const node_child_process_1 = require("node:child_process");
const node_stream_1 = require("node:stream");
const promises_2 = require("node:stream/promises");
const node_path_1 = require("node:path");
const node_os_1 = require("node:os");
const tar_1 = require("tar");
const entries_1 = require("../entries/index.js");
const client_1 = require("../client.js");
const manifest_1 = require("../manifest.js");
const workspacePaths_1 = require("../workspacePaths.js");
const session_1 = require("../session.js");
const unixLocal_1 = require("./unixLocal.js");
const permissions_1 = require("../permissions.js");
const localWorkspace_1 = require("./shared/localWorkspace.js");
const manifestPersistence_1 = require("./shared/manifestPersistence.js");
const mountSecurity_1 = require("../mountSecurity.js");
const media_1 = require("../shared/media.js");
const localSnapshots_1 = require("./shared/localSnapshots.js");
const pty_1 = require("./shared/pty.js");
const unixLocalFiles_1 = require("./shared/unixLocalFiles.js");
const runProcess_1 = require("./shared/runProcess.js");
const shellCommand_1 = require("./shared/shellCommand.js");
const shell_1 = require("../shared/shell.js");
const posixPath_1 = require("../shared/posixPath.js");
const pathProbe_1 = require("../shared/pathProbe.js");
const sessionStateValues_1 = require("./shared/sessionStateValues.js");
const typeGuards_1 = require("../shared/typeGuards.js");
const credentials_1 = require("../shared/credentials.js");
const hostPath_1 = require("../shared/hostPath.js");
const sessionStateTrust_1 = require("../internal/sessionStateTrust.js");
const stableJson_1 = require("../shared/stableJson.js");
const DEFAULT_DOCKER_IMAGE = 'python:3.14-slim';
const DEFAULT_CONTAINER_COMMAND = 'trap "exit 0" TERM INT; while true; do sleep 3600; done';
const DOCKER_FAST_COMMAND_TIMEOUT_MS = 10_000;
const DOCKER_CONTAINER_START_TIMEOUT_MS = 2 * 60_000;
const DOCKER_CONTAINER_REMOVE_TIMEOUT_MS = 30_000;
const DOCKER_OWNERSHIP_LABEL = 'openai-agents-sandbox';
const DOCKER_SESSION_IDENTITY_LABEL = 'openai-agents-sandbox.session-identity';
const DOCKER_MOUNT_AUTHORITY_FINGERPRINT_LABEL = 'openai-agents-sandbox.mount-authority-fingerprint';
const RESERVED_DOCKER_LABELS = new Set([
DOCKER_OWNERSHIP_LABEL,
DOCKER_SESSION_IDENTITY_LABEL,
DOCKER_MOUNT_AUTHORITY_FINGERPRINT_LABEL,
]);
class DockerSandboxSession extends unixLocal_1.UnixLocalSandboxSession {
containerClosed = false;
closeStarted = false;
stagedMountEnvironment;
mountedPathGrants;
constructor(args) {
// Container file APIs do not use a host file worker.
super({ ...args, [unixLocalFiles_1.preparedFileIO]: null });
this.mountedPathGrants = args.state.manifest.extraPathGrants.map((grant) => ({ ...grant }));
}
/** File APIs execute inside the container and never use the host Python backend. */
get fileIOBackend() {
return 'docker';
}
async resolveFilesystemRunAs(runAs) {
if (runAs && runAs.trim().length > 0) {
throw new errors_1.UserError('DockerSandboxClient does not support runAs for host-side materialization. Use container file APIs or execCommand instead.');
}
return undefined;
}
createEditor(runAs) {
this.assertSessionUsable();
return new DockerSandboxEditor(this, runAs);
}
assertSessionUsable() {
super.assertSessionUsable();
if (this.containerClosed || this.closeStarted) {
throw new errors_1.UserError('Docker sandbox session is closed.');
}
}
async viewImage(args) {
const bytes = await this.readDockerFileAs(args.path, args.runAs, {
maxBytes: media_1.MAX_VIEW_IMAGE_BYTES,
image: true,
});
return (0, media_1.imageOutputFromBytes)(args.path, bytes);
}
async pathExists(path, runAs) {
const absolutePath = await this.validateContainerFilesystemPath(path);
return await (0, pathProbe_1.probeSandboxPathExists)({
path: absolutePath,
runCommand: async (command) => await this.runDockerFilesystemCommand(command, { runAs }),
});
}
async directoryExists(path, runAs) {
const absolutePath = await this.validateContainerFilesystemPath(path);
return await (0, pathProbe_1.probeSandboxDirectoryExists)({
path: absolutePath,
runCommand: async (command) => await this.runDockerFilesystemCommand(command, { runAs }),
});
}
async readFile(args) {
const bytes = await this.readDockerFileAs(args.path, args.runAs);
if (typeof args.maxBytes === 'number' && bytes.byteLength > args.maxBytes) {
return bytes.subarray(0, args.maxBytes);
}
return bytes;
}
async listDir(args) {
const absolutePath = await this.validateContainerFilesystemPath(args.path);
const output = await this.runReadableDockerFilesystemCommand([
`test -d ${(0, shell_1.shellQuote)(absolutePath)} && find -H ${(0, shell_1.shellQuote)(absolutePath)} -mindepth 1 -maxdepth 1 -printf '%y\\0%f\\0'`,
].join(' && '), { runAs: args.runAs }, `list directory ${absolutePath}`, absolutePath);
const resolvedPath = new workspacePaths_1.WorkspacePathPolicy({
root: this.state.manifest.root,
extraPathGrants: this.state.manifest.extraPathGrants,
}).resolve(args.path);
const logicalPath = resolvedPath.workspaceRelativePath ?? resolvedPath.path;
const fields = output.split('\0');
const entries = [];
for (let index = 0; index + 1 < fields.length; index += 2) {
const kind = fields[index];
const name = fields[index + 1];
entries.push({
name,
path: logicalPath ? `${logicalPath}/${name}` : name,
type: kind === 'd' ? 'dir' : kind === 'f' ? 'file' : 'other',
});
}
return entries;
}
async materializeEntry(args) {
const entry = structuredClone(args.entry);
if (!(0, entries_1.isMount)(entry)) {
await (0, mountSecurity_1.withExclusiveSandboxManifestMutation)(this.state, async () => {
this.assertSessionUsable();
await this.resolveFilesystemRunAs(args.runAs);
const logicalPath = this.resolveLogicalPath(args.path);
const nextManifest = (0, manifestPersistence_1.mergeManifestEntryDelta)(this.state.manifest, logicalPath, entry);
(0, mountSecurity_1.assertExistingMountTopologyPreserved)(this.state.manifest, nextManifest);
(0, mountSecurity_1.validateMountCredentialBoundaries)(nextManifest);
const permissions = await this.copyEntryIntoContainer(logicalPath, entry);
await this.applyContainerCopyPermissions(permissions);
(0, mountSecurity_1.recordLiveMountCredentialAuthority)(nextManifest, this.state.manifest);
this.state.manifest = nextManifest;
});
return;
}
if (!isDockerInContainerMount(entry)) {
throw new errors_2.SandboxUnsupportedFeatureError('Docker bind and volume mounts must be configured when creating the sandbox.');
}
await (0, mountSecurity_1.withExclusiveSandboxManifestMutation)(this.state, async () => {
await this.materializeDockerInContainerEntry({ ...args, entry });
});
}
async copyEntryIntoContainer(logicalPath, entry, localSourceGrants = this.state.manifest.extraPathGrants, budget = new DockerResourceCopyBudget(this.archiveLimits)) {
const destination = this.resolveContainerFilesystemPath(logicalPath, {
forWrite: true,
});
const execArgs = dockerResourceExecArgs(this);
const permissions = [];
const gitSourceRoot = containsGitRepo(entry)
? await this.createPrivateGitSourceRoot()
: undefined;
const archive = this.readContainerCopyArchive(destination, (0, localWorkspace_1.readLocalWorkspaceEntry)(entry, localSourceGrants, gitSourceRoot), execArgs, permissions, budget);
try {
// Validate the first source and destination before starting the writer.
const first = await archive.next();
const input = node_stream_1.Readable.from((async function* () {
if (!first.done)
yield first.value;
yield* archive;
})());
// Extraction runs as the sandbox user. Archive ownership cannot grant access.
const result = await runDockerProcess([
...execArgs,
'/usr/bin/tar',
'--extract',
'--file=-',
`--directory=${this.state.manifest.root}`,
'--no-same-owner',
'--same-permissions',
'--overwrite',
'--touch',
], input);
if (result.status !== 0) {
throw new errors_1.UserError(`DockerSandboxClient failed to copy entry: ${(0, runProcess_1.formatSandboxProcessError)(result)}`);
}
return permissions;
}
finally {
// Stop the producer before removing a Git checkout it may still be reading.
try {
await archive.return();
}
finally {
if (gitSourceRoot !== undefined) {
await (0, promises_1.rm)(gitSourceRoot, { recursive: true, force: true });
}
}
}
}
async applyContainerCopyPermissions(permissions) {
const execArgs = dockerResourceExecArgs(this);
// Apply configured modes after children, including mount directories.
for (const item of permissions.reverse()) {
await this.validateContainerCopyPath(item.path);
const mode = await runDockerProcess([
...execArgs,
'chmod',
item.mode.toString(8),
'--',
item.path,
]);
if (mode.status !== 0) {
throw new errors_1.UserError(`DockerSandboxClient failed to set entry permissions: ${(0, runProcess_1.formatSandboxProcessError)(mode)}`);
}
}
}
async *readContainerCopyArchive(destination, contents, execArgs, permissions, budget) {
const creationMask = process.umask();
const appendedDirectories = new Set();
for await (const content of contents) {
const item = { ...content, path: node_path_1.posix.join(destination, content.path) };
const parents = [];
for (let parent = node_path_1.posix.dirname(item.path); parent !== this.state.manifest.root &&
pathWithinDockerMount(parent, this.state.manifest.root); parent = node_path_1.posix.dirname(parent))
parents.unshift(parent);
for (const path of parents) {
if (appendedDirectories.has(path))
continue;
budget.addMember(path, 0);
yield* this.readContainerCopyEntry(execArgs, { type: 'dir', path }, creationMask, budget);
appendedDirectories.add(path);
}
const size = item.type === 'file'
? 'chunks' in item
? item.size
: item.content.byteLength
: 0;
budget.addMember(item.path, size);
yield* this.readContainerCopyEntry(execArgs, item, creationMask, budget);
if (item.type === 'dir')
appendedDirectories.add(item.path);
if (item.permissions !== undefined && item.type !== 'symlink') {
permissions.push({ path: item.path, mode: item.permissions });
}
}
budget.addInputBytes(1024);
yield Buffer.alloc(1024);
}
async createPrivateGitSourceRoot() {
const mounts = await inspectDockerBindMounts(this.state.containerId);
if (mounts === undefined) {
throw new errors_1.UserError('Cannot verify a private Git temporary directory.');
}
const parent = await (0, promises_1.realpath)((0, node_os_1.tmpdir)());
if (mounts.some((mount) => (0, hostPath_1.isHostPathWithinRoot)(mount.source, parent))) {
throw new errors_2.SandboxUnsupportedFeatureError('Git resources require a host temporary directory outside the sandbox mounts. Configure TMPDIR to an unshared directory.');
}
return await (0, promises_1.mkdtemp)((0, node_path_1.join)(parent, 'openai-agents-git-source-'));
}
async *readContainerCopyEntry(execArgs, item, creationMask, budget) {
await this.validateContainerCopyPath(item.path);
const result = await runDockerProcess([
...execArgs,
'stat',
'-c',
'%f',
'--',
item.path,
]);
let mode = (item.type === 'dir' ? 0o777 : 0o666) & ~creationMask;
if (result.status === 0) {
const existingMode = Number.parseInt(result.stdout.trim(), 16);
if ((existingMode & 0o170000) !==
(item.type === 'dir'
? 0o040000
: item.type === 'file'
? 0o100000
: 0o120000)) {
throw new errors_1.UserError(`Entry destination has a conflicting file type: ${item.path}`);
}
if (item.type === 'dir') {
if (item.mustBeEmpty) {
const contents = await runDockerProcess([
...execArgs,
'find',
item.path,
'-mindepth',
'1',
'-maxdepth',
'1',
'-print',
'-quit',
]);
if (contents.status !== 0 || contents.stdout !== '') {
throw new errors_1.UserError('git_repo destination must be an empty directory.');
}
}
// Do not ask tar to change an existing parent's mode or ownership.
return;
}
mode = existingMode & 0o7777;
}
else if (result.status !== 1 ||
!/: No such file or directory\n$/.test(result.stderr)) {
throw new errors_1.UserError(`DockerSandboxClient failed to inspect entry destination: ${(0, runProcess_1.formatSandboxProcessError)(result)}`);
}
const size = item.type === 'file'
? 'chunks' in item
? item.size
: item.content.byteLength
: 0;
const header = new tar_1.Header({
path: node_path_1.posix.relative(this.state.manifest.root, item.path) || '.',
type: item.type === 'dir'
? 'Directory'
: item.type === 'file'
? 'File'
: 'SymbolicLink',
...(item.type === 'symlink' ? { linkpath: item.target } : {}),
// Final directory modes are applied after all descendant chmod calls.
mode: item.type === 'dir' && item.permissions !== undefined
? mode | 0o700
: (item.permissions ?? mode),
size,
mtime: new Date(),
});
if (header.encode()) {
const pax = new tar_1.Pax(header).encode();
budget.addInputBytes(pax.byteLength);
yield pax;
}
budget.addInputBytes(512);
yield header.block;
if (item.type === 'file') {
if ('chunks' in item) {
for await (const chunk of item.chunks) {
budget.addInputBytes(chunk.byteLength);
yield chunk;
}
}
else {
budget.addInputBytes(item.content.byteLength);
yield item.content;
}
const padding = (512 - (size % 512)) % 512;
budget.addInputBytes(padding);
yield Buffer.alloc(padding);
}
}
async validateContainerCopyPath(path) {
const output = await resolveTrustedDockerPath(this, path, 'resolve an entry destination');
const resolved = output.endsWith('\n') ? output.slice(0, -1) : output;
if (!resolved.startsWith('/') ||
!pathWithinDockerMount(resolved, this.state.manifest.root)) {
throw new errors_1.UserError('Entry destination escapes the workspace.');
}
// Source filenames are literal filesystem names, not normalized tool paths.
this.assertContainerPathWritable(path);
this.assertContainerPathWritable(resolved);
}
async materializeDockerInContainerEntry(args) {
const nextManifest = (0, manifestPersistence_1.mergeManifestEntryDelta)(this.state.manifest, this.resolveLogicalPath(args.path), args.entry);
(0, mountSecurity_1.assertExistingMountTopologyPreserved)(this.state.manifest, nextManifest);
(0, mountSecurity_1.validateMountCredentialBoundaries)(nextManifest);
assertDockerReadOnlyHostBindsSupported(nextManifest);
(0, mountSecurity_1.validateMountEnvironmentCredentialBoundaries)(nextManifest, this.state.environment);
const logicalPath = this.resolveLogicalPath(args.path);
assertDockerCanApplyInContainerMounts(this.state.manifest, new manifest_1.Manifest({
entries: {
[logicalPath]: args.entry,
},
}));
(0, localWorkspace_1.assertLocalWorkspaceManifestMetadataSupported)('DockerSandboxClient', new manifest_1.Manifest({
entries: {
[logicalPath]: args.entry,
},
}), {
allowLocalBindMounts: false,
allowIdentityMetadata: true,
supportsMount: isSupportedDockerApplyMount,
});
const preparedMount = await prepareDockerInContainerMount(this, logicalPath, args.entry, nextManifest);
try {
await materializeDockerMountPoint(this.state.workspaceRootPath, this.state.manifest.root, logicalPath, args.entry);
if (args.runAs) {
await this.mkdirDockerPathAs(preparedMount.effectiveMountPath, 'root');
await this.chownContainerPath(preparedMount.effectiveMountPath, args.runAs);
}
await applyPreparedDockerInContainerMount(this, preparedMount);
(0, mountSecurity_1.copyValidatedMountEffectivePaths)(nextManifest, this.state.manifest, nextManifest);
this.state.manifest = nextManifest;
(0, mountSecurity_1.captureLiveMountCredentialAuthority)(this.state.manifest, this.state.environment);
}
catch (error) {
await this.invalidateAfterFailedPrivilegedManifestTransition();
throw error;
}
}
async applyManifest(manifest, runAs) {
const manifestSnapshot = (0, manifest_1.cloneManifest)(manifest);
await (0, mountSecurity_1.withExclusiveSandboxManifestMutation)(this.state, async () => {
await this.applyDockerManifestExclusive(manifestSnapshot, runAs);
});
}
async applyDockerManifestExclusive(manifest, runAs) {
const previousManifest = this.state.manifest;
const nextManifest = (0, manifestPersistence_1.mergeManifestDelta)(this.state.manifest, manifest);
(0, mountSecurity_1.assertExistingMountTopologyPreserved)(this.state.manifest, nextManifest);
(0, mountSecurity_1.validateMountCredentialBoundaries)(nextManifest);
assertDockerReadOnlyHostBindsSupported(nextManifest);
assertDockerManifestDeltaSupported(this.state.manifest, manifest);
assertDockerCanApplyInContainerMounts(this.state.manifest, manifest);
const environment = await manifest.resolveEnvironment();
const nextEnvironment = {
...this.state.environment,
...environment,
};
(0, mountSecurity_1.validateMountEnvironmentCredentialBoundaries)(nextManifest, nextEnvironment);
this.stagedMountEnvironment = nextEnvironment;
let providerEffectsMayHaveStarted = false;
try {
const preparedMounts = await prepareDockerInContainerMounts(this, manifest, nextManifest, nextEnvironment);
const preparedMountsByLogicalPath = new Map(preparedMounts.map((prepared) => [prepared.logicalPath, prepared]));
providerEffectsMayHaveStarted = true;
await provisionDockerAccounts(this.state.containerId, manifest);
const materializedManifest = stripDockerIdentityMetadata(manifest);
const permissions = [];
const budget = new DockerResourceCopyBudget(this.archiveLimits);
for (const [path, entry] of Object.entries(withoutDockerMountEntries(materializedManifest.entries))) {
permissions.push(await this.copyEntryIntoContainer(this.resolveLogicalPath(path), entry, nextManifest.extraPathGrants, budget));
}
await (0, localWorkspace_1.materializeLocalWorkspaceManifestMounts)(materializedManifest, this.state.workspaceRootPath, {
allowLocalBindMounts: false,
allowIdentityMetadata: true,
supportsMount: isSupportedDockerApplyMount,
materializeMount: async ({ logicalPath, entry }) => {
await materializeDockerMountPoint(this.state.workspaceRootPath, this.state.manifest.root, logicalPath, entry);
},
});
for (const entryPermissions of permissions) {
await this.applyContainerCopyPermissions(entryPermissions);
}
if (runAs) {
for (const [path, entry] of Object.entries(manifest.entries)) {
if ((0, entries_1.isMount)(entry)) {
const mountPath = preparedMountsByLogicalPath.get(path)?.effectiveMountPath ??
resolveDockerMountPath(this.state.manifest.root, path, entry);
await this.mkdirDockerPathAs(mountPath, 'root');
await this.chownContainerPath(mountPath, runAs);
}
else {
await this.chownContainerPath(this.resolveContainerFilesystemPath(path), runAs);
}
}
}
await applyDockerInContainerMounts(this, manifest, preparedMounts);
const committedManifest = mergeDockerIdentityMetadata((0, manifestPersistence_1.mergeManifestDelta)(this.state.manifest, materializedManifest), manifest);
(0, mountSecurity_1.copyValidatedMountEffectivePaths)(committedManifest, this.state.manifest, manifest);
this.state.manifest = committedManifest;
this.state.environment = nextEnvironment;
if ((0, mountSecurity_1.manifestHasInContainerMounts)(manifest)) {
(0, mountSecurity_1.captureLiveMountCredentialAuthority)(this.state.manifest, this.state.environment);
}
else {
(0, mountSecurity_1.recordLiveMountCredentialAuthority)(this.state.manifest, previousManifest);
}
}
catch (error) {
if ((0, mountSecurity_1.manifestHasInContainerMounts)(manifest) &&
providerEffectsMayHaveStarted) {
await this.invalidateAfterFailedPrivilegedManifestTransition();
}
throw error;
}
finally {
this.stagedMountEnvironment = undefined;
}
}
async resolveExposedPort(port) {
this.assertSessionUsable();
const containerPort = (0, session_1.normalizeExposedPort)(port);
const configuredPorts = this.state.configuredExposedPorts ?? [];
if (configuredPorts.length > 0 &&
!configuredPorts.includes(containerPort)) {
throw new errors_2.SandboxConfigurationError(`DockerSandboxClient was not configured to expose port ${containerPort}.`, {
provider: 'DockerSandboxClient',
port: containerPort,
configuredPorts,
});
}
const recorded = (0, session_1.getRecordedExposedPortEndpoint)(this.state, containerPort);
if (recorded) {
return recorded;
}
const result = await (0, runProcess_1.runSandboxProcess)('docker', ['port', this.state.containerId, `${containerPort}/tcp`], {
timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS,
});
if (result.status !== 0) {
throw new errors_1.UserError(`Failed to resolve Docker exposed port ${containerPort}: ${(0, runProcess_1.formatSandboxProcessError)(result)}`);
}
return (0, session_1.recordExposedPortEndpoint)(this.state, {
...parseDockerPortBinding(result.stdout, containerPort),
tls: false,
}, containerPort);
}
resolveCommandWorkdir(path) {
const resolvedPath = new workspacePaths_1.WorkspacePathPolicy({
root: this.state.manifest.root,
extraPathGrants: this.mountedPathGrants,
}).resolve(path);
if (resolvedPath.grant) {
return resolvedPath.path;
}
const logicalPath = this.resolveLogicalPath(path);
return (0, localWorkspace_1.joinSandboxLogicalPath)(this.state.manifest.root, logicalPath);
}
async spawnShellCommand(command, args) {
const { shellPath, flag } = (0, shellCommand_1.resolveFallbackShellCommand)({
shell: args.shell,
defaultShell: this.defaultShell,
login: args.login,
});
const dockerArgs = ['exec', '-i', '-w', args.cwd];
if (args.tty) {
dockerArgs.splice(2, 0, '-t');
}
for (const [key, value] of Object.entries(this.state.environment)) {
dockerArgs.push('-e', `${key}=${value}`);
}
const runAs = args.runAs ?? this.state.defaultUser;
if (runAs) {
dockerArgs.push('-u', runAs);
}
dockerArgs.push(this.state.containerId, shellPath, flag, command);
if (args.tty) {
return (0, pty_1.spawnInPseudoTerminal)('docker', dockerArgs);
}
return (0, node_child_process_1.spawn)('docker', dockerArgs, {
stdio: 'pipe',
});
}
translateCommandInput(command) {
return command;
}
translateCommandOutput(output) {
return output;
}
async materializeRestoredWorkspaceMounts() {
await prepareDockerWorkspaceRoot(this.state.workspaceRootPath, this.state.manifest);
await (0, localWorkspace_1.materializeLocalWorkspaceManifestMounts)(this.state.manifest, this.state.workspaceRootPath, {
allowLocalBindMounts: false,
allowIdentityMetadata: true,
supportsMount: isSupportedDockerCreateMount,
materializeMount: async ({ logicalPath, entry }) => {
await materializeDockerMountPoint(this.state.workspaceRootPath, this.state.manifest.root, logicalPath, entry);
},
});
await applyDockerInContainerMounts(this, this.state.manifest);
}
resolveSandboxPath(path, options = {}) {
const mountPath = dockerVolumeMountContainingPath(this.state.manifest, path);
if (mountPath) {
throw new errors_1.UserError(`DockerSandboxClient cannot map Docker volume mount path "${path ?? mountPath}" to a host path. Use the session file APIs or execCommand for container-visible paths under "${mountPath}".`);
}
const inContainerMountPath = dockerInContainerMountContainingPath(this.state.manifest, path);
if (inContainerMountPath) {
throw new errors_1.UserError(`DockerSandboxClient cannot map in-container mount path "${path ?? inContainerMountPath}" to a host path. Use the session file APIs or execCommand for container-visible paths under "${inContainerMountPath}".`);
}
return super.resolveSandboxPath(path, options);
}
resolveContainerFilesystemPath(path, options = {}) {
this.assertSessionUsable();
const resolved = new workspacePaths_1.WorkspacePathPolicy({
root: this.state.manifest.root,
extraPathGrants: this.state.manifest.extraPathGrants,
}).resolve(path, options);
const selectedGrant = resolved.grant;
if (selectedGrant &&
!this.mountedPathGrants.some((grant) => grant.path === selectedGrant.path &&
grant.readOnly === selectedGrant.readOnly &&
(0, hostPath_1.sandboxPathGrantHostPath)(grant) ===
(0, hostPath_1.sandboxPathGrantHostPath)(selectedGrant))) {
throw new errors_1.UserError(`Docker path grant "${selectedGrant.path}" is not mounted. Resume or recreate the session before using it.`);
}
if (options.forWrite) {
this.assertContainerPathWritable(resolved.path);
}
return resolved.path;
}
assertContainerPathWritable(path) {
const mount = this.state.manifest
.mountTargets()
.map(({ logicalPath, entry }) => ({
path: resolveDockerMountPath(this.state.manifest.root, logicalPath, entry),
entry,
}))
.sort((left, right) => right.path.length - left.path.length)
.find((mount) => pathWithinDockerMount(path, mount.path));
if (mount && (mount.entry.readOnly ?? true)) {
throw new errors_1.UserError(`Docker path "${path}" is inside a read-only mount.`);
}
}
async validateContainerFilesystemPath(path, options = {}) {
const absolutePath = this.resolveContainerFilesystemPath(path, options);
const output = await resolveTrustedDockerPath(this, absolutePath, 'resolve a filesystem path');
const resolved = output.endsWith('\n') ? output.slice(0, -1) : output;
if (!resolved.startsWith('/')) {
throw new errors_1.UserError('Docker filesystem path resolution returned an invalid path.');
}
if (this.resolveContainerFilesystemPath(resolved, options) !== resolved) {
throw new errors_1.UserError('Docker filesystem path normalization changed the resolved target.');
}
// Keep leaf symlink identity for unlink and moves after checking its target.
return this.resolveContainerFilesystemPath(path, options);
}
async readDockerFileAs(path, runAs, limits) {
path = await this.validateContainerFilesystemPath(path);
const imageLimit = limits?.image ? limits.maxBytes : undefined;
const fileGuard = `if [ -e ${(0, shell_1.shellQuote)(path)} ] && [ ! -f ${(0, shell_1.shellQuote)(path)} ]; then exit 67; fi; `;
const imageGuard = imageLimit === undefined
? ''
: `size=$(wc -c < ${(0, shell_1.shellQuote)(path)}) || exit 1; if [ "$size" -gt ${imageLimit} ]; then exit 68; fi; `;
const limit = limits?.maxBytes;
// GNU base64 adds one newline per 76 encoded bytes, including the last line.
const encodedBytes = limit === undefined ? undefined : Math.ceil(limit / 3) * 4;
const output = await this.runReadableDockerFilesystemCommand(`${fileGuard}${imageGuard}base64 -- ${(0, shell_1.shellQuote)(path)}`, {
runAs,
maxOutputBytes: encodedBytes === undefined
? undefined
: encodedBytes + Math.ceil(encodedBytes / 76),
}, `read file ${path}`, path, imageLimit !== undefined);
const bytes = Buffer.from(output.replace(/\s+/gu, ''), 'base64');
if (limit !== undefined && bytes.byteLength > limit) {
throw new errors_1.UserError(`Docker file exceeds the ${limit} byte read limit: ${path}`);
}
return bytes;
}
async runReadableDockerFilesystemCommand(command, options, action, path, image = false) {
const result = await this.runDockerFilesystemCommand(command, options);
if (result.status === 67)
throw new errors_1.UserError(`${image ? 'Image' : 'File'} path is not a file: ${path}`);
if (image) {
if (result.status === 68)
throw new errors_1.UserError(`Image file exceeds the 10 MB limit: ${path}`);
}
if (result.status !== 0) {
if (result.status === 1 &&
!result.timedOut &&
!result.signal &&
!result.error) {
const exists = await (0, pathProbe_1.probeSandboxPathExists)({
path,
runCommand: async (command) => await this.runDockerFilesystemCommand(command, options),
createError: () => new errors_1.UserError(`DockerSandboxClient failed to ${action}: ${(0, runProcess_1.formatSandboxProcessError)(result)}`),
});
if (!exists) {
if (image)
throw new errors_1.UserError(`Image file not found: ${path}`);
throw new errors_2.SandboxWorkspaceReadNotFoundError(`DockerSandboxClient path does not exist: ${path}`, { path });
}
}
throw new errors_1.UserError(`DockerSandboxClient failed to ${action}: ${(0, runProcess_1.formatSandboxProcessError)(result)}`);
}
return result.stdout;
}
async writeDockerTextFileAs(path, content, runAs, exclusive = false, moveSource) {
path = await this.validateContainerFilesystemPath(path, { forWrite: true });
const parent = dockerPosixDirname(path);
const guard = exclusive
? `if [ -e ${(0, shell_1.shellQuote)(path)} ] || [ -L ${(0, shell_1.shellQuote)(path)} ]; then echo 'File already exists.' >&2; exit 1; fi; set -C; `
: `if [ -e ${(0, shell_1.shellQuote)(path)} ] && [ ! -f ${(0, shell_1.shellQuote)(path)} ]; then echo 'Destination is not a regular file.' >&2; exit 1; fi; `;
const moveGuard = moveSource
? `if [ ${(0, shell_1.shellQuote)(moveSource)} -ef ${(0, shell_1.shellQuote)(path)} ]; then echo 'Cannot move a file onto itself.' >&2; exit 1; fi; `
: '';
const write = `${moveGuard}${guard}cat > ${(0, shell_1.shellQuote)(path)}`;
await this.runCheckedDockerFilesystemCommand(parent === '/' || parent === '.'
? write
: `mkdir -p -- ${(0, shell_1.shellQuote)(parent)} && (${write})`, { runAs, input: content }, `write file ${path}`);
}
async deleteDockerPathAs(path, runAs) {
path = await this.validateContainerFilesystemPath(path, { forWrite: true });
await this.runCheckedDockerFilesystemCommand(`rm -- ${(0, shell_1.shellQuote)(path)}`, { runAs }, `delete path ${path}`);
}
async mkdirDockerPathAs(path, runAs) {
await this.runCheckedDockerFilesystemCommand(`mkdir -p -- ${(0, shell_1.shellQuote)(path)}`, { runAs }, `create directory ${path}`);
}
async runDockerMountCommand(command, action, options = {}) {
let result;
try {
result = await this.runDockerFilesystemCommand(command, {
runAs: 'root',
input: options.input,
mountEnvironment: options.environment ??
this.stagedMountEnvironment ??
this.state.environment,
});
}
catch {
throw new errors_1.UserError(`DockerSandboxClient failed to ${action}.`);
}
if (result.status !== 0) {
throw new errors_1.UserError(`DockerSandboxClient failed to ${action} with exit status ${result.status}.`);
}
return result.stdout;
}
async chownContainerPath(path, runAs) {
await this.runCheckedDockerFilesystemCommand(`chown -R ${(0, shell_1.shellQuote)(runAs)}:${(0, shell_1.shellQuote)(runAs)} -- ${(0, shell_1.shellQuote)(path)}`, { runAs: 'root' }, `set ownership on ${path}`);
}
async runCheckedDockerFilesystemCommand(command, options = {}, action) {
const result = await this.runDockerFilesystemCommand(command, options);
if (result.status !== 0) {
if (options.redactProcessOutput) {
throw new errors_1.UserError(`DockerSandboxClient failed to ${action} with exit status ${result.status}.`);
}
throw new errors_1.UserError(`DockerSandboxClient failed to ${action}: ${(0, runProcess_1.formatSandboxProcessError)(result)}`);
}
return result.stdout;
}
async runDockerFilesystemCommand(command, options = {}) {
this.assertSessionUsable();
const dockerArgs = ['exec', '-i', '-w', '/'];
if (options.mountEnvironment) {
for (const [key, value] of Object.entries(options.mountEnvironment)) {
dockerArgs.push('-e', `${key}=${value}`);
}
}
else {
dockerArgs.push(...dockerFileEnvironmentArgs(this.state.environment));
}
const runAs = options.runAs ?? this.state.defaultUser;
if (runAs) {
dockerArgs.push('-u', runAs);
}
dockerArgs.push(this.state.containerId);
if (options.mountEnvironment) {
dockerArgs.push('/bin/sh', '-lc', command);
}
else {
dockerArgs.push('/usr/bin/env', '-i', 'PATH=/usr/bin:/bin', 'LC_ALL=C', '/bin/sh', '-c', command);
}
return await runDockerProcess(dockerArgs, options.input, options.maxOutputBytes);
}
async close() {
this.closeStarted = true;
await this.closeContainerResources();
}
async closeContainerResources() {
let cleanupError;
if (!this.containerClosed) {
try {
await removeDockerContainer(this.state.containerId, {
ignoreMissing: true,
});
this.containerClosed = true;
}
catch (error) {
cleanupError = error;
}
}
try {
await removeDockerVolumes(this.state.dockerVolumeNames ?? []);
}
catch (error) {
cleanupError ??= error;
}
try {
await super.close();
}
catch (error) {
cleanupError ??= error;
}
if (cleanupError) {
throw cleanupError;
}
}
async invalidateAfterFailedPrivilegedManifestTransition() {
(0, mountSecurity_1.markSandboxSessionStateUnsafe)(this.state);
try {
await removeDockerContainer(this.state.containerId, {
ignoreMissing: true,
});
this.containerClosed = true;
}
catch {
// The tombstone remains authoritative when force termination fails.
}
}
}
exports.DockerSandboxSession = DockerSandboxSession;
class DockerSandboxEditor {
session;
runAs;
constructor(session, runAs) {
this.session = session;
this.runAs = runAs;
}
canAccessPathForEdit(path) {
try {
this.session.resolveContainerFilesystemPath(path, { forWrite: true });
return true;
}
catch {
return false;
}
}
async createFile(operation) {
this.session.resolveContainerFilesystemPath(operation.path, {
forWrite: true,
});
const content = (0, applyDiff_1.applyDiff)('', operation.diff, 'create');
await this.session.writeDockerTextFileAs(operation.path, content, this.runAs, true);
return {};
}
async updateFile(operation) {
const path = await this.session.validateContainerFilesystemPath(operation.path, {
forWrite: true,
});
const destination = operation.moveTo
? this.session.resolveContainerFilesystemPath(operation.moveTo, {
forWrite: true,
})
: path;
const current = new TextDecoder().decode(await this.session.readDockerFileAs(path, this.runAs, {
maxBytes: 10 * 1024 * 1024,
}));
const next = (0, applyDiff_1.applyDiff)(current, operation.diff);
await this.session.writeDockerTextFileAs(destination, next, this.runAs, false, destination !== path ? path : undefined);
if (operation.moveTo && destination !== path) {
await this.session.deleteDockerPathAs(path, this.runAs);
}
return {};
}
async deleteFile(operation) {
await this.session.deleteDockerPathAs(operation.path, this.runAs);
return {};
}
}
/**
* Docker file APIs run inside a running container using its default user or runAs.
* They require /bin/sh and standard GNU filesystem utilities, including realpath.
* Resource materialization also requires GNU tar inside the container.
* Editor updates reject source files larger than 10 MB before applying changes.
* File helpers use an isolated environment; application and mount commands retain
* their configured environment.
* File operations do not use host Python or fall back to host paths. Path grants
* added after creation become container-visible only after resume or recreation.
*/
class DockerSandboxClient {
backendId = 'docker';
supportsDefaultOptions = true;
options;
failedCreateCleanupTokens = new Map();
constructor(options = {}) {
this.options = {
...options,
...(options.labels === undefined
? {}
: {
labels: normalizeDockerLabels(options.labels, 'DockerSandboxClient labels'),
}),
};
}
async create(args, manifestOptions) {
const createArgs = (0, client_1.normalizeSandboxClientCreateArgs)(args, manifestOptions);
const manifest = createArgs.manifest;
assertDockerManifestSupported(manifest);
const resolvedOptions = {
...this.options,
...createArgs.options,
...(createArgs.snapshot
? { snapshot: createArgs.snapshot }
: {}),
...(createArgs.concurrencyLimits
? { concurrencyLimits: createArgs.concurrencyLimits }
: {}),
...(createArgs.archiveLimits !== undefined
? { archiveLimits: createArgs.archiveLimits }
: {}),
};
const { configuredExposedPorts, networkMode } = resolveDockerNetworkConfiguration(this.options, createArgs.options);
const labels = resolveDockerLabels(this.options, createArgs.options);
const environment = await manifest.resolveEnvironment();
(0, mountSecurity_1.validateMountEnvironmentCredentialBoundaries)(manifest, environment);
await ensureDockerAvailable();
await this.retryFailedCreateCleanups();
const workspaceRootPath = await (0, promises_1.mkdtemp)((0, node_path_1.join)(resolvedOptions.workspaceBaseDir ?? (0, node_os_1.tmpdir)(), 'openai-agents-docker-sandbox-'));
await (0, localWorkspace_1.materializeLocalWorkspaceManifest)(manifest, workspaceRootPath, {
concurrencyLimits: resolvedOptions.concurrencyLimits,
allowLocalBindMounts: false,
allowIdentityMetadata: true,
supportsMount: isSupportedDockerCreateMount,
materializeMount: async ({ logicalPath, entry }) => {
await materializeDockerMountPoint(workspaceRootPath, manifest.root, logicalPath, entry);
},
});
await prepareDockerWorkspaceRoot(workspaceRootPath, manifest);
const image = resolvedOptions.image ?? DEFAULT_DOCKER_IMAGE;
const defaultUser = getHostDockerUser();
const sessionIdentity = (0, node_crypto_1.randomUUID)();
const container = await startDockerContainer({
image,
manifest,
manifestRoot: manifest.root,
workspaceRootPath,
sessionIdentity,
environment,
defaultUser,
exposedPorts: configuredExposedPorts,
networkMode,
labels,
});
const session = new DockerSandboxSession({
state: {
sessionIdentity,
materializedEntriesFingerprint: dockerMaterializedEntriesFingerprint(manifest),
manifest,
workspaceRootPath,
workspaceRootOwned: true,
environment,
snapshotSpec: resolvedOptions.snapshot ?? null,
snapshot: null,
image,
containerId: container.containerId,
defaultUser,
configuredExposedPorts,
networkMode,
labels: { ...labels },
dockerVolumeNames: container.volumeNames,
},
archiveLimits: resolvedOptions.archiveLimits,
});
try {
await provisionDockerAccounts(container.containerId, manifest);
await applyDockerInContainerMounts(session, manifest);
(0, mountSecurity_1.captureLiveMountCredentialAuthority)(manifest, environment);
}
catch (error) {
const cleanupToken = {
containerId: container.containerId,
volumeNames: container.volumeNames,
workspaceRootPath,
removeWorkspace: true,
};
this.failedCreateCleanupTokens.set(container.containerId, cleanupToken);
try {
await cleanupStartedDockerContainer(cleanupToken);
this.failedCreateCleanupTokens.delete(container.containerId);
}
catch (cleanupError) {
throw new errors_2.SandboxLifecycleError('Docker sandbox creation failed and cleanup could not complete.', { errors: [error, cleanupError] });
}
throw error;
}
return session;
}
async resume(state, options = {}) {
const trustedConfig = (0, sessionStateTrust_1.getRunStateSessionTrustedConfig)(state);
this.validateSessionStateForResume({
source: trustedConfig ? 'runState' : 'explicit',
state,
}, trustedConfig
? {
...options,
clientOptions: trustedConfig.clientOptions,
}
: options);
let resumeState = state;
if (!(0, sessionStateTrust_1.isRunStateSessionState)(state)) {
const { configuredExposedPorts, networkMode } = resolveDockerNetworkConfiguration(this.options, options.clientOptions, state);
if (state.networkMode !== networkMode ||
!dockerExposedPortSetsEqual(state.configuredExposedPorts ?? [], configuredExposedPorts)) {
resumeState = {
...state,
configuredExposedPorts,
networkMode,
exposedPorts: undefined,
};
}
const labels = resolveDockerLabels(this.options, options.clientOptions, state);
resumeState = {
...resumeState,
labels,
};
}
(0, manifestPersistence_1.assertMountCredentialsRebound)(resumeState);
(0, manifestPersistence_1.assertHostPathGrantsRebound)(resumeState);
assertDockerManifestSupported(resumeState.manifest);
(0, mountSecurity_1.validateMountEnvironmentCredentialBoundaries)(resumeState.manifest, resumeState.environment);
await ensureDockerAvailable();
await this.retryFailedCreateCleanups();
const archiveLimits = options.archiveLimits === undefined
? this.options.archiveLimits
: options.archiveLimits;
const restoredState = await this.restoreIfNeeded(resumeState, archiveLimits);
return new DockerSandboxSession({
state: restoredState,
archiveLimits,
});
}
validateSessionStateForResume(input, options = {}) {
const configuredExposedPorts = input.source === 'explicit'
? (0, sessionStateValues_1.normalizeExposedPorts)(input.state.configuredExposedPorts)
: (0, sessionStateValues_1.normalizeExposedPorts)((0, typeGuards_1.readOptionalNumberArray)(input.state.configuredExposedPorts));
const networkMode = validateDockerNetworkConfiguration(input.state.networkMode, configuredExposedPorts);
resolveDockerNetworkConfiguration(this.options, options.clientOptions, input.source === 'explicit'
? { configuredExposedPorts, networkMode }
: undefined);
const persistedLabels = normalizeDockerLabels(input.state.labels, 'Docker sandbox session state labels');
resolveDockerLabels(this.options, options.clientOptions, input.source === 'explicit' ? { labels: persistedLabels } : undefined);
}
async canReusePreservedOwnedSession(state, options = {}) {
validateDockerNetworkConfiguration(state.networkMode, state.configuredExposedPorts);
if ((0, mountSecurity_1.isSandboxSessionStateUnsafe)(state)) {
return false;
}
if ((0, sessionStateTrust_1.isRunStateSessionState)(state)) {
return false;
}
assertDockerReadOnlyHostBindsSupported(state.manifest);
if (!(0, mountSecurity_1.liveMountEnvironmentAuthorityMatches)(state.manifest, state.manifest, state.environment)) {
return false;
}
const resolvedOptions = {
...this.options,
...options.clientOptions,
};
try {
resolveDockerLabels(this.options, options.clientOptions, state);
}
catch {
return false;
}
const { configuredExposedPorts, networkMode } = resolveDockerNetworkConfiguration(this.options, options.clientOptions);
if (state.image !== (resolvedOptions.image ?? DEFAULT_DOCKER_IMAGE) ||
!dockerExposedPortSetsEqual(state.configuredExposedPorts ?? [], configuredExposedPorts) ||
state.networkMode !== networkMode) {
return false;
}
if (options.revalidateManifestEntries === true) {
if (state.materializedEntriesFingerprint === undefined) {
return false;
}
if (state.materializedEntriesFingerprint !==
dockerMaterializedEntriesFingerprint(state.manifest)) {
throw new errors_1.UserError('Docker sandbox resume cannot apply changes to non-mount manifest entries. Start a fresh sandbox session instead.');
}
}
let inspection;
try {
if (!(await inspectContainerRunning(state.containerId))) {
return false;
}
inspection = await inspectRunningDockerContainerForReuse(state);
}
catch {
return false;
}
return inspection.reusable;
}
async serializeSessionState(state, options = {}) {
(0, mountSecurity_1.assertSandboxSessionStateUsable)(state);
const labels = normalizeDockerLabels(state.labels, 'Docker sandbox session state labels');
(0, mountSecurity_1.validateMountEnvironmentCredentialBoundaries)(state.manifest, state.environment);
const stateGeneration = (0, mountSecurity_1.captureSandboxStateGeneration)(state);
const snapshotSpec = state.snapshotSpec ?? this.options.snapshot ?? null;
attachDockerSnapshotExcludedPaths(state);
const snapshot = await (0, localSnapshots_1.persistLocalSnapshot)('DockerSandboxClient', state, snapshotSpec);
state.snapshotSpec = snapshotSpec;
if (options.preserveOwnedSession === true &&
options.reuseLiveSession === false &&
options.willCloseAfterSerialize === true &&
snapshot === null) {
throw new errors_1.UserError('Docker sandbox session cannot be preserved after live reuse was rejected because no restorable snapshot is configured.');
}
const sanitizedMountEnvironment = (0, mountSecurity_1.sanitizeMountCredentialEnvironmentForPersistence)(state);
const serialized = {
...(0, manifestPersistence_1.serializeHostPathGrantRedactionMetadata)(state),
...(0, manifestPersistence_1.serializeMountCredentialRedactionMetadata)(state),
manifest: (0, manifestPersistence_1.serializeManifest)(sanitizedMountEnvironment.manifest),
sessionIdentity: state.sessionIdentity,
workspaceRootPath: state.workspaceRootPath,
workspaceRootOwned: state.workspaceRootOwned,
environment: (0, manifestPersistence_1.sanitizeEnvironmentForPersistence)(sanitizedMountEnvironment),
snapshotSpec: (0, localSnapshots_1.serializeLocalSnapshotSpec)(snapshotSpec),
snapshot,
snapshotFingerprint: state.snapshotFingerprint ?? null,
snapshotFingerprintVersion: state.snapshotFingerprintVersion ?? null,
image: state.image,
containerId: state.containerId,
defaultUser: state.defaultUser,
configuredExposedPorts: state.configuredExposedPorts ?? [],
...(state.networkMode !== undefined
? { networkMode: state.networkMode }
: {}),
labels,
dockerVolumeNames: state.dockerVolumeNames ?? [],
exposedPorts: state.exposedPorts ?? null,
};
(0, mountSecurity_1.assertSandboxStateGenerationUnchanged)(state, stateGeneration);
return serialized;
}
async deserializeSessionState(state) {
const configuredExposedPorts = (0, sessionStateValues_1.normalizeExposedPorts)((0, typeGuards_1.readOptionalNumberArray)(state.configuredExposedPorts));
const networkMode = validateDockerNetworkConfiguration(state.networkMode, configuredExposedPorts);
const baseState = await (0, sessionStateValues_1.deserializeLocalSandboxSessionStateValues)(state, this.options.snapshot);
return {
...baseState,
sessionIdentity: (0, typeGuards_1.readOptionalString)(state, 'sessionIdentity'),
image: (0, typeGuards_1.readString)(state, 'image'),
containerId: (0, typeGuards_1.readString)(state, 'containerId'),
defaultUser: (0, typeGuards_1.readOptionalString)(state, 'defaultUser') ?? getHostDockerUser(),
networkMode,
labels: normalizeDockerLabels(state.labels, 'Serialized Docker sandbox session state labels'),
dockerVolumeNames: (0, typeGuards_1.readStringArray)(state.dockerVolumeNames),
};
}
async restoreIfNeeded(state, archiveLimits) {
attachDockerSnapshotExcludedPaths(state);
if ((0, sessionStateTrust_1.isRunStateSessionState)(state)) {
const trustedConfig = (0, sessionStateTrust_1.getRunStateSessionTrustedConfig)(state);
const resolvedOptions = {
...this.options,
...trustedConfig?.clientOptions,
};
const { configuredExposedPorts, networkMode } = resolveDockerNetworkConfiguration(this.options, trustedConfig?.clientOptions);
const labels = resolveDockerLabels(this.options, trustedConfig?.clientOptions);
const trustedState = {
...state,
sessionIdentity: undefined,
materializedEntriesFingerprint: undefined,
image: resolvedOptions.image ?? DEFAULT_DOCKER_IMAGE,
environment: await resolveDockerRunStateEnvironment(state),
snapshotSpec: trustedConfig?.snapshot ??
resolvedOptions.snapshot ??
null,
defaultUser: getHostDockerUser(),
configuredExposedPorts,
networkMode,
labels,
dockerVolumeNames: [],
exposedPorts: undefined,
};
attachDockerSnapshotExcludedPaths(trustedState);
if (!(await (0, localSnapshots_1.localSnapshotIsRestorable)(trustedState))) {
throw new errors_1.UserError('Docker sandbox resources are unavailable and no local snapshot could be restored.');
}
return await this.restoreSnapshotIntoNewWorkspace(trustedState, archiveLimits, resolvedOptions.workspaceBaseDir);
}
const containerRunning = await inspectContainerRunning(state.containerId);
const workspaceExists = await (0, localWorkspace_1.pathExists)(state.workspaceRootPath);
if (workspaceExists) {
if (containerRunning) {
const inspection = await inspectRunningDockerContainerForReuse(state);
if (inspection.reusable) {
return state;
}
if (inspection.ownershipVerified &&
inspection.requiredLabelsMatch === false) {
throw new errors_1.UserError('Existing Docker sandbox labels do not match required labels. Start a fresh sandbox session instead.');
}
if (!inspection.ownershipVerified) {
throw new errors_1.UserError('Docker sandbox container identity could not be verified. Refusing to resume or replace the running container.');
}
await this.cleanupDockerResources(state);
return await this.restartContainer(state, state.workspaceRootPath, archiveLimits);
}
if (await (0, localSnapshots_1.canReuseLocalSnapshotWorkspace)(state)) {
await this.cleanupDockerResources(state);
return await this.restartContainer(state, state.workspaceRootPath, archiveLimits);
}
if (await (0, localSnapshots_1.localSnapshotIsRestorable)(state)) {
const restoredState = await (0, localSnapshots_1.restoreLocalSnapshotToWorkspace)(state, state.workspaceRootPath, { archiveLimits });
await (0, localWorkspace_1.restoreLocalWorkspaceManifestStickyPermissions)(restoredState.manifest, restoredState.workspaceRootPath);
await this.cleanupDockerResources(state);
return await this.restartContainer(restoredState, restoredState.workspaceRootPath, archiveLimits);
}
}
if (containerRunning) {
const labels = await inspectDockerContainerLabels(state.containerId);
if (!dockerContainerIdentityMatches(state.sessionIdentity, labels)) {
throw new errors_1.UserError('Docker sandbox container identity could not be verified. Refusing to resume or replace the running container.');
}
if (!dockerRequiredLabelsMatch(state.labels, labels)) {
throw new errors_1.UserError('Existing Docker sandbox labels do not match required labels. Start a fresh sandbox session instead.');
}
}
if (!(await (0, localSnapshots_1.localSnapshotIsRestorable)(state))) {
throw new errors_1.UserError('Docker sandbox resources are unavailable and no local snapshot could be restored.');
}
await this.cleanupDockerResources(state);
return await this.restoreSnapshotIntoNewWorkspace(state, archiveLimits);
}
async restoreSnapshotIntoNewWorkspace(state, archiveLimits, workspaceBaseDir = this.options.workspaceBaseDir) {
const workspaceRootPath = await (0, promises_1.mkdtemp)((0, node_path_1.join)(workspaceBaseDir ?? (0, node_os_1.tmpdir)(), 'openai-agents-docker-sandbox-'));
try {
const restoredState = await (0, localSnapshots_1.restoreLocalSnapshotToWorkspace)({
...state,
workspaceRootPath,
workspaceRootOwned: true,
}, workspaceRootPath, { archiveLimits });
await (0, localWorkspace_1.restoreLocalWorkspaceManifestStickyPermissions)(restoredState.manifest, workspaceRootPath);
return await this.restartContainer(restoredState, workspaceRootPath, archiveLimits);
}
catch (error) {
await (0, promises_1.rm)(workspaceRootPath, { recursive: true, force: true }).catch(() => undefined);
throw error;
}
}
async cleanupDockerResources(state) {
await removeDockerContainer(state.containerId, { ignoreMissing: true });
await removeDockerVolumes(state.dockerVolumeNames ?? []);
}
async restartContainer(state, workspaceRootPath, archiveLimits) {
await (0, localWorkspace_1.materializeLocalWorkspaceManifestMounts)(state.manifest, workspaceRootPath, {
allowLocalBindMounts: false,
allowIdentityMetadata: true,
supportsMount: isSupportedDockerCreateMount,
materializeMount: async ({ logicalPath, entry }) => {
await materializeDockerMountPoint(workspaceRootPath, state.manifest.root, logicalPath, entry);
},
});
await prepareDockerWorkspaceRoot(workspaceRootPath, state.manifest);
const sessionIdentity = state.sessionIdentity ?? (0, node_crypto_1.randomUUID)();
const container = await startDockerContainer({
image: state.image,
manifest: state.manifest,
manifestRoot: state.manifest.root,
workspaceRootPath,
sessionIdentity,
environment: state.environment,
defaultUser: state.defaultUser,
exposedPorts: state.configuredExposedPorts,
networkMode: state.networkMode,
labels: normalizeDockerLabels(state.labels, 'Docker sandbox session state labels'),
});
const nextState = {
...state,
sessionIdentity,
materializedEntriesFingerprint: dockerMaterializedEntriesFingerprint(state.manifest),
workspaceRootPath,
containerId: container.containerId,
labels: normalizeDockerLabels(state.labels, 'Docker sandbox session state labels'),
dockerVolumeNames: container.volumeNames,
exposedPorts: undefined,
};
const session = new DockerSandboxSession({
state: nextState,
archiveLimits,
});
const cleanupToken = {
containerId: container.containerId,
volumeNames: container.volumeNames,
workspaceRootPath,
removeWorkspace: false,
};
this.failedCreateCleanupTokens.set(container.containerId, cleanupToken);
try {
await provisionDockerAccounts(container.containerId, state.manifest);
await applyDockerInContainerMounts(session, state.manifest);
}
catch (error) {
try {
await cleanupStartedDockerContainer(cleanupToken);
this.failedCreateCleanupTokens.delete(container.containerId);
}
catch (cleanupError) {
throw new errors_2.SandboxLifecycleError('Docker sandbox restart failed and cleanup could not complete.', { errors: [error, cleanupError] });
}
throw error;
}
this.failedCreateCleanupTokens.delete(container.containerId);
return nextState;
}
async retryFailedCreateCleanups() {
for (const [containerId, cleanupToken] of this.failedCreateCleanupTokens) {
await cleanupStartedDockerContainer(cleanupToken);
this.failedCreateCleanupTokens.delete(containerId);
}
}
}
exports.DockerSandboxClient = DockerSandboxClient;
async function cleanupStartedDockerContainer(args) {
let containerRemovalError;
try {
await removeDockerContainer(args.containerId, { ignoreMissing: true });
}
catch (error) {
containerRemovalError = error;
}
await removeDockerVolumes(args.volumeNames);
if (args.removeWorkspace) {
await (0, promises_1.rm)(args.workspaceRootPath, { recursive: true, force: true }).catch(() => undefined);
}
if (containerRemovalError) {
throw containerRemovalError;
}
}
async function resolveDockerRunStateEnvironment(state) {
return Object.fromEntries(await Promise.all(Object.entries(state.manifest.environment).map(async ([key, value]) => [
key,
(0, manifest_1.isEnvValueReference)(value) && typeof state.environment[key] === 'string'
? state.environment[key]
: await value.resolve(),
])));
}
function assertDockerManifestSupported(manifest) {
(0, mountSecurity_1.validateMountCredentialBoundaries)(manifest);
assertDockerReadOnlyHostBindsSupported(manifest);
assertDockerManifestRootSupported(manifest);
for (const grant of manifest.extraPathGrants) {
(0, hostPath_1.sandboxPathGrantHostPath)(grant);
}
(0, localWorkspace_1.assertLocalWorkspaceManifestMetadataSupported)('DockerSandboxClient', manifest, {
allowLocalBindMounts: false,
allowIdentityMetadata: true,
supportsMount: isSupportedDockerCreateMount,
});
}
function assertDockerReadOnlyHostBindsSupported(manifest) {
if ((manifest.extraPathGrants.some((grant) => grant.readOnly) ||
manifest
.mountTargetsForMaterialization()
.some(({ entry }) => isDockerBindMount(entry) && (entry.readOnly ?? true))) &&
dockerInContainerMountPrivilege(manifest) !== 'none') {
// SYS_ADMIN can allow a sandbox process to remount a read-only host bind.
throw new errors_2.SandboxMountError('Docker read-only host binds cannot be combined with in-container storage mounts that require SYS_ADMIN. Remove the read-only host bind or use a storage strategy that does not require container mount privileges.', undefined, 'mount_config_invalid');
}
}
async function inspectRunningDockerContainerForReuse(state) {
const labels = await inspectDockerContainerLabels(state.containerId);
const sessionIdentity = state.sessionIdentity;
if (!labels) {
return { ownershipVerified: false, reusable: false };
}
if (!sessionIdentity) {
return await inspectLegacyDockerContainerForReuse(state, labels);
}
if (!dockerContainerIdentityMatches(sessionIdentity, labels)) {
return { ownershipVerified: false, reusable: false };
}
if (!dockerRequiredLabelsMatch(state.labels, labels)) {
return {
ownershipVerified: true,
reusable: false,
requiredLabelsMatch: false,
};
}
const workspaceMountBeforeInspect = await resolveDockerWorkspaceMount(state.workspaceRootPath, state.manifest.root);
const pathGrantMountsBeforeInspect = await resolveDockerPathGrantMounts(state.manifest);
const manifestBindMountsBeforeInspect = await resolveDockerManifestBindMounts(state.manifest);
const actualBindMounts = await inspectDockerBindMounts(state.containerId);
const workspaceMountAfterInspect = await resolveDockerWorkspaceMount(state.workspaceRootPath, state.manifest.root);
const pathGrantMountsAfterInspect = await resolveDockerPathGrantMounts(state.manifest);
const manifestBindMountsAfterInspect = await resolveDockerManifestBindMounts(state.manifest);
if (!actualBindMounts) {
return { ownershipVerified: false, reusable: false };
}
const expectedWorkspaceMount = dockerBindMountsFromAuthority([
workspaceMountAfterInspect,
])[0];
const ownershipVerified = actualBindMounts.some((mount) => dockerBindMountsEqual(mount, expectedWorkspaceMount));
if (!ownershipVerified) {
return { ownershipVerified: false, reusable: false };
}
if (!(await dockerNetworkConfigurationMatches(state))) {
return { ownershipVerified: true, reusable: false };
}
const expectedBindMounts = dockerBindMountsFromAuthority([
workspaceMountAfterInspect,
...pathGrantMountsAfterInspect,
...manifestBindMountsAfterInspect,
]);
const recordedFingerprint = labels[DOCKER_MOUNT_AUTHORITY_FINGERPRINT_LABEL];
if (recordedFingerprint === undefined) {
return { ownershipVerified: true, reusable: false };
}
const fingerprintBeforeInspect = dockerMountAuthorityFingerprint(sessionIdentity, workspaceMountBeforeInspect, pathGrantMountsBeforeInspect, manifestBindMountsBeforeInspect, state.manifest);
const fingerprintAfterInspect = dockerMountAuthorityFingerprint(sessionIdentity, workspaceMountAfterInspect, pathGrantMountsAfterInspect, manifestBindMountsAfterInspect, state.manifest);
return {
ownershipVerified: true,
reusable: fingerprintBeforeInspect === fingerprintAfterInspect &&
recordedFingerprint === fingerprintAfterInspect &&
dockerBindMountSetsEqual(actualBindMounts, expectedBindMounts),
};
}
async function inspectLegacyDockerContainerForReuse(state, labels) {
if (labels[DOCKER_OWNERSHIP_LABEL] !== 'true' ||
labels[DOCKER_SESSION_IDENTITY_LABEL] !== undefined ||
labels[DOCKER_MOUNT_AUTHORITY_FINGERPRINT_LABEL] !== undefined) {
return { ownershipVerified: false, reusable: false };
}
if (!dockerRequiredLabelsMatch(state.labels, labels)) {
return {
ownershipVerified: true,
reusable: false,
requiredLabelsMatch: false,
};
}
const workspaceMountBeforeInspect = await resolveDockerWorkspaceMount(state.workspaceRootPath, state.manifest.root);
const manifestBindMountsBeforeInspect = await resolveDockerManifestBindMounts(state.manifest);
const actualBindMounts = await inspectDockerBindMounts(state.containerId);
const workspaceMountAfterInspect = await resolveDockerWorkspaceMount(state.workspaceRootPath, state.manifest.root);
const manifestBindMountsAfterInspect = await resolveDockerManifestBindMounts(state.manifest);
if (!actualBindMounts) {
return { ownershipVerified: false, reusable: false };
}
const expectedWorkspaceMount = dockerBindMountsFromAuthority([
workspaceMountAfterInspect,
])[0];
const ownershipVerified = actualBindMounts.some((mount) => dockerBindMountsEqual(mount, expectedWorkspaceMount));
const networkConfigurationMatches = ownershipVerified && (await dockerNetworkConfigurationMatches(state));
return {
ownershipVerified,
reusable: networkConfigurationMatches &&
dockerMountAuthoritiesEqual(workspaceMountBeforeInspect, workspaceMountAfterInspect) &&
dockerMountAuthoritySetsEqual(manifestBindMountsBeforeInspect, manifestBindMountsAfterInspect) &&
state.manifest.extraPathGrants.length === 0 &&
dockerBindMountSetsEqual(actualBindMounts, [
expectedWorkspaceMount,
...dockerBindMountsFromAuthority(manifestBindMountsAfterInspect),
]),
};
}
async function dockerNetworkConfigurationMatches(state) {
if (state.networkMode === undefined) {
return true;
}
const inspectionResult = await (0, runProcess_1.runSandboxProcess)('docker', [
'inspect',
'--type',
'container',
'--format',
'{{json .HostConfig.NetworkMode}}\n{{json .NetworkSettings.Networks}}',
state.containerId,
], { timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS });
if (inspectionResult.status !== 0) {
return false;
}
try {
const [modeJson, networksJson, ...unexpectedLines] = inspectionResult.stdout
.trim()
.split('\n');
if (modeJson === undefined ||
networksJson === undefined ||
unexpectedLines.length > 0) {
return false;
}
const mode = JSON.parse(modeJson);
const networks = JSON.parse(networksJson);
return (mode === 'none' &&
typeof networks === 'object' &&
networks !== null &&
!Array.isArray(networks) &&
Object.keys(networks).every((network) => network === 'none'));
}
catch {
return false;
}
}
function dockerContainerIdentityMatches(sessionIdentity, labels) {
return (sessionIdentity !== undefined &&
labels?.[DOCKER_OWNERSHIP_LABEL] === 'true' &&
labels[DOCKER_SESSION_IDENTITY_LABEL] === sessionIdentity);
}
function dockerMountAuthoritiesEqual(left, right) {
return (left.source === right.source &&
left.sourceIdentity.device === right.sourceIdentity.device &&
left.sourceIdentity.inode === right.sourceIdentity.inode &&
left.target === right.target &&
left.readOnly === right.readOnly);
}
function dockerMountAuthoritySetsEqual(left, right) {
const sortedLeft = sortDockerMountAuthorities(left);
const sortedRight = sortDockerMountAuthorities(right);
return (sortedLeft.length === sortedRight.length &&
sortedLeft.every((mount, index) => dockerMountAuthoritiesEqual(mount, sortedRight[index])));
}
function sortDockerMountAuthorities(mounts) {
return [...mounts].sort((left, right) => {
const targetOrder = left.target.localeCompare(right.target);
return targetOrder !== 0
? targetOrder
: left.source.localeCompare(right.source);
});
}
async function inspectDockerContainerLabels(containerId) {
const result = await (0, runProcess_1.runSandboxProcess)('docker', [
'inspect',
'--type',
'container',
'--format',
'{{json .Config.Labels}}',
containerId,
], {
timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS,
});
if (result.status !== 0) {
return undefined;
}
try {
const labels = JSON.parse(result.stdout);
return typeof labels === 'object' &&
labels !== null &&
!Array.isArray(labels)
? labels
: undefined;
}
catch {
return undefined;
}
}
async function inspectDockerBindMounts(containerId) {
const result = await (0, runProcess_1.runSandboxProcess)('docker', [
'inspect',
'--type',
'container',
'--format',
'{{json .Mounts}}',
containerId,
], {
timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS,
});
if (result.status !== 0) {
return undefined;
}
try {
const mounts = JSON.parse(result.stdout);
if (!Array.isArray(mounts)) {
return undefined;
}
return await Promise.all(mounts
.filter((mount) => typeof mount === 'object' &&
mount !== null &&
mount.Type === 'bind')
.map(async (mount) => {
if (typeof mount.Source !== 'string' ||
typeof mount.Destination !== 'string' ||
typeof mount.RW !== 'boolean') {
throw new Error('Invalid Docker bind mount inspection result.');
}
return {
source: await (0, promises_1.realpath)(mount.Source),
target: mount.Destination,
readWrite: mount.RW,
};
}));
}
catch {
return undefined;
}
}
function dockerBindMountsEqual(left, right) {
return (left.source === right.source &&
left.target === right.target &&
left.readWrite === right.readWrite);
}
function dockerBindMountsFromAuthority(mounts) {
return mounts.map((mount) => ({
source: mount.source,
target: mount.target,
readWrite: !mount.readOnly,
}));
}
function dockerBindMountSetsEqual(left, right) {
return (JSON.stringify(sortDockerBindMounts(left)) ===
JSON.stringify(sortDockerBindMounts(right)));
}
function sortDockerBindMounts(mounts) {
return [...mounts].sort((left, right) => {
const targetOrder = left.target.localeCompare(right.target);
return targetOrder !== 0
? targetOrder
: left.source.localeCompare(right.source);
});
}
async function resolveDockerWorkspaceMount(workspaceRootPath, manifestRoot) {
return await resolveDockerMountAuthority(workspaceRootPath, manifestRoot, false);
}
async function resolveDockerMountAuthority(sourcePath, target, readOnly) {
const source = await (0, promises_1.realpath)(sourcePath);
const sourceStats = await (0, promises_1.stat)(source, { bigint: true });
return {
source,
sourceIdentity: {
device: sourceStats.dev.toString(),
inode: sourceStats.ino.toString(),
},
target,
readOnly,
};
}
async function resolveDockerPathGrantMounts(manifest) {
return await Promise.all(manifest.extraPathGrants.map(async (grant) => await resolveDockerMountAuthority((0, hostPath_1.sandboxPathGrantHostPath)(grant), grant.path, grant.readOnly)));
}
async function resolveDockerManifestBindMounts(manifest) {
return await Promise.all(manifest
.mountTargetsForMaterialization()
.filter(({ entry }) => isDockerBindMount(entry))
.map(async ({ mountPath, entry }) => await resolveDockerMountAuthority(entry.source, mountPath, entry.readOnly ?? true)));
}
function dockerMountAuthorityFingerprint(sessionIdentity, workspaceMount, pathGrantMounts, manifestBindMounts, manifest) {
const mountedGrants = [...pathGrantMounts].sort((left, right) => left.target < right.target ? -1 : left.target > right.target ? 1 : 0);
const mountedManifestBinds = [...manifestBindMounts].sort((left, right) => left.target < right.target ? -1 : left.target > right.target ? 1 : 0);
return (0, node_crypto_1.createHash)('sha256')
.update((0, stableJson_1.stableJsonStringify)({
sessionIdentity,
workspaceMount,
mountedGrants,
mountedManifestBinds,
users: manifest.users,
groups: manifest.groups,
mountedManifestEntries: manifest
.mountTargetsForMaterialization()
.map(({ mountPath, entry }) => ({ mountPath, entry }))
.sort((left, right) => left.mountPath.localeCompare(right.mountPath)),
}))
.digest('hex');
}
function dockerMaterializedEntriesFingerprint(manifest) {
return (0, node_crypto_1.createHash)('sha256')
.update((0, stableJson_1.stableJsonStringify)({
entries: dockerNonMountManifestEntries(manifest),
}))
.digest('hex');
}
function dockerNonMountManifestEntries(manifest) {
const flattened = [];
const visit = (children, parentPath = '') => {
for (const [name, entry] of Object.entries(children).sort(([left], [right]) => left.localeCompare(right))) {
const path = parentPath ? `${parentPath}/${name}` : name;
if ((0, entries_1.isMount)(entry)) {
continue;
}
if (entry.type === 'dir') {
const { children: nestedChildren, ...directory } = entry;
flattened.push({ path, entry: directory });
if (nestedChildren) {
visit(nestedChildren, path);
}
continue;
}
flattened.push({ path, entry });
}
};
visit(manifest.entries);
return flattened;
}
function dockerExposedPortSetsEqual(left, right) {
return (JSON.stringify([...left].sort((a, b) => a - b)) ===
JSON.stringify([...right].sort((a, b) => a - b)));
}
function normalizeDockerLabels(value, source) {
if (value === undefined) {
return {};
}
if (!(0, typeGuards_1.isStringRecord)(value)) {
throw new errors_1.UserError(`${source} must be a record of string values.`);
}
const labels = { ...value };
for (const key of Object.keys(labels)) {
if (RESERVED_DOCKER_LABELS.has(key)) {
throw new errors_1.UserError(`${source} cannot override reserved SDK label "${key}".`);
}
}
return labels;
}
function dockerLabelRecordsEqual(left, right) {
const leftKeys = Object.keys(left);
return (leftKeys.length === Object.keys(right).length &&
leftKeys.every((key) => right[key] === left[key]));
}
function resolveDockerLabels(clientOptions, perRunOptions, explicitStateFallback) {
const trustedLabels = perRunOptions?.labels !== undefined
? normalizeDockerLabels(perRunOptions.labels, 'DockerSandboxClient per-run labels')
: clientOptions.labels !== undefined
? normalizeDockerLabels(clientOptions.labels, 'DockerSandboxClient labels')
: undefined;
const persistedLabels = explicitStateFallback
? normalizeDockerLabels(explicitStateFallback.labels, 'Docker sandbox session state labels')
: undefined;
if (trustedLabels !== undefined &&
persistedLabels !== undefined &&
!dockerLabelRecordsEqual(trustedLabels, persistedLabels)) {
throw new errors_1.UserError('DockerSandboxClient labels cannot be changed when resuming explicit session state. Start a fresh sandbox session instead.');
}
return { ...(trustedLabels ?? persistedLabels ?? {}) };
}
function dockerRequiredLabelsMatch(requiredLabels, actualLabels) {
const normalizedRequiredLabels = normalizeDockerLabels(requiredLabels, 'Docker sandbox session state labels');
return Object.entries(normalizedRequiredLabels).every(([key, value]) => actualLabels?.[key] === value);
}
function validateDockerNetworkConfiguration(networkMode, exposedPorts) {
if (networkMode !== undefined && networkMode !== 'none') {
throw new errors_1.UserError('DockerSandboxClient networkMode must be "none" when provided.');
}
if (networkMode === 'none' && (exposedPorts?.length ?? 0) > 0) {
throw new errors_1.UserError('DockerSandboxClient exposedPorts cannot be used when networkMode is "none".');
}
return networkMode;
}
function resolveDockerNetworkConfiguration(clientOptions, perRunOptions, explicitStateFallback) {
const trustedExposedPorts = perRunOptions?.exposedPorts !== undefined
? (0, sessionStateValues_1.normalizeExposedPorts)(perRunOptions.exposedPorts)
: clientOptions.exposedPorts !== undefined
? (0, sessionStateValues_1.normalizeExposedPorts)(clientOptions.exposedPorts)
: undefined;
const exposedPorts = explicitStateFallback
? (0, sessionStateValues_1.normalizeExposedPorts)(explicitStateFallback.configuredExposedPorts)
: (trustedExposedPorts ?? []);
if (explicitStateFallback &&
trustedExposedPorts !== undefined &&
!dockerExposedPortSetsEqual(trustedExposedPorts, exposedPorts)) {
throw new errors_1.UserError('DockerSandboxClient exposedPorts cannot be changed when resuming explicit session state. Start a fresh sandbox session instead.');
}
const networkMode = validateDockerNetworkConfiguration(perRunOptions?.networkMode !== undefined
? perRunOptions.networkMode
: clientOptions.networkMode !== undefined
? clientOptions.networkMode
: explicitStateFallback?.networkMode, exposedPorts);
return { configuredExposedPorts: exposedPorts, networkMode };
}
function assertDockerManifestDeltaSupported(currentManifest, deltaManifest) {
const currentGrantsByPath = new Map(currentManifest.extraPathGrants.map((grant) => [grant.path, grant]));
const splitGrant = deltaManifest.extraPathGrants.find((grant) => grant.hostPath !== undefined ||
currentGrantsByPath.get(grant.path)?.hostPath !== undefined);
if (splitGrant) {
throw new errors_2.SandboxUnsupportedFeatureError(`DockerSandboxClient cannot add or change path grant hostPath for "${splitGrant.path}" on an already-running container. Configure it in the manifest passed to create().`, {
provider: 'DockerSandboxClient',
feature: 'manifest.extraPathGrants.hostPath',
path: splitGrant.path,
});
}
(0, localWorkspace_1.assertLocalWorkspaceManifestMetadataSupported)('DockerSandboxClient', deltaManifest, {
allowLocalBindMounts: false,
allowIdentityMetadata: true,
supportsMount: isSupportedDockerApplyMount,
});
}
function assertDockerManifestRootSupported(manifest) {
// Docker maps the host workspace as a bind mount at manifest.root; mounting it
// over "/" would hide the image filesystem rather than emulate root confinement.
if (manifest.root === '/') {
throw new errors_1.UserError('DockerSandboxClient does not support manifest root "/".');
}
}
async function prepareDockerWorkspaceRoot(workspaceRootPath, manifest) {
if (manifest.users.length === 0 && manifest.groups.length === 0) {
return;
}
const sticky = (0, permissions_1.permissionsForSandboxEntry)(manifest.entries['']?.permissions).sticky;
await (0, promises_1.chmod)(workspaceRootPath, 0o755 | (sticky ? 0o1000 : 0));
}
async function provisionDockerAccounts(containerId, manifest) {
for (const command of dockerAccountProvisionCommands(manifest)) {
const result = await (0, runProcess_1.runSandboxProcess)('docker', ['exec', '-u', 'root', containerId, '/bin/sh', '-c', command], {
timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS,
});
if (result.status !== 0) {
throw new errors_1.UserError(`Failed to provision Docker sandbox manifest accounts: ${(0, runProcess_1.formatSandboxProcessError)(result)}`);
}
}
}
function dockerAccountProvisionCommands(manifest) {
const commands = [];
const users = new Set(manifest.users.map((user) => user.name));
for (const group of manifest.groups) {
commands.push(`getent group ${(0, shell_1.shellQuote)(group.name)} >/dev/null 2>&1 || groupadd ${(0, shell_1.shellQuote)(group.name)}`);
for (const user of group.users ?? []) {
users.add(user.name);
}
}
for (const user of users) {
const quotedUser = (0, shell_1.shellQuote)(user);
commands.push([
`if id -u ${quotedUser} >/dev/null 2>&1; then exit 0; fi`,
`if getent group ${quotedUser} >/dev/null 2>&1; then useradd -M -s /usr/sbin/nologin -g ${quotedUser} ${quotedUser}; else useradd -U -M -s /usr/sbin/nologin ${quotedUser}; fi`,
].join('; '));
}
for (const group of manifest.groups) {
for (const user of group.users ?? []) {
commands.push(`usermod -aG ${(0, shell_1.shellQuote)(group.name)} ${(0, shell_1.shellQuote)(user.name)}`);
}
}
return commands;
}
function stripDockerIdentityMetadata(manifest) {
const stripped = new manifest_1.Manifest({
version: manifest.version,
root: manifest.root,
entries: manifest.entries,
environment: Object.fromEntries(Object.entries(manifest.environment).map(([key, value]) => [
key,
value.init(),
])),
extraPathGrants: manifest.extraPathGrants,
remoteMountCommandAllowlist: manifest.remoteMountCommandAllowlist,
});
(0, manifest_1.copyManifestMountCredentialExposurePolicy)(stripped, manifest);
return stripped;
}
// Mounts keep their dedicated setup path; only resource bytes use the writer.
function withoutDockerMountEntries(entries) {
return Object.fromEntries(Object.entries(entries)
.filter(([, entry]) => !(0, entries_1.isMount)(entry))
.map(([path, entry]) => [
path,
entry.type === 'dir'
? {
...entry,
children: withoutDockerMountEntries(entry.children ?? {}),
}
: entry,
]));
}
function mergeDockerIdentityMetadata(current, delta) {
return (0, manifestPersistence_1.mergeManifestDelta)(current, new manifest_1.Manifest({
users: delta.users,
groups: delta.groups,
}));
}
async function ensureDockerAvailable() {
const result = await (0, runProcess_1.runSandboxProcess)('docker', ['version'], {
timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS,
});
if (result.status !== 0) {
throw new errors_1.UserError('Docker sandbox execution requires a working Docker CLI and daemon.');
}
}
async function inspectContainerRunning(containerId) {
const result = await (0, runProcess_1.runSandboxProcess)('docker', [
'inspect',
'--type',
'container',
'--format',
'{{.State.Running}}',
containerId,
], {
timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS,
});
if (result.status !== 0) {
if (isMissingDockerContainerError(result)) {
return false;
}
throw new errors_1.UserError(`Failed to inspect Docker sandbox container: ${(0, runProcess_1.formatSandboxProcessError)(result)}`);
}
const running = result.stdout.trim();
if (running === 'true') {
return true;
}
if (running === 'false') {
return false;
}
throw new errors_1.UserError(`Failed to inspect Docker sandbox container: unexpected running state "${running}".`);
}
// One budget covers every resource and generated parent in a manifest update.
class DockerResourceCopyBudget {
limits;
members = 0;
extractedBytes = 0;
inputBytes = 0;
constructor(limits) {
this.limits = (0, client_1.resolveSandboxArchiveLimits)(limits === undefined ? {} : limits);
}
addMember(path, size) {
this.check('member count', ++this.members, this.limits?.maxMembers, path);
this.extractedBytes += size;
this.check('extracted size', this.extractedBytes, this.limits?.maxExtractedBytes, path);
}
addInputBytes(size) {
this.inputBytes += size;
this.check('input size', this.inputBytes, this.limits?.maxInputBytes);
}
check(kind, actual, limit, member) {
if (limit != null && actual > limit) {
throw new errors_2.SandboxArchiveError(`Docker resource ${kind} exceeds limit.`, {
reason: `archive ${kind} exceeds limit`,
limit,
actual,
...(member === undefined ? {} : { member }),
});
}
}
}
async function runDockerProcess(args, input, maxOutputBytes) {
const child = (0, node_child_process_1.spawn)('docker', args, {
stdio: 'pipe',
});
const stdoutChunks = [];
const stderrChunks = [];
let stdinError;
let outputBytes = 0;
let outputLimitError;
const captureOutput = (chunks, chunk) => {
if (outputLimitError)
return;
outputBytes += chunk.length;
if (maxOutputBytes !== undefined && outputBytes > maxOutputBytes) {
outputLimitError = new Error('Docker filesystem command exceeded its output limit.');
stdoutChunks.length = 0;
stderrChunks.length = 0;
child.kill('SIGKILL');
return;
}
chunks.push(chunk);
};
// A rejected command can close its input before buffered editor content drains.
child.stdin.on('error', (error) => {
stdinError = error;
});
child.stdout.on('data', (chunk) => {
captureOutput(stdoutChunks, chunk);
});
child.stderr.on('data', (chunk) => {
captureOutput(stderrChunks, chunk);
});
const closed = new Promise((resolve) => {
child.on('close', (code) => {
resolve(code ?? 1);
});
child.on('error', (error) => {
stderrChunks.push(Buffer.from(error.message));
});
});
if (input instanceof node_stream_1.Readable) {
try {
await (0, promises_2.pipeline)(input, child.stdin);
}
catch (error) {
stdinError = error;
stderrChunks.push(Buffer.from(stdinError.message));
// Closing stdin ends the archive. Keep the client attached until the
// container writer exits; killing only the client can leave it running.
}
}
else {
if (input !== undefined)
child.stdin.write(input);
child.stdin.end();
}
let status = await closed;
if (outputLimitError) {
return {
status: 1,
signal: null,
timedOut: false,
stdout: '',
stderr: outputLimitError.message,
error: outputLimitError,
};
}
if (stdinError instanceof errors_2.SandboxArchiveError) {
throw stdinError;
}
// Preserve command diagnostics, but never report success for failed input.
if (status === 0 && stdinError) {
status = 1;
stderrChunks.push(Buffer.from(stdinError.message));
}
return {
status,
signal: null,
timedOut: false,
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
stderr: Buffer.concat(stderrChunks).toString('utf8'),
};
}
async function removeDockerContainer(containerId, options = {}) {
const result = await (0, runProcess_1.runSandboxProcess)('docker', ['rm', '-f', containerId], {
timeoutMs: DOCKER_CONTAINER_REMOVE_TIMEOUT_MS,
});
if (result.status !== 0) {
if (options.ignoreMissing && isMissingDockerContainerError(result)) {
return;
}
throw new errors_1.UserError(`Failed to remove Docker sandbox container: ${(0, runProcess_1.formatSandboxProcessError)(result)}`);
}
}
function isMissingDockerContainerError(result) {
const message = (0, runProcess_1.formatSandboxProcessError)(result).toLowerCase();
return (message.includes('no such container') || message.includes('no such object'));
}
async function startDockerContainer(args) {
const labels = normalizeDockerLabels(args.labels, 'Docker sandbox container labels');
validateDockerNetworkConfiguration(args.networkMode, args.exposedPorts);
const envArgs = Object.entries(args.environment).flatMap(([key, value]) => [
'-e',
`${key}=${value}`,
]);
const userArgs = args.defaultUser ? ['--user', args.defaultUser] : [];
const portArgs = (0, sessionStateValues_1.normalizeExposedPorts)(args.exposedPorts).flatMap((port) => [
'-p',
`127.0.0.1::${port}`,
]);
const networkArgs = args.networkMode ? ['--network', args.networkMode] : [];
const labelArgs = Object.entries(labels).flatMap(([key, value]) => [
'--label',
`${key}=${value}`,
]);
const containerName = `openai-agents-sandbox-${(0, node_crypto_1.randomUUID)().slice(0, 8)}`;
const workspaceMount = await resolveDockerWorkspaceMount(args.workspaceRootPath, args.manifestRoot);
const pathGrantMounts = await resolveDockerPathGrantMounts(args.manifest);
const manifestBindMounts = await resolveDockerManifestBindMounts(args.manifest);
const { mountArgs, volumeNames } = dockerMountArgsForManifest(args.manifest, containerName, manifestBindMounts);
const privilegeArgs = dockerInContainerMountPrivilegeArgs(args.manifest);
const result = await (0, runProcess_1.runSandboxProcess)('docker', [
'run',
'-d',
'--name',
containerName,
...labelArgs,
'--label',
`${DOCKER_OWNERSHIP_LABEL}=true`,
'--label',
`${DOCKER_SESSION_IDENTITY_LABEL}=${args.sessionIdentity}`,
'--label',
`${DOCKER_MOUNT_AUTHORITY_FINGERPRINT_LABEL}=${dockerMountAuthorityFingerprint(args.sessionIdentity, workspaceMount, pathGrantMounts, manifestBindMounts, args.manifest)}`,
'-v',
`${workspaceMount.source}:${args.manifestRoot}`,
...dockerExtraPathGrantMountArgs(pathGrantMounts),
...mountArgs,
...privilegeArgs,
'-w',
args.manifestRoot,
...networkArgs,
...portArgs,
...userArgs,
...envArgs,
args.image,
'/bin/sh',
'-c',
DEFAULT_CONTAINER_COMMAND,
], {
timeoutMs: DOCKER_CONTAINER_START_TIMEOUT_MS,
});
if (result.status !== 0) {
throw new errors_1.UserError(`Failed to start Docker sandbox container: ${(0, runProcess_1.formatSandboxProcessError)(result)}`);
}
return {
containerId: result.stdout.trim(),
volumeNames,
};
}
async function materializeDockerMountPoint(workspaceRootPath, manifestRoot, logicalPath, entry) {
const relativePath = resolveDockerMountWorkspaceRelativePath(manifestRoot, logicalPath, entry);
if (!relativePath) {
return;
}
await (0, promises_1.mkdir)((0, node_path_1.resolve)(workspaceRootPath, relativePath), { recursive: true });
}
function isSupportedDockerCreateMount(entry) {
return (isDockerBindMount(entry) ||
isDockerVolumeMount(entry) ||
isSupportedDockerInContainerMount(entry));
}
function isSupportedDockerApplyMount(entry) {
return isSupportedDockerInContainerMount(entry);
}
function isDockerBindMount(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 isDockerVolumeMount(entry) {
return Boolean(dockerVolumeDriverConfig(entry));
}
function isDockerInContainerMount(entry) {
return entry.mountStrategy?.type === 'in_container';
}
function isSupportedDockerInContainerMount(entry) {
if (!isDockerInContainerMount(entry)) {
return false;
}
let pattern;
try {
pattern = dockerInContainerMountPattern(entry);
}
catch {
return false;
}
switch (pattern.type) {
case 'rclone':
return dockerRcloneMountTypes.has(entry.type);
case 'mountpoint':
return entry.type === 's3_mount' || entry.type === 'gcs_mount';
case 's3files':
return entry.type === 's3_files_mount';
case 'fuse':
return entry.type === 'azure_blob_mount' || Boolean(pattern.command);
default:
return false;
}
}
function assertDockerCanApplyInContainerMounts(currentManifest, deltaManifest) {
const currentPrivilege = dockerInContainerMountPrivilege(currentManifest);
const requiredPrivilege = dockerInContainerMountPrivilege(deltaManifest);
if (dockerPrivilegeSatisfies(currentPrivilege, requiredPrivilege)) {
return;
}
throw new errors_2.SandboxUnsupportedFeatureError('DockerSandboxClient cannot add this in-container mount to an already-running container because it requires Docker privileges that were not granted at container start.', {
provider: 'DockerSandboxClient',
currentPrivilege,
requiredPrivilege,
});
}
function dockerPrivilegeSatisfies(current, required) {
if (required === 'none' || current === required) {
return true;
}
return current === 'fuse' && required === 'sys_admin';
}
function dockerVolumeMountContainingPath(manifest, path) {
return dockerMountContainingPath(manifest, path, isDockerVolumeMount);
}
function dockerInContainerMountContainingPath(manifest, path) {
return dockerMountContainingPath(manifest, path, isDockerInContainerMount);
}
function dockerMountContainingPath(manifest, path, predicate) {
const resolved = new workspacePaths_1.WorkspacePathPolicy({
root: manifest.root,
extraPathGrants: manifest.extraPathGrants,
}).resolve(path);
for (const { entry, mountPath } of manifest.mountTargets()) {
if (!predicate(entry)) {
continue;
}
if (pathWithinDockerMount(resolved.path, mountPath)) {
return mountPath;
}
}
return undefined;
}
function containsGitRepo(entry) {
return (entry.type === 'git_repo' ||
(entry.type === 'dir' &&
Object.values(entry.children ?? {}).some(containsGitRepo)));
}
function pathWithinDockerMount(path, mountPath) {
if (mountPath === '/') {
return true;
}
return path === mountPath || path.startsWith(`${mountPath}/`);
}
async function applyDockerInContainerMounts(session, manifest, preparedMounts) {
const mounts = preparedMounts ?? (await prepareDockerInContainerMounts(session, manifest));
const appliedMounts = [];
for (const preparedMount of mounts) {
try {
const mountPath = await applyPreparedDockerInContainerMount(session, preparedMount);
appliedMounts.push(dockerAppliedMountCleanupTarget(preparedMount, mountPath));
}
catch (error) {
await throwAfterDockerAppliedMountCleanup(session, appliedMounts, error);
}
}
}
async function prepareDockerInContainerMounts(session, manifest, credentialBoundaryManifest = manifest, environment = session.state.environment) {
const preparedMounts = [];
for (const { logicalPath, entry, } of manifest.mountTargetsForMaterialization()) {
if (isDockerInContainerMount(entry)) {
preparedMounts.push(await prepareDockerInContainerMount(session, logicalPath, entry, manifest, credentialBoundaryManifest, environment));
}
}
return preparedMounts;
}
async function prepareDockerInContainerMount(session, logicalPath, entry, manifest, credentialBoundaryManifest = manifest, environment = session.state.environment) {
const mountPath = resolveDockerMountPath(session.state.manifest.root, logicalPath, entry);
const prepareCandidate = async () => {
const effectiveMountPath = await resolveDockerEffectiveMountPath(session, logicalPath, entry, mountPath);
const credentialExposure = (0, mountSecurity_1.validateMountCredentialBoundariesAtEffectivePath)(manifest, logicalPath, entry, effectiveMountPath);
const preparedCredentialFiles = await prepareDockerMountCredentialFiles(session, credentialBoundaryManifest, entry, mountPath, environment);
return {
entry: preparedCredentialFiles.entry,
effectiveMountPath,
pattern: dockerInContainerMountPattern(preparedCredentialFiles.entry),
credentialExposureAcknowledged: credentialExposure.credentialExposureAcknowledged,
allowAmbientCredentials: credentialExposure.broadCredentialExposureAcknowledged,
environment: preparedCredentialFiles.environment,
};
};
const candidate = await prepareCandidate();
return {
logicalPath,
...candidate,
revalidateMountAuthority: async () => {
if (!candidate.credentialExposureAcknowledged) {
return;
}
const current = await prepareCandidate();
if (current.effectiveMountPath !== candidate.effectiveMountPath ||
current.credentialExposureAcknowledged !==
candidate.credentialExposureAcknowledged ||
current.allowAmbientCredentials !== candidate.allowAmbientCredentials ||
(0, stableJson_1.stableJsonStringify)(current.entry) !==
(0, stableJson_1.stableJsonStringify)(candidate.entry) ||
(0, stableJson_1.stableJsonStringify)(current.environment) !==
(0, stableJson_1.stableJsonStringify)(candidate.environment) ||
(0, stableJson_1.stableJsonStringify)(current.pattern) !==
(0, stableJson_1.stableJsonStringify)(candidate.pattern)) {
throw new errors_2.SandboxMountError('Docker sandbox mount authority changed after validation. Retry from current trusted configuration.', { mountType: entry.type }, 'mount_config_invalid');
}
},
};
}
async function prepareDockerMountCredentialFiles(session, manifest, entry, mountPath, environment) {
const references = (0, mountSecurity_1.mountCredentialFileReferences)(manifest, entry, environment);
if (references.length === 0) {
return { entry: structuredClone(entry), environment };
}
const resolveEffectivePath = async (path) => {
const candidate = path.startsWith('/')
? (0, posixPath_1.normalizePosixPath)(path)
: (0, localWorkspace_1.joinSandboxLogicalPath)(manifest.root, path);
const resolvedPath = (await resolveTrustedDockerPath(session, candidate, 'resolve Docker mount credential path')).trim();
if (!resolvedPath.startsWith('/') || resolvedPath.includes('\n')) {
throw new errors_2.SandboxMountError('DockerSandboxClient failed to resolve an absolute mount credential path.', { mountType: entry.type }, 'mount_config_invalid');
}
return (0, posixPath_1.normalizePosixPath)(resolvedPath);
};
const manifestEntries = await Promise.all([...manifest.iterEntries()]
.filter(({ entry }) => !(0, entries_1.isMount)(entry))
.map(async ({ absolutePath, entry }) => ({
path: await resolveEffectivePath(absolutePath),
recursive: entry.type === 'local_dir' || entry.type === 'git_repo',
})));
const resolvedReferences = await Promise.all(references.map(async ({ field, path }) => {
if (field === 'mountStrategy.pattern.configFilePath') {
try {
resolveDockerRcloneConfigPath(manifest, path);
}
catch {
throw new errors_2.SandboxMountError('DockerSandboxClient failed to resolve the rclone config file.', { mountType: entry.type }, 'mount_config_invalid');
}
}
return { field, path: await resolveEffectivePath(path) };
}));
(0, mountSecurity_1.validateMountCredentialFileEffectivePaths)({
entry,
mountPath,
credentialFiles: resolvedReferences,
manifestEntries,
});
const preparedEntry = structuredClone(entry);
const preparedEnvironment = { ...environment };
for (const reference of resolvedReferences) {
if (reference.field.startsWith('environment.')) {
preparedEnvironment[reference.field.slice('environment.'.length)] =
reference.path;
continue;
}
if (reference.field === 'mountStrategy.pattern.configFilePath') {
try {
resolveDockerRcloneConfigPath(manifest, reference.path);
}
catch {
throw new errors_2.SandboxMountError('DockerSandboxClient failed to resolve the rclone config file.', { mountType: entry.type }, 'mount_config_invalid');
}
}
writeDockerNestedField(preparedEntry, reference.field, reference.path);
}
return {
entry: preparedEntry,
environment: preparedEnvironment,
};
}
function dockerResourceExecArgs(session) {
const args = [
'exec',
'-i',
'-w',
'/',
...dockerFileEnvironmentArgs(session.state.environment),
];
if (session.state.defaultUser)
args.push('-u', session.state.defaultUser);
args.push(session.state.containerId, '/usr/bin/env', '-i', 'PATH=/usr/bin:/bin', 'LC_ALL=C');
return args;
}
function dockerFileEnvironmentArgs(environment) {
// Clear manifest values before starting even the trusted environment launcher.
const clearedEnvironment = new Set([
...Object.keys(environment),
'LD_PRELOAD',
'LD_LIBRARY_PATH',
'LD_AUDIT',
]);
return [...clearedEnvironment].flatMap((key) => ['-e', `${key}=`]);
}
async function resolveTrustedDockerPath(session, path, action) {
session.resolveContainerFilesystemPath();
const dockerArgs = [
'exec',
'-i',
'-w',
'/',
...dockerFileEnvironmentArgs(session.state.environment),
];
dockerArgs.push('-e', 'PATH=/usr/bin:/bin', '-e', 'HOME=/root', '-u', 'root', session.state.containerId, '/usr/bin/realpath', '-m', '--', path);
let result;
try {
result = await runDockerProcess(dockerArgs);
}
catch {
throw new errors_1.UserError(`DockerSandboxClient failed to ${action}.`);
}
if (result.status !== 0) {
throw new errors_1.UserError(`DockerSandboxClient failed to ${action} with exit status ${result.status}.`);
}
return result.stdout;
}
function writeDockerNestedField(record, field, value) {
const segments = field.split('.');
let current = record;
for (const segment of segments.slice(0, -1)) {
if (typeof current[segment] !== 'object' ||
current[segment] === null ||
Array.isArray(current[segment])) {
current[segment] = {};
}
current = current[segment];
}
current[segments.at(-1)] = value;
}
async function applyPreparedDockerInContainerMount(session, prepared) {
const { entry, effectiveMountPath, pattern, allowAmbientCredentials, environment, } = prepared;
await prepared.revalidateMountAuthority();
try {
switch (pattern.type) {
case 'rclone':
await applyDockerRcloneMount(session, entry, effectiveMountPath, pattern, allowAmbientCredentials, environment, prepared.revalidateMountAuthority);
return effectiveMountPath;
case 'mountpoint':
await applyDockerMountpointMount(session, entry, effectiveMountPath, pattern, allowAmbientCredentials, environment, prepared.revalidateMountAuthority);
return effectiveMountPath;
case 's3files':
await applyDockerS3FilesMount(session, entry, effectiveMountPath, pattern, allowAmbientCredentials, prepared.revalidateMountAuthority);
return effectiveMountPath;
case 'fuse':
await applyDockerFuseMount(session, entry, effectiveMountPath, pattern, allowAmbientCredentials, prepared.revalidateMountAuthority);
return effectiveMountPath;
default:
throw new errors_2.SandboxUnsupportedFeatureError('DockerSandboxClient does not support this in-container mount pattern.', {
provider: 'DockerSandboxClient',
feature: 'entry.mountStrategy.pattern',
mountType: entry.type,
patternType: pattern.type,
});
}
}
catch (error) {
return await throwAfterDockerAppliedMountCleanup(session, [dockerAppliedMountCleanupTarget(prepared, effectiveMountPath)], error);
}
}
async function resolveDockerEffectiveMountPath(session, logicalPath, entry, declaredMountPath) {
const workspaceRelativePath = resolveDockerMountWorkspaceRelativePath(session.state.manifest.root, logicalPath, entry);
if (workspaceRelativePath === null) {
const resolvedPath = (await resolveTrustedDockerPath(session, declaredMountPath, 'resolve Docker mount path')).trim();
if (!resolvedPath.startsWith('/') || resolvedPath.includes('\n')) {
throw new errors_2.SandboxMountError('DockerSandboxClient failed to resolve an absolute mount path.', { mountType: entry.type }, 'mount_config_invalid');
}
return (0, posixPath_1.normalizePosixPath)(resolvedPath);
}
const workspaceRoot = await (0, promises_1.realpath)(session.state.workspaceRootPath);
const effectiveHostPath = await realpathWithMissingTail((0, node_path_1.resolve)(session.state.workspaceRootPath, workspaceRelativePath));
const effectiveWorkspacePath = (0, node_path_1.relative)(workspaceRoot, effectiveHostPath);
if (effectiveWorkspacePath === '..' ||
effectiveWorkspacePath.startsWith(`..${node_path_1.sep}`) ||
(0, node_path_1.isAbsolute)(effectiveWorkspacePath)) {
throw new errors_2.SandboxMountError('DockerSandboxClient mount path resolves outside the sandbox workspace.', { mountType: entry.type }, 'mount_config_invalid');
}
return (0, localWorkspace_1.joinSandboxLogicalPath)(session.state.manifest.root, effectiveWorkspacePath.split(node_path_1.sep).join('/'));
}
async function realpathWithMissingTail(path) {
const missingSegments = [];
let existingPath = path;
while (true) {
try {
return (0, node_path_1.resolve)(await (0, promises_1.realpath)(existingPath), ...missingSegments);
}
catch (error) {
if (!isMissingPathError(error)) {
throw error;
}
const parent = (0, node_path_1.dirname)(existingPath);
if (parent === existingPath) {
throw error;
}
missingSegments.unshift((0, node_path_1.basename)(existingPath));
existingPath = parent;
}
}
}
function isMissingPathError(error) {
return (error instanceof Error &&
'code' in error &&
error.code === 'ENOENT');
}
function dockerAppliedMountCleanupTarget(prepared, mountPath) {
return {
mountPath,
verifyRcloneNfsTermination: prepared.pattern.type === 'rclone' &&
prepared.pattern.mode === 'nfs',
};
}
async function cleanupDockerAppliedMounts(session, targets) {
const cleanupErrors = [];
for (const target of [...targets].reverse()) {
const mountPath = target.mountPath;
try {
await session.runDockerMountCommand([
`fusermount3 -u ${(0, shell_1.shellQuote)(mountPath)} >/dev/null 2>&1 || true`,
`fusermount -u ${(0, shell_1.shellQuote)(mountPath)} >/dev/null 2>&1 || true`,
`umount ${(0, shell_1.shellQuote)(mountPath)} >/dev/null 2>&1 || true`,
`umount -l ${(0, shell_1.shellQuote)(mountPath)} >/dev/null 2>&1 || true`,
...(target.verifyRcloneNfsTermination
? [
dockerSafeKillRcloneNfsPidFileCommand(dockerRcloneNfsPidPath(session, mountPath)),
]
: []),
].join(' ; '), `cleanup Docker mount ${mountPath}`);
}
catch (error) {
if (target.verifyRcloneNfsTermination) {
cleanupErrors.push(error);
}
}
}
if (cleanupErrors.length > 0) {
throw new errors_2.SandboxMountError('DockerSandboxClient credential-bearing mount cleanup failed.', {
provider: 'docker',
operation: 'cleanup Docker mounts',
cleanupFailed: true,
failedMountCount: cleanupErrors.length,
}, 'mount_failed');
}
}
async function throwAfterDockerAppliedMountCleanup(session, targets, mountError) {
try {
await cleanupDockerAppliedMounts(session, targets);
}
catch {
throw new errors_2.SandboxMountError('DockerSandboxClient mount failed and credential cleanup could not complete.', {
provider: 'docker',
operation: 'mount in-container filesystem',
cleanupFailed: true,
}, 'mount_failed');
}
throw mountError;
}
function dockerInContainerMountPattern(entry) {
const pattern = entry.mountStrategy?.pattern;
if (pattern) {
return pattern;
}
if (entry.type === 's3_files_mount') {
return { type: 's3files' };
}
if (dockerRcloneMountTypes.has(entry.type)) {
return { type: 'rclone', mode: 'fuse' };
}
throw new errors_2.SandboxUnsupportedFeatureError('DockerSandboxClient in-container mounts require a mount strategy pattern for this mount type.', {
provider: 'DockerSandboxClient',
feature: 'entry.mountStrategy.pattern',
mountType: entry.type,
});
}
function attachDockerSnapshotExcludedPaths(state) {
state.snapshotExcludedPaths = dockerInternalSnapshotExcludedPaths(state.manifest);
}
function dockerInternalSnapshotExcludedPaths(manifest) {
const paths = new Set();
for (const { entry } of manifest.mountTargetsForMaterialization()) {
if (!isDockerInContainerMount(entry)) {
continue;
}
const pattern = dockerInContainerMountPattern(entry);
if (entry.type === 'azure_blob_mount' && pattern.type === 'fuse') {
paths.add('.sandbox-blobfuse-config');
paths.add('.sandbox-blobfuse-cache');
const cachePath = dockerBlobfuseWorkspaceCachePath(pattern);
if (cachePath) {
paths.add(cachePath);
}
}
}
return [...paths];
}
async function applyDockerRcloneMount(session, entry, mountPath, pattern, allowAmbientCredentials, environment, revalidateMountAuthority) {
const mode = pattern.mode ?? 'fuse';
if (mode !== 'fuse' && mode !== 'nfs') {
throw new errors_2.SandboxUnsupportedFeatureError('DockerSandboxClient rclone mounts support fuse and nfs modes only.', {
provider: 'DockerSandboxClient',
feature: 'entry.mountStrategy.pattern.mode',
mode,
});
}
const config = await dockerRcloneMountConfig(session, entry, pattern, mountPath, allowAmbientCredentials);
const configDir = `/tmp/openai-agents-docker-mounts-${dockerPathHash(`${session.state.containerId}:${mountPath}`)}`;
const configPath = `${configDir}/${config.remoteName}.conf`;
const baseCommand = [
`mkdir -p -- ${(0, shell_1.shellQuote)(mountPath)}`,
`rm -rf -- ${(0, shell_1.shellQuote)(configDir)}`,
`trap ${(0, shell_1.shellQuote)(`rm -rf -- ${(0, shell_1.shellQuote)(configDir)}`)} EXIT`,
`install -d -m 700 -- ${(0, shell_1.shellQuote)(configDir)}`,
`(umask 077 && cat > ${(0, shell_1.shellQuote)(configPath)})`,
];
if (mode === 'nfs') {
const nfsAddr = rclonePatternString(pattern, 'nfsAddr') ?? '127.0.0.1:2049';
const pidPath = dockerRcloneNfsPidPath(session, mountPath);
const [host, port] = splitDockerNfsAddr(nfsAddr);
const serverArgs = [
'rclone',
'serve',
'nfs',
`${config.remoteName}:${config.remotePath}`,
'--addr',
nfsAddr,
'--config',
configPath,
...(config.readOnly ? ['--read-only'] : []),
...dockerRclonePatternArgs(pattern),
];
const mountOptions = rclonePatternStringArray(pattern, 'nfsMountOptions') ?? ['vers=4.1', 'tcp', `port=${port}`, 'soft', 'timeo=50', 'retrans=1'];
const mountCommand = [
'{ mounted=0',
`for i in 1 2 3; do if mount -v -t nfs -o ${(0, shell_1.shellQuote)(mountOptions.join(','))} ${(0, shell_1.shellQuote)(`${host}:/`)} ${(0, shell_1.shellQuote)(mountPath)}; then mounted=1; break; fi; sleep 1; done`,
`if [ "$mounted" = 1 ]; then rm -rf -- ${(0, shell_1.shellQuote)(configDir)}; exit 0; fi`,
dockerSafeKillRcloneNfsPidFileCommand(pidPath),
`rm -rf -- ${(0, shell_1.shellQuote)(configDir)}`,
'exit 1',
'}',
].join('; ');
await revalidateMountAuthority();
await session.runDockerMountCommand([
...baseCommand,
`(${shellCommand(serverArgs)} & printf %s "$!" > ${(0, shell_1.shellQuote)(pidPath)}) && ${mountCommand}`,
].join(' && '), `mount rclone ${entry.type}`, { input: config.configText, environment });
return;
}
const mountArgs = [
'rclone',
'mount',
`${config.remoteName}:${config.remotePath}`,
mountPath,
...(config.readOnly ? ['--read-only'] : []),
...dockerRcloneFuseAccessArgs(session),
'--config',
configPath,
'--daemon',
...dockerRclonePatternArgs(pattern),
];
await revalidateMountAuthority();
await session.runDockerMountCommand([
...baseCommand,
dockerEnableFuseAllowOtherCommand(),
shellCommand(mountArgs),
`rm -rf -- ${(0, shell_1.shellQuote)(configDir)}`,
].join(' && '), `mount rclone ${entry.type}`, { input: config.configText, environment });
}
function dockerRcloneNfsPidPath(session, mountPath) {
return `/tmp/openai-agents-rclone-nfs-${dockerPathHash(`${session.state.containerId}:${mountPath}`)}.pid`;
}
function dockerSafeKillRcloneNfsPidFileCommand(pidPath) {
return [
`openai_agents_kill_rclone_nfs() { pid=$(cat ${(0, shell_1.shellQuote)(pidPath)} 2>/dev/null || true); case "$pid" in ''|0|*[!0-9]*) return 0 ;; esac; cmdline=$(tr '\\000' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true); case "$cmdline" in *rclone*serve\\ nfs*) ;; *) return 1 ;; esac; kill "$pid" >/dev/null 2>&1 || return 1; attempts=0; while [ "$attempts" -lt 50 ]; do cmdline=$(tr '\\000' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true); case "$cmdline" in *rclone*serve\\ nfs*) ;; *) return 0 ;; esac; attempts=$((attempts + 1)); sleep 0.1; done; return 1; }`,
'helper_status=0',
'openai_agents_kill_rclone_nfs || helper_status=$?',
`[ "$helper_status" -eq 0 ] && rm -f -- ${(0, shell_1.shellQuote)(pidPath)}`,
'[ "$helper_status" -eq 0 ]',
].join('; ');
}
function dockerEnableFuseAllowOtherCommand() {
return "touch /etc/fuse.conf && (grep -qxF user_allow_other /etc/fuse.conf || printf '\\nuser_allow_other\\n' >> /etc/fuse.conf)";
}
function dockerRcloneFuseAccessArgs(session) {
const user = session.state.defaultUser;
const match = /^(\d+):(\d+)$/u.exec(user ?? '');
return [
'--allow-other',
...(match ? ['--uid', match[1], '--gid', match[2]] : []),
];
}
async function applyDockerMountpointMount(session, entry, mountPath, pattern, allowAmbientCredentials, environment, revalidateMountAuthority) {
if (entry.type !== 's3_mount' && entry.type !== 'gcs_mount') {
throw new errors_2.SandboxUnsupportedFeatureError('DockerSandboxClient mountpoint mounts support S3 and GCS mount entries only.', {
provider: 'DockerSandboxClient',
mountType: entry.type,
});
}
const options = dockerMountpointPatternOptions(pattern);
const endpointUrl = entry.endpointUrl ??
options.endpointUrl ??
(entry.type === 'gcs_mount' ? 'https://storage.googleapis.com' : undefined);
const hasExplicitCredentials = entry.type === 's3_mount'
? Boolean(entry.accessKeyId && entry.secretAccessKey)
: Boolean((0, typeGuards_1.readOptionalString)(entry, 'accessId') &&
(0, typeGuards_1.readOptionalString)(entry, 'secretAccessKey'));
const mountArgs = [
'mount-s3',
...(!hasExplicitCredentials && !allowAmbientCredentials
? ['--no-sign-request']
: []),
...((entry.readOnly ?? true)
? ['--read-only']
: ['--allow-overwrite', '--allow-delete']),
...((entry.region ?? options.region)
? ['--region', entry.region ?? options.region]
: []),
...(endpointUrl ? ['--endpoint-url', endpointUrl] : []),
...(entry.type === 'gcs_mount' ? ['--upload-checksums', 'off'] : []),
...((entry.prefix ?? options.prefix)
? ['--prefix', entry.prefix ?? options.prefix]
: []),
entry.bucket,
mountPath,
];
const envText = entry.type === 's3_mount'
? dockerMountpointAwsEnv(entry.accessKeyId, entry.secretAccessKey, entry.sessionToken)
: dockerMountpointAwsEnv((0, typeGuards_1.readOptionalString)(entry, 'accessId'), (0, typeGuards_1.readOptionalString)(entry, 'secretAccessKey'));
const envDir = `/tmp/openai-agents-mountpoint-env-${dockerPathHash(`${session.state.containerId}:${mountPath}`)}`;
const envPath = `${envDir}/credentials.env`;
const cleanupEnvCommand = `rm -rf -- ${(0, shell_1.shellQuote)(envDir)}`;
const clearedInheritedCredentialAuthorityNames = [
...DOCKER_AWS_CREDENTIAL_AUTHORITY_NAMES,
...(entry.type === 'gcs_mount'
? DOCKER_GCS_CREDENTIAL_AUTHORITY_NAMES
: []),
];
const clearInheritedCredentialAuthorityCommand = clearedInheritedCredentialAuthorityNames.join(' ');
const command = envText
? [
`trap ${(0, shell_1.shellQuote)(cleanupEnvCommand)} EXIT HUP INT TERM`,
cleanupEnvCommand,
`install -d -m 700 -- ${(0, shell_1.shellQuote)(envDir)}`,
`(umask 077 && cat > ${(0, shell_1.shellQuote)(envPath)})`,
`unset ${clearInheritedCredentialAuthorityCommand}`,
`set -a && . ${(0, shell_1.shellQuote)(envPath)} && set +a`,
cleanupEnvCommand,
shellCommand(mountArgs),
].join(' && ')
: shellCommand(mountArgs);
await revalidateMountAuthority();
await session.runDockerMountCommand([`mkdir -p -- ${(0, shell_1.shellQuote)(mountPath)}`, command].join(' && '), `mount mountpoint ${entry.type}`, envText
? {
input: envText,
environment: Object.fromEntries([
...Object.entries(environment),
...clearedInheritedCredentialAuthorityNames.map((name) => [name, '']),
]),
}
: { environment });
}
const DOCKER_AWS_CREDENTIAL_AUTHORITY_NAMES = [
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
'AWS_SESSION_TOKEN',
'AWS_SECURITY_TOKEN',
'AWS_PROFILE',
'AWS_SHARED_CREDENTIALS_FILE',
'AWS_CONFIG_FILE',
'AWS_ROLE_ARN',
'AWS_WEB_IDENTITY_TOKEN_FILE',
'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI',
'AWS_CONTAINER_CREDENTIALS_FULL_URI',
'AWS_CONTAINER_AUTHORIZATION_TOKEN',
'AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE',
];
const DOCKER_GCS_CREDENTIAL_AUTHORITY_NAMES = [
'GOOGLE_APPLICATION_CREDENTIALS',
];
async function applyDockerS3FilesMount(session, entry, mountPath, pattern, allowAmbientCredentials, revalidateMountAuthority) {
if (!allowAmbientCredentials) {
throw new errors_2.SandboxMountError('DockerSandboxClient s3files mounts require explicit trusted credential exposure for their workload identity provider.', { mountPath, mountType: entry.type }, 'mount_config_invalid');
}
if (entry.type !== 's3_files_mount') {
throw new errors_2.SandboxUnsupportedFeatureError('DockerSandboxClient s3files mounts require an s3FilesMount() entry.', {
provider: 'DockerSandboxClient',
mountType: entry.type,
});
}
const s3Files = entry;
if (!s3Files.fileSystemId) {
throw new errors_2.SandboxMountError('s3FilesMount() requires fileSystemId for Docker in-container mounts.', { mountType: entry.type }, 'mount_config_invalid');
}
const patternOptions = dockerS3FilesPatternOptions(pattern);
const options = {
...readStringNullRecord(patternOptions.extraOptions),
...readStringNullRecord(s3Files.extraOptions),
};
if (entry.readOnly ?? true) {
options.ro = null;
}
const mountTargetIp = s3Files.mountTargetIp ?? patternOptions.mountTargetIp;
const accessPoint = s3Files.accessPoint ?? patternOptions.accessPoint;
const region = s3Files.region ?? patternOptions.region;
if (mountTargetIp) {
options.mounttargetip = mountTargetIp;
}
if (accessPoint) {
options.accesspoint = accessPoint;
}
if (region) {
options.region = region;
}
const device = s3Files.subpath
? `${s3Files.fileSystemId}:${s3Files.subpath}`
: s3Files.fileSystemId;
const mountArgs = [
'mount',
'-t',
's3files',
...(Object.keys(options).length > 0
? ['-o', dockerMountOptions(options)]
: []),
device,
mountPath,
];
await revalidateMountAuthority();
await session.runDockerMountCommand([`mkdir -p -- ${(0, shell_1.shellQuote)(mountPath)}`, shellCommand(mountArgs)].join(' && '), `mount s3files ${entry.type}`);
}
async function applyDockerFuseMount(session, entry, mountPath, pattern, allowAmbientCredentials, revalidateMountAuthority) {
if (entry.type === 'azure_blob_mount') {
await applyDockerAzureBlobFuseMount(session, entry, mountPath, pattern, allowAmbientCredentials, revalidateMountAuthority);
return;
}
if (!pattern.command) {
throw new errors_2.SandboxUnsupportedFeatureError('DockerSandboxClient fuse command mounts require pattern.command.', {
provider: 'DockerSandboxClient',
mountType: entry.type,
});
}
const command = Array.isArray(pattern.command)
? shellCommand(pattern.command)
: pattern.command;
await revalidateMountAuthority();
await session.runDockerMountCommand([
`mkdir -p -- ${(0, shell_1.shellQuote)(mountPath)}`,
[
`export OPENAI_AGENTS_MOUNT_PATH=${(0, shell_1.shellQuote)(mountPath)}`,
...(entry.source
? [`export OPENAI_AGENTS_MOUNT_SOURCE=${(0, shell_1.shellQuote)(entry.source)}`]
: []),
command,
].join('; '),
].join(' && '), `mount fuse command ${entry.type}`);
}
async function applyDockerAzureBlobFuseMount(session, entry, mountPath, pattern, allowAmbientCredentials, revalidateMountAuthority) {
const account = entry.account ?? entry.accountName;
if (!account) {
throw new errors_2.SandboxMountError('azureBlobMount() requires account or accountName for Docker fuse mounts.', { mountType: entry.type }, 'mount_config_invalid');
}
if (entry.prefix) {
throw new errors_2.SandboxUnsupportedFeatureError('DockerSandboxClient blobfuse mounts do not support azureBlobMount().prefix. Use an rclone mount pattern for prefix-scoped Azure Blob mounts.', {
provider: 'DockerSandboxClient',
mountType: entry.type,
});
}
if (!entry.accountKey &&
!entry.identityClientId &&
!allowAmbientCredentials) {
throw new errors_2.SandboxMountError('DockerSandboxClient blobfuse mounts require explicit trusted credential exposure before using managed identity.', { mountPath, mountType: entry.type }, 'mount_config_invalid');
}
const cacheType = dockerFusePatternString(pattern, 'cacheType', 'block_cache');
if (cacheType !== 'block_cache' && cacheType !== 'file_cache') {
throw new errors_2.SandboxMountError('blobfuse cacheType must be "block_cache" or "file_cache".', { mountType: entry.type, cacheType }, 'mount_config_invalid');
}
const workspaceRoot = session.state.manifest.root;
const cacheDir = resolveDockerBlobfuseCacheDir(workspaceRoot, pattern, session.state.containerId, account, entry.container);
if (pathWithinDockerMount(cacheDir, mountPath)) {
throw new errors_2.SandboxMountError('blobfuse cachePath must be outside the mount path.', {
mountPath,
cachePath: cacheDir,
}, 'mount_config_invalid');
}
const configDir = `/tmp/openai-agents-blobfuse-config-${dockerPathHash(`${session.state.containerId}:${mountPath}`)}`;
const configName = `${sanitizeDockerMountName(account)}_${sanitizeDockerMountName(entry.container)}.yaml`;
const configPath = `${configDir}/${configName}`;
const configText = dockerBlobfuseConfigText({
account,
container: entry.container,
endpoint: azureBlobEndpoint(entry) ?? `https://${account}.blob.core.windows.net`,
cacheType,
cacheSizeMb: dockerFusePatternNumber(pattern, 'cacheSizeMb') ??
(cacheType === 'block_cache' ? 50_000 : 4_096),
blockCacheBlockSizeMb: dockerFusePatternNumber(pattern, 'blockCacheBlockSizeMb') ?? 16,
blockCacheDiskTimeoutSec: dockerFusePatternNumber(pattern, 'blockCacheDiskTimeoutSec') ?? 3600,
fileCacheTimeoutSec: dockerFusePatternNumber(pattern, 'fileCacheTimeoutSec') ?? 120,
fileCacheMaxSizeMb: dockerFusePatternNumber(pattern, 'fileCacheMaxSizeMb'),
cacheDir,
allowOther: dockerFusePatternBoolean(pattern, 'allowOther', true),
logType: dockerFusePatternString(pattern, 'logType', 'syslog'),
logLevel: dockerFusePatternString(pattern, 'logLevel', 'log_debug'),
entryCacheTimeoutSec: dockerFusePatternNumber(pattern, 'entryCacheTimeoutSec'),
negativeEntryCacheTimeoutSec: dockerFusePatternNumber(pattern, 'negativeEntryCacheTimeoutSec'),
attrCacheTimeoutSec: dockerFusePatternNumber(pattern, 'attrCacheTimeoutSec'),
identityClientId: entry.identityClientId,
accountKey: entry.accountKey,
});
const mountArgs = [
'blobfuse2',
'mount',
...((entry.readOnly ?? true) ? ['--read-only'] : []),
'--config-file',
configPath,
mountPath,
];
await revalidateMountAuthority();
await session.runDockerMountCommand([
'command -v blobfuse2 >/dev/null 2>&1',
`mkdir -p -- ${(0, shell_1.shellQuote)(mountPath)} ${(0, shell_1.shellQuote)(cacheDir)}`,
`rm -rf -- ${(0, shell_1.shellQuote)(configDir)}`,
`trap ${(0, shell_1.shellQuote)(`rm -rf -- ${(0, shell_1.shellQuote)(configDir)}`)} EXIT`,
`install -d -m 700 -- ${(0, shell_1.shellQuote)(configDir)}`,
`(umask 077 && cat > ${(0, shell_1.shellQuote)(configPath)})`,
dockerEnableFuseAllowOtherCommand(),
shellCommand(mountArgs),
`rm -rf -- ${(0, shell_1.shellQuote)(configDir)}`,
].join(' && '), `mount blobfuse ${entry.type}`, { input: configText });
}
function resolveDockerBlobfuseCacheDir(workspaceRoot, pattern, containerId, account, container) {
const configuredCachePath = dockerFusePatternString(pattern, 'cachePath');
if (configuredCachePath) {
return (0, localWorkspace_1.joinSandboxLogicalPath)(workspaceRoot, normalizeDockerBlobfuseCachePath(configuredCachePath));
}
return (0, localWorkspace_1.joinSandboxLogicalPath)(workspaceRoot, [
'.sandbox-blobfuse-cache',
dockerPathHash(containerId),
sanitizeDockerMountName(account),
sanitizeDockerMountName(container),
].join('/'));
}
function dockerBlobfuseWorkspaceCachePath(pattern) {
const configuredCachePath = dockerFusePatternString(pattern, 'cachePath');
return configuredCachePath
? normalizeDockerBlobfuseCachePath(configuredCachePath)
: undefined;
}
function normalizeDockerBlobfuseCachePath(cachePath) {
const normalized = cachePath.replace(/\\/gu, '/');
const parts = normalized.split('/').filter((part) => part.length > 0);
if (normalized.startsWith('/') ||
/^[A-Za-z]:\//u.test(normalized) ||
normalized === '.' ||
parts.includes('..')) {
throw new errors_2.SandboxMountError('blobfuse cachePath must be relative to the workspace root.', { cachePath }, 'mount_config_invalid');
}
return normalized.replace(/^\.\/+/u, '');
}
function dockerBlobfuseConfigText(args) {
const lines = [];
if (args.allowOther) {
lines.push('allow-other: true', '');
}
lines.push('logging:', ` type: ${args.logType}`, ` level: ${args.logLevel}`, '', 'components:', ' - libfuse', ` - ${args.cacheType}`, ' - attr_cache', ' - azstorage', '');
const libfuseLines = [];
if (args.entryCacheTimeoutSec !== undefined) {
libfuseLines.push(` entry-expiration-sec: ${args.entryCacheTimeoutSec}`);
}
if (args.negativeEntryCacheTimeoutSec !== undefined) {
libfuseLines.push(` negative-entry-expiration-sec: ${args.negativeEntryCacheTimeoutSec}`);
}
if (libfuseLines.length > 0) {
lines.push('libfuse:', ...libfuseLines, '');
}
if (args.cacheType === 'block_cache') {
lines.push('block_cache:', ` block-size-mb: ${args.blockCacheBlockSizeMb}`, ` mem-size-mb: ${args.cacheSizeMb}`, ` path: ${args.cacheDir}`, ` disk-size-mb: ${args.cacheSizeMb}`, ` disk-timeout-sec: ${args.blockCacheDiskTimeoutSec}`, '');
}
else {
lines.push('file_cache:', ` path: ${args.cacheDir}`, ` timeout-sec: ${args.fileCacheTimeoutSec}`, ` max-size-mb: ${args.fileCacheMaxSizeMb ?? args.cacheSizeMb}`, '');
}
lines.push('attr_cache:', ` timeout-sec: ${args.attrCacheTimeoutSec ?? 7200}`, '', 'azstorage:', ' type: block', ` account-name: ${args.account}`, ` container: ${args.container}`, ` endpoint: ${args.endpoint}`);
if (args.accountKey) {
lines.push(' auth-type: key', ` account-key: ${args.accountKey}`);
}
else {
lines.push(' mode: msi');
}
if (args.identityClientId) {
lines.push(` identity-client-id: ${args.identityClientId}`);
}
lines.push('');
return lines.join('\n');
}
function dockerFusePatternString(pattern, key, fallback) {
const value = pattern[key];
return typeof value === 'string' ? value : (fallback ?? '');
}
function dockerFusePatternNumber(pattern, key, fallback) {
const value = pattern[key];
return typeof value === 'number' ? value : fallback;
}
function dockerFusePatternBoolean(pattern, key, fallback) {
const value = pattern[key];
return typeof value === 'boolean' ? value : fallback;
}
function dockerMountArgsForManifest(manifest, containerName, manifestBindMounts) {
const mountArgs = [];
const volumeNames = [];
const bindMountsByTarget = new Map(manifestBindMounts.map((mount) => [mount.target, mount]));
for (const { mountPath, entry, } of manifest.mountTargetsForMaterialization()) {
if (isDockerBindMount(entry)) {
const bindMount = bindMountsByTarget.get(mountPath);
if (!bindMount) {
throw new errors_2.SandboxConfigurationError(`Docker bind mount authority was not resolved for "${mountPath}".`);
}
mountArgs.push('--mount', dockerMountArg({
type: 'bind',
source: bindMount.source,
target: mountPath,
readOnly: bindMount.readOnly,
}));
continue;
}
const volumeConfig = dockerVolumeDriverConfig(entry);
if (!volumeConfig) {
continue;
}
const volumeName = dockerVolumeName(containerName, mountPath);
volumeNames.push(volumeName);
mountArgs.push('--mount', dockerMountArg({
type: 'volume',
source: volumeName,
target: mountPath,
readOnly: volumeConfig.readOnly,
volumeDriver: volumeConfig.driver,
volumeOptions: volumeConfig.options,
}));
}
return { mountArgs, volumeNames };
}
function dockerExtraPathGrantMountArgs(pathGrantMounts) {
return pathGrantMounts.flatMap((grant) => [
'--mount',
dockerMountArg({
type: 'bind',
source: grant.source,
target: grant.target,
readOnly: grant.readOnly,
}),
]);
}
function dockerVolumeDriverConfig(entry) {
if (entry.mountStrategy?.type !== 'docker_volume') {
return undefined;
}
const driver = entry.mountStrategy.driver ?? 'local';
const driverOptions = entry.mountStrategy.driverOptions ?? {};
const readOnly = entry.readOnly ?? true;
switch (entry.type) {
case 's3_mount':
if (driver !== 'rclone' && driver !== 'mountpoint') {
return undefined;
}
return {
driver,
options: {
...(driver === 'rclone'
? dockerRcloneS3Options(entry)
: dockerMountpointS3Options(entry)),
...driverOptions,
},
readOnly,
};
case 'gcs_mount':
if (driver !== 'rclone' && driver !== 'mountpoint') {
return undefined;
}
return {
driver,
options: {
...(driver === 'rclone'
? dockerRcloneGcsOptions(entry)
: dockerMountpointGcsOptions(entry)),
...driverOptions,
},
readOnly,
};
case 'r2_mount':
if (driver !== 'rclone') {
return undefined;
}
return {
driver,
options: {
...dockerRcloneR2Options(entry),
...driverOptions,
},
readOnly,
};
case 'azure_blob_mount':
if (driver !== 'rclone') {
return undefined;
}
return {
driver,
options: {
...dockerRcloneAzureBlobOptions(entry),
...driverOptions,
},
readOnly,
};
case 'box_mount':
if (driver !== 'rclone') {
return undefined;
}
return {
driver,
options: {
...dockerRcloneBoxOptions(entry),
...driverOptions,
},
readOnly,
};
default:
return undefined;
}
}
const dockerRcloneMountTypes = new Set([
's3_mount',
'r2_mount',
'gcs_mount',
'azure_blob_mount',
'box_mount',
]);
async function dockerRcloneMountConfig(session, entry, pattern, mountPath, allowAmbientCredentials) {
const remoteName = dockerRcloneRemoteName(entry, pattern, mountPath);
const withConfigText = async (config) => {
const configFilePath = rclonePatternString(pattern, 'configFilePath');
if (!configFilePath) {
return config;
}
let sourcePath;
try {
sourcePath = resolveDockerRcloneConfigPath(session.state.manifest, configFilePath);
}
catch {
throw new errors_2.SandboxMountError('DockerSandboxClient failed to resolve the rclone config file.', { mountType: entry.type }, 'mount_config_invalid');
}
const encodedConfig = await session.runDockerMountCommand(`base64 -- ${(0, shell_1.shellQuote)(sourcePath)}`, 'read rclone config');
const sourceConfigText = Buffer.from(encodedConfig.replace(/\s+/gu, ''), 'base64').toString('utf8');
return {
...config,
configText: supplementDockerRcloneConfigText(sourceConfigText, remoteName, config.configText, entry.type),
};
};
switch (entry.type) {
case 's3_mount':
return await withConfigText({
remoteName,
remotePath: joinRemotePath(entry.bucket, entry.prefix),
configText: [
`[${remoteName}]`,
'type = s3',
`provider = ${entry.s3Provider ?? 'AWS'}`,
...(entry.endpointUrl ? [`endpoint = ${entry.endpointUrl}`] : []),
...(entry.region ? [`region = ${entry.region}`] : []),
...s3CredentialLines(entry, allowAmbientCredentials),
'',
].join('\n'),
readOnly: entry.readOnly ?? true,
});
case 'r2_mount':
validateCredentialPair('R2', entry.type, entry.accessKeyId, entry.secretAccessKey);
if (!entry.accountId) {
throw new errors_2.SandboxMountError('R2 mounts require accountId.', { mountType: entry.type }, 'mount_config_invalid');
}
return await withConfigText({
remoteName,
remotePath: joinRemotePath(entry.bucket, entry.prefix),
configText: [
`[${remoteName}]`,
'type = s3',
'provider = Cloudflare',
`endpoint = ${entry.customDomain ?? `https://${entry.accountId}.r2.cloudflarestorage.com`}`,
'acl = private',
...(entry.accessKeyId && entry.secretAccessKey
? [
'env_auth = false',
`access_key_id = ${entry.accessKeyId}`,
`secret_access_key = ${entry.secretAccessKey}`,
]
: [`env_auth = ${allowAmbientCredentials ? 'true' : 'false'}`]),
'',
].join('\n'),
readOnly: entry.readOnly ?? true,
});
case 'gcs_mount':
return await withConfigText(dockerGcsRcloneMountConfig(entry, remoteName, allowAmbientCredentials));
case 'azure_blob_mount':
return await withConfigText(dockerAzureBlobRcloneMountConfig(entry, remoteName, allowAmbientCredentials));
case 'box_mount':
return await withConfigText({
remoteName,
remotePath: normalizeBoxRemotePath(entry.path),
configText: [
`[${remoteName}]`,
'type = box',
...(entry.clientId ? [`client_id = ${entry.clientId}`] : []),
...(entry.clientSecret
? [`client_secret = ${entry.clientSecret}`]
: []),
...(entry.accessToken ? [`access_token = ${entry.accessToken}`] : []),
...(entry.token ? [`token = ${entry.token}`] : []),
...(entry.boxConfigFile
? [`box_config_file = ${entry.boxConfigFile}`]
: []),
...(entry.configCredentials
? [`config_credentials = ${entry.configCredentials}`]
: []),
...(entry.boxSubType && entry.boxSubType !== 'user'
? [`box_sub_type = ${entry.boxSubType}`]
: []),
...(entry.rootFolderId
? [`root_folder_id = ${entry.rootFolderId}`]
: []),
...(entry.impersonate ? [`impersonate = ${entry.impersonate}`] : []),
...(entry.ownedBy ? [`owned_by = ${entry.ownedBy}`] : []),
'',
].join('\n'),
readOnly: entry.readOnly ?? true,
});
default:
throw new errors_2.SandboxUnsupportedFeatureError('DockerSandboxClient rclone mounts do not support this mount entry.', {
provider: 'DockerSandboxClient',
mountType: entry.type,
});
}
}
function dockerAzureBlobRcloneMountConfig(entry, remoteName, allowAmbientCredentials) {
const account = entry.account ?? entry.accountName;
if (!account) {
throw new errors_2.SandboxMountError('Azure Blob mounts require account or accountName.', { mountType: entry.type }, 'mount_config_invalid');
}
return {
remoteName,
remotePath: joinRemotePath(entry.container, entry.prefix),
configText: [
`[${remoteName}]`,
'type = azureblob',
`account = ${account}`,
...(azureBlobEndpoint(entry)
? [`endpoint = ${azureBlobEndpoint(entry)}`]
: []),
...(entry.accountKey
? [`key = ${entry.accountKey}`]
: entry.identityClientId
? ['use_msi = true', `msi_client_id = ${entry.identityClientId}`]
: allowAmbientCredentials
? ['use_msi = true']
: ['use_msi = false', 'env_auth = false']),
'',
].join('\n'),
readOnly: entry.readOnly ?? true,
};
}
function azureBlobEndpoint(entry) {
return entry.endpointUrl ?? entry.endpoint;
}
function dockerGcsRcloneMountConfig(entry, remoteName, allowAmbientCredentials) {
const accessId = (0, typeGuards_1.readOptionalString)(entry, 'accessId');
const secretAccessKey = (0, typeGuards_1.readOptionalString)(entry, 'secretAccessKey');
if (accessId && secretAccessKey) {
return {
remoteName,
remotePath: joinRemotePath(entry.bucket, entry.prefix),
configText: [
`[${remoteName}]`,
'type = s3',
'provider = GCS',
'env_auth = false',
`access_key_id = ${accessId}`,
`secret_access_key = ${secretAccessKey}`,
`endpoint = ${entry.endpointUrl ?? 'https://storage.googleapis.com'}`,
...(entry.region ? [`region = ${entry.region}`] : []),
'',
].join('\n'),
readOnly: entry.readOnly ?? true,
};
}
return {
remoteName,
remotePath: joinRemotePath(entry.bucket, entry.prefix),
configText: [
`[${remoteName}]`,
'type = google cloud storage',
...(entry.serviceAccountFile
? [`service_account_file = ${entry.serviceAccountFile}`]
: []),
...(entry.serviceAccountCredentials
? [`service_account_credentials = ${entry.serviceAccountCredentials}`]
: []),
...(entry.accessToken ? [`access_token = ${entry.accessToken}`] : []),
entry.serviceAccountFile ||
entry.serviceAccountCredentials ||
entry.accessToken ||
!allowAmbientCredentials
? 'env_auth = false'
: 'env_auth = true',
...(!entry.serviceAccountFile &&
!entry.serviceAccountCredentials &&
!entry.accessToken &&
!allowAmbientCredentials
? ['anonymous = true']
: []),
'',
].join('\n'),
readOnly: entry.readOnly ?? true,
};
}
function dockerInContainerMountPrivilegeArgs(manifest) {
const privilege = dockerInContainerMountPrivilege(manifest);
if (privilege === 'fuse') {
return [
'--device',
'/dev/fuse',
'--cap-add',
'SYS_ADMIN',
'--security-opt',
'apparmor:unconfined',
];
}
if (privilege === 'sys_admin') {
return ['--cap-add', 'SYS_ADMIN', '--security-opt', 'apparmor:unconfined'];
}
return [];
}
function dockerInContainerMountPrivilege(manifest) {
let needsSysAdmin = false;
for (const { entry } of manifest.mountTargetsForMaterialization()) {
if (!isDockerInContainerMount(entry)) {
continue;
}
const pattern = dockerInContainerMountPattern(entry);
if (pattern.type === 'fuse' ||
pattern.type === 'mountpoint' ||
(pattern.type === 'rclone' && (pattern.mode ?? 'fuse') === 'fuse')) {
return 'fuse';
}
if (pattern.type === 's3files' ||
(pattern.type === 'rclone' && pattern.mode === 'nfs')) {
needsSysAdmin = true;
}
}
return needsSysAdmin ? 'sys_admin' : 'none';
}
function s3CredentialLines(entry, allowAmbientCredentials) {
const lines = [];
if (entry.accessKeyId && entry.secretAccessKey) {
lines.push('env_auth = false');
lines.push(`access_key_id = ${entry.accessKeyId}`);
lines.push(`secret_access_key = ${entry.secretAccessKey}`);
if (entry.sessionToken) {
lines.push(`session_token = ${entry.sessionToken}`);
}
}
else {
lines.push(`env_auth = ${allowAmbientCredentials ? 'true' : 'false'}`);
}
return lines;
}
function validateCredentialPair(provider, mountType, accessKeyId, secretAccessKey) {
(0, credentials_1.validateCredentialPair)({
accessKeyId,
secretAccessKey,
message: `${provider} mounts require both accessKeyId and secretAccessKey when either is provided.`,
details: { mountType },
code: 'mount_config_invalid',
});
}
function dockerMountpointAwsEnv(accessKeyId, secretAccessKey, sessionToken) {
if (!accessKeyId || !secretAccessKey) {
return '';
}
return [
`AWS_ACCESS_KEY_ID=${(0, shell_1.shellQuote)(accessKeyId)}`,
`AWS_SECRET_ACCESS_KEY=${(0, shell_1.shellQuote)(secretAccessKey)}`,
...(sessionToken ? [`AWS_SESSION_TOKEN=${(0, shell_1.shellQuote)(sessionToken)}`] : []),
'',
].join('\n');
}
function splitDockerNfsAddr(value) {
const index = value.lastIndexOf(':');
if (index < 0) {
return [
value === '0.0.0.0' || value === '::' ? '127.0.0.1' : value,
'2049',
];
}
const host = value.slice(0, index);
return [
host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host,
value.slice(index + 1),
];
}
function dockerMountpointPatternOptions(pattern) {
const options = readRecord(pattern.options);
return {
prefix: (0, typeGuards_1.readOptionalString)(options, 'prefix'),
region: (0, typeGuards_1.readOptionalString)(options, 'region'),
endpointUrl: (0, typeGuards_1.readOptionalString)(options, 'endpointUrl'),
};
}
function dockerS3FilesPatternOptions(pattern) {
const options = readRecord(pattern.options);
return {
mountTargetIp: (0, typeGuards_1.readOptionalString)(options, 'mountTargetIp'),
accessPoint: (0, typeGuards_1.readOptionalString)(options, 'accessPoint'),
region: (0, typeGuards_1.readOptionalString)(options, 'region'),
extraOptions: readStringNullRecord(options.extraOptions),
};
}
function dockerRcloneRemoteName(entry, pattern, mountPath) {
const remoteName = rclonePatternString(pattern, 'remoteName') ??
rclonePatternString(pattern, 'remote');
if (!remoteName) {
return `sandbox_${sanitizeDockerMountName(entry.type)}_${dockerPathHash(mountPath)}`;
}
if (!/^[A-Za-z0-9_-]+$/u.test(remoteName)) {
throw new errors_2.SandboxMountError('DockerSandboxClient rclone mounts require remoteName to contain only letters, numbers, underscores, and hyphens.', {
mountType: entry.type,
remoteName,
}, 'mount_config_invalid');
}
return remoteName;
}
function resolveDockerRcloneConfigPath(manifest, configFilePath) {
return new workspacePaths_1.WorkspacePathPolicy({
root: manifest.root,
extraPathGrants: manifest.extraPathGrants,
}).resolve(configFilePath).path;
}
function supplementDockerRcloneConfigText(configText, remoteName, requiredConfigText, mountType) {
const escapedRemote = remoteName.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
const sectionPattern = new RegExp(`^\\s*\\[${escapedRemote}\\]\\s*$`, 'mu');
const match = sectionPattern.exec(configText);
if (!match) {
throw new errors_2.SandboxMountError('DockerSandboxClient rclone config file is missing the required remote section.', {
mountType,
remoteName,
}, 'mount_config_invalid');
}
const sectionStart = match.index;
const sectionEnd = match.index + match[0].length;
const nextSection = /^\s*\[.+\]\s*$/mu.exec(configText.slice(sectionEnd));
const sectionBodyEnd = nextSection
? sectionEnd + nextSection.index
: configText.length;
const before = configText.slice(0, sectionStart);
const sectionBody = configText.slice(sectionStart, sectionBodyEnd).trimEnd();
const after = configText.slice(sectionBodyEnd);
const requiredLines = requiredConfigText.trimEnd().split('\n').slice(1);
const supplement = requiredLines.length > 0 ? `\n${requiredLines.join('\n')}` : '';
return `${before}${sectionBody}${supplement}\n${after}`;
}
function dockerRclonePatternArgs(pattern) {
return [
...(rclonePatternStringArray(pattern, 'args') ?? []),
...(rclonePatternStringArray(pattern, 'extraArgs') ?? []),
];
}
function rclonePatternString(pattern, key) {
return (0, typeGuards_1.readOptionalString)(pattern, key);
}
function rclonePatternStringArray(pattern, key) {
const value = (0, typeGuards_1.readStringArray)(pattern[key]);
return value.length > 0 ? value : undefined;
}
function readRecord(value) {
return value && typeof value === 'object' && !Array.isArray(value)
? value
: {};
}
function readStringNullRecord(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {};
}
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string' || entry[1] === null));
}
function dockerMountOptions(options) {
return Object.entries(options)
.map(([key, value]) => (value === null ? key : `${key}=${value}`))
.join(',');
}
function shellCommand(parts) {
return parts.map((part) => (0, shell_1.shellQuote)(part)).join(' ');
}
function sanitizeDockerMountName(value) {
return value.replace(/[^A-Za-z0-9_-]/gu, '_');
}
function dockerPathHash(path) {
return (0, node_crypto_1.createHash)('sha256').update(path).digest('hex').slice(0, 12);
}
function dockerPosixDirname(path) {
const index = path.lastIndexOf('/');
if (index <= 0) {
return index === 0 ? '/' : '.';
}
return path.slice(0, index);
}
function resolveDockerMountPath(manifestRoot, logicalPath, entry) {
if (entry.mountPath?.startsWith('/')) {
return entry.mountPath;
}
const mountPath = entry.mountPath ?? logicalPath;
return (0, localWorkspace_1.joinSandboxLogicalPath)(manifestRoot, mountPath);
}
function resolveDockerMountWorkspaceRelativePath(manifestRoot, logicalPath, entry) {
const target = resolveDockerMountPath(manifestRoot, logicalPath, entry);
if (target === '/') {
throw new errors_2.SandboxUnsupportedFeatureError('DockerSandboxClient does not support mounting over the container root.');
}
try {
const resolved = new workspacePaths_1.WorkspacePathPolicy({ root: manifestRoot }).resolve(target, { forWrite: true });
return resolved.workspaceRelativePath ?? null;
}
catch {
return null;
}
}
function dockerMountArg(args) {
return [
dockerMountField('type', args.type),
dockerMountField('source', args.source),
dockerMountField('target', args.target),
...(args.readOnly ? ['readonly'] : []),
...(args.volumeDriver
? [dockerMountField('volume-driver', args.volumeDriver)]
: []),
...Object.entries(args.volumeOptions ?? {}).map(([key, value]) => dockerMountField('volume-opt', `${key}=${value}`)),
].join(',');
}
function dockerMountField(key, value) {
return dockerMountCsvField(`${key}=${value}`);
}
function dockerMountCsvField(field) {
if (!/[",\n\r]/u.test(field)) {
return field;
}
return `"${field.replace(/"/gu, '""')}"`;
}
function dockerVolumeName(containerName, mountPath) {
const pathHash = (0, node_crypto_1.createHash)('sha256')
.update(mountPath)
.digest('hex')
.slice(0, 12);
const safePath = mountPath.replace(/[^A-Za-z0-9_.-]+/gu, '_').replace(/^_+|_+$/gu, '') ||
'workspace';
return `${containerName}-${pathHash}-${safePath.slice(0, 80)}`;
}
async function removeDockerVolumes(volumeNames) {
await Promise.all(volumeNames.map(async (volumeName) => {
await (0, runProcess_1.runSandboxProcess)('docker', ['volume', 'rm', '-f', volumeName], {
timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS,
}).catch(() => undefined);
}));
}
function dockerRcloneS3Options(entry) {
return withDefinedStringValues({
type: 's3',
's3-provider': entry.s3Provider ?? 'AWS',
path: joinRemotePath(entry.bucket, entry.prefix),
's3-access-key-id': entry.accessKeyId,
's3-secret-access-key': entry.secretAccessKey,
's3-session-token': entry.sessionToken,
's3-endpoint': entry.endpointUrl,
's3-region': entry.region,
});
}
function dockerMountpointS3Options(entry) {
return withDefinedStringValues({
bucket: entry.bucket,
access_key_id: entry.accessKeyId,
secret_access_key: entry.secretAccessKey,
session_token: entry.sessionToken,
endpoint_url: entry.endpointUrl,
region: entry.region,
prefix: entry.prefix,
});
}
function dockerRcloneGcsOptions(entry) {
if (entry.accessId && entry.secretAccessKey) {
return withDefinedStringValues({
type: 's3',
path: joinRemotePath(entry.bucket, entry.prefix),
's3-provider': 'GCS',
's3-access-key-id': entry.accessId,
's3-secret-access-key': entry.secretAccessKey,
's3-endpoint': entry.endpointUrl ?? 'https://storage.googleapis.com',
's3-region': entry.region,
});
}
return withDefinedStringValues({
type: 'google cloud storage',
path: joinRemotePath(entry.bucket, entry.prefix),
'gcs-service-account-file': entry.serviceAccountFile,
'gcs-service-account-credentials': entry.serviceAccountCredentials,
'gcs-access-token': entry.accessToken,
});
}
function dockerMountpointGcsOptions(entry) {
return withDefinedStringValues({
bucket: entry.bucket,
endpoint_url: entry.endpointUrl ?? 'https://storage.googleapis.com',
access_key_id: entry.accessId,
secret_access_key: entry.secretAccessKey,
region: entry.region,
prefix: entry.prefix,
});
}
function dockerRcloneR2Options(entry) {
validateCredentialPair('R2', entry.type, entry.accessKeyId, entry.secretAccessKey);
if (!entry.accountId) {
throw new errors_2.SandboxMountError('R2 Docker volume mounts require accountId.', { mountType: entry.type }, 'mount_config_invalid');
}
return withDefinedStringValues({
type: 's3',
path: joinRemotePath(entry.bucket, entry.prefix),
's3-provider': 'Cloudflare',
's3-endpoint': entry.customDomain ??
`https://${entry.accountId}.r2.cloudflarestorage.com`,
's3-access-key-id': entry.accessKeyId,
's3-secret-access-key': entry.secretAccessKey,
});
}
function dockerRcloneAzureBlobOptions(entry) {
return withDefinedStringValues({
type: 'azureblob',
path: joinRemotePath(entry.container, entry.prefix),
'azureblob-account': entry.account ?? entry.accountName,
'azureblob-endpoint': azureBlobEndpoint(entry),
'azureblob-msi-client-id': entry.identityClientId,
'azureblob-key': entry.accountKey,
});
}
function dockerRcloneBoxOptions(entry) {
return withDefinedStringValues({
type: 'box',
path: normalizeBoxRemotePath(entry.path),
'box-client-id': entry.clientId,
'box-client-secret': entry.clientSecret,
'box-access-token': entry.accessToken,
'box-token': entry.token,
'box-box-config-file': entry.boxConfigFile,
'box-config-credentials': entry.configCredentials,
'box-box-sub-type': entry.boxSubType && entry.boxSubType !== 'user'
? entry.boxSubType
: undefined,
'box-root-folder-id': entry.rootFolderId,
'box-impersonate': entry.impersonate,
'box-owned-by': entry.ownedBy,
});
}
function joinRemotePath(base, prefix) {
const normalizedPrefix = prefix?.replace(/^\/+|\/+$/gu, '');
return normalizedPrefix ? `${base}/${normalizedPrefix}` : base;
}
function normalizeBoxRemotePath(path) {
return path?.replace(/^\/+/gu, '') ?? '';
}
function withDefinedStringValues(values) {
return Object.fromEntries(Object.entries(values).filter((entry) => typeof entry[1] === 'string'));
}
function parseDockerPortBinding(stdout, containerPort) {
const line = stdout
.split(/\r?\n/u)
.map((value) => value.trim())
.find(Boolean);
if (!line) {
throw new errors_1.UserError(`Docker did not report a host binding for exposed port ${containerPort}.`);
}
const bracketMatch = line.match(/^\[([^\]]+)\]:(\d+)$/u);
const match = bracketMatch ?? line.match(/^(.+):(\d+)$/u);
if (!match) {
throw new errors_1.UserError(`Docker reported an unrecognized host binding for exposed port ${containerPort}: ${line}`);
}
const rawHost = match[1];
return {
host: normalizeDockerBindingHost(rawHost),
port: (0, session_1.normalizeExposedPort)(Number(match[2])),
};
}
function normalizeDockerBindingHost(host) {
if (host === '0.0.0.0') {
return '127.0.0.1';
}
if (host === '::' || host === '') {
return '::1';
}
return host;
}
function getHostDockerUser() {
if (typeof process.getuid !== 'function' ||
typeof process.getgid !== 'function') {
return undefined;
}
return `${process.getuid()}:${process.getgid()}`;
}
//# sourceMappingURL=docker.js.map