UNPKG

@openai/agents-core

Version:

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

3,599 lines • 161 kB
import { UserError } from "../../errors.mjs";
import { applyDiff } from "../../utils/applyDiff.mjs";
import { SandboxArchiveError, SandboxConfigurationError, SandboxLifecycleError, SandboxMountError, SandboxUnsupportedFeatureError, SandboxWorkspaceReadNotFoundError, } from "../errors.mjs";
import { chmod, mkdir, mkdtemp, realpath, rm, stat } from 'node:fs/promises';
import { createHash, randomUUID } from 'node:crypto';
import { spawn } from 'node:child_process';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep, } from 'node:path';
import { tmpdir } from 'node:os';
import { Header, Pax } from 'tar';
import { isMount, } from "../entries/index.mjs";
import { normalizeSandboxClientCreateArgs, resolveSandboxArchiveLimits, } from "../client.mjs";
import { cloneManifest, copyManifestMountCredentialExposurePolicy, isEnvValueReference, Manifest, } from "../manifest.mjs";
import { WorkspacePathPolicy, } from "../workspacePaths.mjs";
import { getRecordedExposedPortEndpoint, normalizeExposedPort, recordExposedPortEndpoint, } from "../session.mjs";
import { UnixLocalSandboxSession, } from "./unixLocal.mjs";
import { permissionsForSandboxEntry } from "../permissions.mjs";
import { assertLocalWorkspaceManifestMetadataSupported, joinSandboxLogicalPath, materializeLocalWorkspaceManifest, materializeLocalWorkspaceManifestMounts, restoreLocalWorkspaceManifestStickyPermissions, pathExists, readLocalWorkspaceEntry, } from "./shared/localWorkspace.mjs";
import { assertHostPathGrantsRebound, assertMountCredentialsRebound, mergeManifestEntryDelta, mergeManifestDelta, sanitizeEnvironmentForPersistence, serializeHostPathGrantRedactionMetadata, serializeMountCredentialRedactionMetadata, serializeManifest, } from "./shared/manifestPersistence.mjs";
import { assertExistingMountTopologyPreserved, assertSandboxSessionStateUsable, assertSandboxStateGenerationUnchanged, captureLiveMountCredentialAuthority, captureSandboxStateGeneration, copyValidatedMountEffectivePaths, isSandboxSessionStateUnsafe, liveMountEnvironmentAuthorityMatches, manifestHasInContainerMounts, mountCredentialFileReferences, markSandboxSessionStateUnsafe, recordLiveMountCredentialAuthority, sanitizeMountCredentialEnvironmentForPersistence, validateMountCredentialBoundariesAtEffectivePath, validateMountCredentialBoundaries, validateMountCredentialFileEffectivePaths, validateMountEnvironmentCredentialBoundaries, withExclusiveSandboxManifestMutation, } from "../mountSecurity.mjs";
import { imageOutputFromBytes, MAX_VIEW_IMAGE_BYTES } from "../shared/media.mjs";
import { canReuseLocalSnapshotWorkspace, localSnapshotIsRestorable, persistLocalSnapshot, restoreLocalSnapshotToWorkspace, serializeLocalSnapshotSpec, } from "./shared/localSnapshots.mjs";
import { spawnInPseudoTerminal } from "./shared/pty.mjs";
import { preparedFileIO } from "./shared/unixLocalFiles.mjs";
import { formatSandboxProcessError, runSandboxProcess, } from "./shared/runProcess.mjs";
import { resolveFallbackShellCommand } from "./shared/shellCommand.mjs";
import { shellQuote } from "../shared/shell.mjs";
import { normalizePosixPath } from "../shared/posixPath.mjs";
import { probeSandboxDirectoryExists, probeSandboxPathExists, } from "../shared/pathProbe.mjs";
import { deserializeLocalSandboxSessionStateValues, normalizeExposedPorts, } from "./shared/sessionStateValues.mjs";
import { isStringRecord, readOptionalNumberArray, readOptionalString, readString, readStringArray, } from "../shared/typeGuards.mjs";
import { validateCredentialPair as validateSandboxCredentialPair } from "../shared/credentials.mjs";
import { isHostPathWithinRoot, sandboxPathGrantHostPath, } from "../shared/hostPath.mjs";
import { getRunStateSessionTrustedConfig, isRunStateSessionState, } from "../internal/sessionStateTrust.mjs";
import { stableJsonStringify } from "../shared/stableJson.mjs";
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,
]);
export class DockerSandboxSession extends UnixLocalSandboxSession {
    containerClosed = false;
    closeStarted = false;
    stagedMountEnvironment;
    mountedPathGrants;
    constructor(args) {
        // Container file APIs do not use a host file worker.
        super({ ...args, [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 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 UserError('Docker sandbox session is closed.');
        }
    }
    async viewImage(args) {
        const bytes = await this.readDockerFileAs(args.path, args.runAs, {
            maxBytes: MAX_VIEW_IMAGE_BYTES,
            image: true,
        });
        return imageOutputFromBytes(args.path, bytes);
    }
    async pathExists(path, runAs) {
        const absolutePath = await this.validateContainerFilesystemPath(path);
        return await probeSandboxPathExists({
            path: absolutePath,
            runCommand: async (command) => await this.runDockerFilesystemCommand(command, { runAs }),
        });
    }
    async directoryExists(path, runAs) {
        const absolutePath = await this.validateContainerFilesystemPath(path);
        return await 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 ${shellQuote(absolutePath)} && find -H ${shellQuote(absolutePath)} -mindepth 1 -maxdepth 1 -printf '%y\\0%f\\0'`,
        ].join(' && '), { runAs: args.runAs }, `list directory ${absolutePath}`, absolutePath);
        const resolvedPath = new 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 (!isMount(entry)) {
            await withExclusiveSandboxManifestMutation(this.state, async () => {
                this.assertSessionUsable();
                await this.resolveFilesystemRunAs(args.runAs);
                const logicalPath = this.resolveLogicalPath(args.path);
                const nextManifest = mergeManifestEntryDelta(this.state.manifest, logicalPath, entry);
                assertExistingMountTopologyPreserved(this.state.manifest, nextManifest);
                validateMountCredentialBoundaries(nextManifest);
                const permissions = await this.copyEntryIntoContainer(logicalPath, entry);
                await this.applyContainerCopyPermissions(permissions);
                recordLiveMountCredentialAuthority(nextManifest, this.state.manifest);
                this.state.manifest = nextManifest;
            });
            return;
        }
        if (!isDockerInContainerMount(entry)) {
            throw new SandboxUnsupportedFeatureError('Docker bind and volume mounts must be configured when creating the sandbox.');
        }
        await 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, 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 = 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 UserError(`DockerSandboxClient failed to copy entry: ${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 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 UserError(`DockerSandboxClient failed to set entry permissions: ${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: posix.join(destination, content.path) };
            const parents = [];
            for (let parent = posix.dirname(item.path); parent !== this.state.manifest.root &&
                pathWithinDockerMount(parent, this.state.manifest.root); parent = 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 UserError('Cannot verify a private Git temporary directory.');
        }
        const parent = await realpath(tmpdir());
        if (mounts.some((mount) => isHostPathWithinRoot(mount.source, parent))) {
            throw new SandboxUnsupportedFeatureError('Git resources require a host temporary directory outside the sandbox mounts. Configure TMPDIR to an unshared directory.');
        }
        return await mkdtemp(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 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 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 UserError(`DockerSandboxClient failed to inspect entry destination: ${formatSandboxProcessError(result)}`);
        }
        const size = item.type === 'file'
            ? 'chunks' in item
                ? item.size
                : item.content.byteLength
            : 0;
        const header = new Header({
            path: 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 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 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 = mergeManifestEntryDelta(this.state.manifest, this.resolveLogicalPath(args.path), args.entry);
        assertExistingMountTopologyPreserved(this.state.manifest, nextManifest);
        validateMountCredentialBoundaries(nextManifest);
        assertDockerReadOnlyHostBindsSupported(nextManifest);
        validateMountEnvironmentCredentialBoundaries(nextManifest, this.state.environment);
        const logicalPath = this.resolveLogicalPath(args.path);
        assertDockerCanApplyInContainerMounts(this.state.manifest, new Manifest({
            entries: {
                [logicalPath]: args.entry,
            },
        }));
        assertLocalWorkspaceManifestMetadataSupported('DockerSandboxClient', new 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);
            copyValidatedMountEffectivePaths(nextManifest, this.state.manifest, nextManifest);
            this.state.manifest = nextManifest;
            captureLiveMountCredentialAuthority(this.state.manifest, this.state.environment);
        }
        catch (error) {
            await this.invalidateAfterFailedPrivilegedManifestTransition();
            throw error;
        }
    }
    async applyManifest(manifest, runAs) {
        const manifestSnapshot = cloneManifest(manifest);
        await withExclusiveSandboxManifestMutation(this.state, async () => {
            await this.applyDockerManifestExclusive(manifestSnapshot, runAs);
        });
    }
    async applyDockerManifestExclusive(manifest, runAs) {
        const previousManifest = this.state.manifest;
        const nextManifest = mergeManifestDelta(this.state.manifest, manifest);
        assertExistingMountTopologyPreserved(this.state.manifest, nextManifest);
        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,
        };
        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 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 (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(mergeManifestDelta(this.state.manifest, materializedManifest), manifest);
            copyValidatedMountEffectivePaths(committedManifest, this.state.manifest, manifest);
            this.state.manifest = committedManifest;
            this.state.environment = nextEnvironment;
            if (manifestHasInContainerMounts(manifest)) {
                captureLiveMountCredentialAuthority(this.state.manifest, this.state.environment);
            }
            else {
                recordLiveMountCredentialAuthority(this.state.manifest, previousManifest);
            }
        }
        catch (error) {
            if (manifestHasInContainerMounts(manifest) &&
                providerEffectsMayHaveStarted) {
                await this.invalidateAfterFailedPrivilegedManifestTransition();
            }
            throw error;
        }
        finally {
            this.stagedMountEnvironment = undefined;
        }
    }
    async resolveExposedPort(port) {
        this.assertSessionUsable();
        const containerPort = normalizeExposedPort(port);
        const configuredPorts = this.state.configuredExposedPorts ?? [];
        if (configuredPorts.length > 0 &&
            !configuredPorts.includes(containerPort)) {
            throw new SandboxConfigurationError(`DockerSandboxClient was not configured to expose port ${containerPort}.`, {
                provider: 'DockerSandboxClient',
                port: containerPort,
                configuredPorts,
            });
        }
        const recorded = getRecordedExposedPortEndpoint(this.state, containerPort);
        if (recorded) {
            return recorded;
        }
        const result = await runSandboxProcess('docker', ['port', this.state.containerId, `${containerPort}/tcp`], {
            timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS,
        });
        if (result.status !== 0) {
            throw new UserError(`Failed to resolve Docker exposed port ${containerPort}: ${formatSandboxProcessError(result)}`);
        }
        return recordExposedPortEndpoint(this.state, {
            ...parseDockerPortBinding(result.stdout, containerPort),
            tls: false,
        }, containerPort);
    }
    resolveCommandWorkdir(path) {
        const resolvedPath = new WorkspacePathPolicy({
            root: this.state.manifest.root,
            extraPathGrants: this.mountedPathGrants,
        }).resolve(path);
        if (resolvedPath.grant) {
            return resolvedPath.path;
        }
        const logicalPath = this.resolveLogicalPath(path);
        return joinSandboxLogicalPath(this.state.manifest.root, logicalPath);
    }
    async spawnShellCommand(command, args) {
        const { shellPath, flag } = 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 spawnInPseudoTerminal('docker', dockerArgs);
        }
        return spawn('docker', dockerArgs, {
            stdio: 'pipe',
        });
    }
    translateCommandInput(command) {
        return command;
    }
    translateCommandOutput(output) {
        return output;
    }
    async materializeRestoredWorkspaceMounts() {
        await prepareDockerWorkspaceRoot(this.state.workspaceRootPath, this.state.manifest);
        await 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 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 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 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 &&
                sandboxPathGrantHostPath(grant) ===
                    sandboxPathGrantHostPath(selectedGrant))) {
            throw new 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 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 UserError('Docker filesystem path resolution returned an invalid path.');
        }
        if (this.resolveContainerFilesystemPath(resolved, options) !== resolved) {
            throw new 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 ${shellQuote(path)} ] && [ ! -f ${shellQuote(path)} ]; then exit 67; fi; `;
        const imageGuard = imageLimit === undefined
            ? ''
            : `size=$(wc -c < ${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 -- ${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 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 UserError(`${image ? 'Image' : 'File'} path is not a file: ${path}`);
        if (image) {
            if (result.status === 68)
                throw new 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 probeSandboxPathExists({
                    path,
                    runCommand: async (command) => await this.runDockerFilesystemCommand(command, options),
                    createError: () => new UserError(`DockerSandboxClient failed to ${action}: ${formatSandboxProcessError(result)}`),
                });
                if (!exists) {
                    if (image)
                        throw new UserError(`Image file not found: ${path}`);
                    throw new SandboxWorkspaceReadNotFoundError(`DockerSandboxClient path does not exist: ${path}`, { path });
                }
            }
            throw new UserError(`DockerSandboxClient failed to ${action}: ${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 ${shellQuote(path)} ] || [ -L ${shellQuote(path)} ]; then echo 'File already exists.' >&2; exit 1; fi; set -C; `
            : `if [ -e ${shellQuote(path)} ] && [ ! -f ${shellQuote(path)} ]; then echo 'Destination is not a regular file.' >&2; exit 1; fi; `;
        const moveGuard = moveSource
            ? `if [ ${shellQuote(moveSource)} -ef ${shellQuote(path)} ]; then echo 'Cannot move a file onto itself.' >&2; exit 1; fi; `
            : '';
        const write = `${moveGuard}${guard}cat > ${shellQuote(path)}`;
        await this.runCheckedDockerFilesystemCommand(parent === '/' || parent === '.'
            ? write
            : `mkdir -p -- ${shellQuote(parent)} && (${write})`, { runAs, input: content }, `write file ${path}`);
    }
    async deleteDockerPathAs(path, runAs) {
        path = await this.validateContainerFilesystemPath(path, { forWrite: true });
        await this.runCheckedDockerFilesystemCommand(`rm -- ${shellQuote(path)}`, { runAs }, `delete path ${path}`);
    }
    async mkdirDockerPathAs(path, runAs) {
        await this.runCheckedDockerFilesystemCommand(`mkdir -p -- ${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 UserError(`DockerSandboxClient failed to ${action}.`);
        }
        if (result.status !== 0) {
            throw new UserError(`DockerSandboxClient failed to ${action} with exit status ${result.status}.`);
        }
        return result.stdout;
    }
    async chownContainerPath(path, runAs) {
        await this.runCheckedDockerFilesystemCommand(`chown -R ${shellQuote(runAs)}:${shellQuote(runAs)} -- ${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 UserError(`DockerSandboxClient failed to ${action} with exit status ${result.status}.`);
            }
            throw new UserError(`DockerSandboxClient failed to ${action}: ${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() {
        markSandboxSessionStateUnsafe(this.state);
        try {
            await removeDockerContainer(this.state.containerId, {
                ignoreMissing: true,
            });
            this.containerClosed = true;
        }
        catch {
            // The tombstone remains authoritative when force termination fails.
        }
    }
}
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 = 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 = 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.
 */
export 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 = 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();
        validateMountEnvironmentCredentialBoundaries(manifest, environment);
        await ensureDockerAvailable();
        await this.retryFailedCreateCleanups();
        const workspaceRootPath = await mkdtemp(join(resolvedOptions.workspaceBaseDir ?? tmpdir(), 'openai-agents-docker-sandbox-'));
        await 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 = 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);
            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 SandboxLifecycleError('Docker sandbox creation failed and cleanup could not complete.', { errors: [error, cleanupError] });
            }
            throw error;
        }
        return session;
    }
    async resume(state, options = {}) {
        const trustedConfig = getRunStateSessionTrustedConfig(state);
        this.validateSessionStateForResume({
            source: trustedConfig ? 'runState' : 'explicit',
            state,
        }, trustedConfig
            ? {
                ...options,
                clientOptions: trustedConfig.clientOptions,
            }
            : options);
        let resumeState = state;
        if (!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,
            };
        }
        assertMountCredentialsRebound(resumeState);
        assertHostPathGrantsRebound(resumeState);
        assertDockerManifestSupported(resumeState.manifest);
        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'
            ? normalizeExposedPorts(input.state.configuredExposedPorts)
            : normalizeExposedPorts(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 (isSandboxSessionStateUnsafe(state)) {
            return false;
        }
        if (isRunStateSessionState(state)) {
            return false;
        }
        assertDockerReadOnlyHostBindsSupported(state.manifest);
        if (!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 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 = {}) {
        assertSandboxSessionStateUsable(state);
        const labels = normalizeDockerLabels(state.labels, 'Docker sandbox session state labels');
        validateMountEnvironmentCredentialBoundaries(state.manifest, state.environment);
        const stateGeneration = captureSandboxStateGeneration(state);
        const snapshotSpec = state.snapshotSpec ?? this.options.snapshot ?? null;
        attachDockerSnapshotExcludedPaths(state);
        const snapshot = await persistLocalSnapshot('DockerSandboxClient', state, snapshotSpec);
        state.snapshotSpec = snapshotSpec;
        if (options.preserveOwnedSession === true &&
            options.reuseLiveSession === false &&
            options.willCloseAfterSerialize === true &&
            snapshot === null) {
            throw new UserError('Docker sandbox session cannot be preserved after live reuse was rejected because no restorable snapshot is configured.');
        }
        const sanitizedMountEnvironment = sanitizeMountCredentialEnvironmentForPersistence(state);
        const serialized = {
            ...serializeHostPathGrantRedactionMetadata(state),
            ...serializeMountCredentialRedactionMetadata(state),
            manifest: serializeManifest(sanitizedMountEnvironment.manifest),
            sessionIdentity: state.sessionIdentity,
            workspaceRootPath: state.workspaceRootPath,
            workspaceRootOwned: state.workspaceRootOwned,
            environment: sanitizeEnvironmentForPersistence(sanitizedMountEnvironment),
            snapshotSpec: 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,
        };
        assertSandboxStateGenerationUnchanged(state, stateGeneration);
        return serialized;
    }
    async deserializeSessionState(state) {
        const configuredExposedPorts = normalizeExposedPorts(readOptionalNumberArray(state.configuredExposedPorts));
        const networkMode = validateDockerNetworkConfiguration(state.networkMode, configuredExposedPorts);
        const baseState = await deserializeLocalSandboxSessionStateValues(state, this.options.snapshot);
        return {
            ...baseState,
            sessionIdentity: readOptionalString(state, 'sessionIdentity'),
            image: readString(state, 'image'),
            containerId: readString(state, 'containerId'),
            defaultUser: readOptionalString(state, 'defaultUser') ?? getHostDockerUser(),
            networkMode,
            labels: normalizeDockerLabels(state.labels, 'Serialized Docker sandbox session state labels'),
            dockerVolumeNames: readStringArray(state.dockerVolumeNames),
        };
    }
    async restoreIfNeeded(state, archiveLimits) {
        attachDockerSnapshotExcludedPaths(state);
        if (isRunStateSessionState(state)) {
            const trustedConfig = 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 localSnapshotIsRestorable(trustedState))) {
                throw new 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 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 UserError('Existing Docker sandbox labels do not match required labels. Start a fresh sandbox session instead.');
                }
                if (!inspection.ownershipVerified) {
                    throw new 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 canReuseLocalSnapshotWorkspace(state)) {
                await this.cleanupDockerResources(state);
                return await this.restartContainer(state, state.workspaceRootPath, archiveLimits);
            }
            if (await localSnapshotIsRestorable(state)) {
                const restoredState = await restoreLocalSnapshotToWorkspace(state, state.workspaceRootPath, { archiveLimits });
                await 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 UserError('Docker sandbox container identity could not be verified. Refusing to resume or replace the running container.');
            }
            if (!dockerRequiredLabelsMatch(state.labels, labels)) {
                throw new UserError('Existing Docker sandbox labels do not match required labels. Start a fresh sandbox session instead.');
            }
        }
        if (!(await localSnapshotIsRestorable(state))) {
            throw new 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 mkdtemp(join(workspaceBaseDir ?? tmpdir(), 'openai-agents-docker-sandbox-'));
        try {
            const restoredState = await restoreLocalSnapshotToWorkspace({
                ...state,
                workspaceRootPath,
                workspaceRootOwned: true,
            }, workspaceRootPath, { archiveLimits });
            await restoreLocalWorkspaceManifestStickyPermissions(restoredState.manifest, workspaceRootPath);
            return await this.restartContainer(restoredState, workspaceRootPath, archiveLimits);
        }
        catch (error) {
            await 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 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 ?? 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 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);
        }
    }
}
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 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,
        isEnvValueReference(value) && typeof state.environment[key] === 'string'
            ? state.environment[key]
            : await value.resolve(),
    ])));
}
function assertDockerManifestSupported(manifest) {
    validateMountCredentialBoundaries(manifest);
    assertDockerReadOnlyHostBindsSupported(manifest);
    assertDockerManifestRootSupported(manifest);
    for (const grant of manifest.extraPathGrants) {
        sandboxPathGrantHostPath(grant);
    }
    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 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 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 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 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 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 realpath(sourcePath);
    const sourceStats = await 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(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 createHash('sha256')
        .update(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 createHash('sha256')
        .update(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 (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 (!isStringRecord(value)) {
        throw new 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 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 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 UserError('DockerSandboxClient networkMode must be "none" when provided.');
    }
    if (networkMode === 'none' && (exposedPorts?.length ?? 0) > 0) {
        throw new UserError('DockerSandboxClient exposedPorts cannot be used when networkMode is "none".');
    }
    return networkMode;
}
function resolveDockerNetworkConfiguration(clientOptions, perRunOptions, explicitStateFallback) {
    const trustedExposedPorts = perRunOptions?.exposedPorts !== undefined
        ? normalizeExposedPorts(perRunOptions.exposedPorts)
        : clientOptions.exposedPorts !== undefined
            ? normalizeExposedPorts(clientOptions.exposedPorts)
            : undefined;
    const exposedPorts = explicitStateFallback
        ? normalizeExposedPorts(explicitStateFallback.configuredExposedPorts)
        : (trustedExposedPorts ?? []);
    if (explicitStateFallback &&
        trustedExposedPorts !== undefined &&
        !dockerExposedPortSetsEqual(trustedExposedPorts, exposedPorts)) {
        throw new 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 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,
        });
    }
    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 UserError('DockerSandboxClient does not support manifest root "/".');
    }
}
async function prepareDockerWorkspaceRoot(workspaceRootPath, manifest) {
    if (manifest.users.length === 0 && manifest.groups.length === 0) {
        return;
    }
    const sticky = permissionsForSandboxEntry(manifest.entries['']?.permissions).sticky;
    await chmod(workspaceRootPath, 0o755 | (sticky ? 0o1000 : 0));
}
async function provisionDockerAccounts(containerId, manifest) {
    for (const command of dockerAccountProvisionCommands(manifest)) {
        const result = await runSandboxProcess('docker', ['exec', '-u', 'root', containerId, '/bin/sh', '-c', command], {
            timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS,
        });
        if (result.status !== 0) {
            throw new UserError(`Failed to provision Docker sandbox manifest accounts: ${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 ${shellQuote(group.name)} >/dev/null 2>&1 || groupadd ${shellQuote(group.name)}`);
        for (const user of group.users ?? []) {
            users.add(user.name);
        }
    }
    for (const user of users) {
        const quotedUser = 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 ${shellQuote(group.name)} ${shellQuote(user.name)}`);
        }
    }
    return commands;
}
function stripDockerIdentityMetadata(manifest) {
    const stripped = new 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,
    });
    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]) => !isMount(entry))
        .map(([path, entry]) => [
        path,
        entry.type === 'dir'
            ? {
                ...entry,
                children: withoutDockerMountEntries(entry.children ?? {}),
            }
            : entry,
    ]));
}
function mergeDockerIdentityMetadata(current, delta) {
    return mergeManifestDelta(current, new Manifest({
        users: delta.users,
        groups: delta.groups,
    }));
}
async function ensureDockerAvailable() {
    const result = await runSandboxProcess('docker', ['version'], {
        timeoutMs: DOCKER_FAST_COMMAND_TIMEOUT_MS,
    });
    if (result.status !== 0) {
        throw new UserError('Docker sandbox execution requires a working Docker CLI and daemon.');
    }
}
async function inspectContainerRunning(containerId) {
    const result = await 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 UserError(`Failed to inspect Docker sandbox container: ${formatSandboxProcessError(result)}`);
    }
    const running = result.stdout.trim();
    if (running === 'true') {
        return true;
    }
    if (running === 'false') {
        return false;
    }
    throw new 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 = 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 SandboxArchiveError(`Docker resource ${kind} exceeds limit.`, {
                reason: `archive ${kind} exceeds limit`,
                limit,
                actual,
                ...(member === undefined ? {} : { member }),
            });
        }
    }
}
async function runDockerProcess(args, input, maxOutputBytes) {
    const child = 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 Readable) {
        try {
            await 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 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 runSandboxProcess('docker', ['rm', '-f', containerId], {
        timeoutMs: DOCKER_CONTAINER_REMOVE_TIMEOUT_MS,
    });
    if (result.status !== 0) {
        if (options.ignoreMissing && isMissingDockerContainerError(result)) {
            return;
        }
        throw new UserError(`Failed to remove Docker sandbox container: ${formatSandboxProcessError(result)}`);
    }
}
function isMissingDockerContainerError(result) {
    const message = 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 = 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-${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 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 UserError(`Failed to start Docker sandbox container: ${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 mkdir(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' &&
        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 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 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 = 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 ||
                stableJsonStringify(current.entry) !==
                    stableJsonStringify(candidate.entry) ||
                stableJsonStringify(current.environment) !==
                    stableJsonStringify(candidate.environment) ||
                stableJsonStringify(current.pattern) !==
                    stableJsonStringify(candidate.pattern)) {
                throw new 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 = mountCredentialFileReferences(manifest, entry, environment);
    if (references.length === 0) {
        return { entry: structuredClone(entry), environment };
    }
    const resolveEffectivePath = async (path) => {
        const candidate = path.startsWith('/')
            ? normalizePosixPath(path)
            : joinSandboxLogicalPath(manifest.root, path);
        const resolvedPath = (await resolveTrustedDockerPath(session, candidate, 'resolve Docker mount credential path')).trim();
        if (!resolvedPath.startsWith('/') || resolvedPath.includes('\n')) {
            throw new SandboxMountError('DockerSandboxClient failed to resolve an absolute mount credential path.', { mountType: entry.type }, 'mount_config_invalid');
        }
        return normalizePosixPath(resolvedPath);
    };
    const manifestEntries = await Promise.all([...manifest.iterEntries()]
        .filter(({ entry }) => !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 SandboxMountError('DockerSandboxClient failed to resolve the rclone config file.', { mountType: entry.type }, 'mount_config_invalid');
            }
        }
        return { field, path: await resolveEffectivePath(path) };
    }));
    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 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 UserError(`DockerSandboxClient failed to ${action}.`);
    }
    if (result.status !== 0) {
        throw new 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 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 SandboxMountError('DockerSandboxClient failed to resolve an absolute mount path.', { mountType: entry.type }, 'mount_config_invalid');
        }
        return normalizePosixPath(resolvedPath);
    }
    const workspaceRoot = await realpath(session.state.workspaceRootPath);
    const effectiveHostPath = await realpathWithMissingTail(resolve(session.state.workspaceRootPath, workspaceRelativePath));
    const effectiveWorkspacePath = relative(workspaceRoot, effectiveHostPath);
    if (effectiveWorkspacePath === '..' ||
        effectiveWorkspacePath.startsWith(`..${sep}`) ||
        isAbsolute(effectiveWorkspacePath)) {
        throw new SandboxMountError('DockerSandboxClient mount path resolves outside the sandbox workspace.', { mountType: entry.type }, 'mount_config_invalid');
    }
    return joinSandboxLogicalPath(session.state.manifest.root, effectiveWorkspacePath.split(sep).join('/'));
}
async function realpathWithMissingTail(path) {
    const missingSegments = [];
    let existingPath = path;
    while (true) {
        try {
            return resolve(await realpath(existingPath), ...missingSegments);
        }
        catch (error) {
            if (!isMissingPathError(error)) {
                throw error;
            }
            const parent = dirname(existingPath);
            if (parent === existingPath) {
                throw error;
            }
            missingSegments.unshift(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 ${shellQuote(mountPath)} >/dev/null 2>&1 || true`,
                `fusermount -u ${shellQuote(mountPath)} >/dev/null 2>&1 || true`,
                `umount ${shellQuote(mountPath)} >/dev/null 2>&1 || true`,
                `umount -l ${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 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 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 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 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 -- ${shellQuote(mountPath)}`,
        `rm -rf -- ${shellQuote(configDir)}`,
        `trap ${shellQuote(`rm -rf -- ${shellQuote(configDir)}`)} EXIT`,
        `install -d -m 700 -- ${shellQuote(configDir)}`,
        `(umask 077 && cat > ${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 ${shellQuote(mountOptions.join(','))} ${shellQuote(`${host}:/`)} ${shellQuote(mountPath)}; then mounted=1; break; fi; sleep 1; done`,
            `if [ "$mounted" = 1 ]; then rm -rf -- ${shellQuote(configDir)}; exit 0; fi`,
            dockerSafeKillRcloneNfsPidFileCommand(pidPath),
            `rm -rf -- ${shellQuote(configDir)}`,
            'exit 1',
            '}',
        ].join('; ');
        await revalidateMountAuthority();
        await session.runDockerMountCommand([
            ...baseCommand,
            `(${shellCommand(serverArgs)} & printf %s "$!" > ${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 -- ${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 ${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 -- ${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 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(readOptionalString(entry, 'accessId') &&
            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(readOptionalString(entry, 'accessId'), readOptionalString(entry, 'secretAccessKey'));
    const envDir = `/tmp/openai-agents-mountpoint-env-${dockerPathHash(`${session.state.containerId}:${mountPath}`)}`;
    const envPath = `${envDir}/credentials.env`;
    const cleanupEnvCommand = `rm -rf -- ${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 ${shellQuote(cleanupEnvCommand)} EXIT HUP INT TERM`,
            cleanupEnvCommand,
            `install -d -m 700 -- ${shellQuote(envDir)}`,
            `(umask 077 && cat > ${shellQuote(envPath)})`,
            `unset ${clearInheritedCredentialAuthorityCommand}`,
            `set -a && . ${shellQuote(envPath)} && set +a`,
            cleanupEnvCommand,
            shellCommand(mountArgs),
        ].join(' && ')
        : shellCommand(mountArgs);
    await revalidateMountAuthority();
    await session.runDockerMountCommand([`mkdir -p -- ${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 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 SandboxUnsupportedFeatureError('DockerSandboxClient s3files mounts require an s3FilesMount() entry.', {
            provider: 'DockerSandboxClient',
            mountType: entry.type,
        });
    }
    const s3Files = entry;
    if (!s3Files.fileSystemId) {
        throw new 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 -- ${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 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 -- ${shellQuote(mountPath)}`,
        [
            `export OPENAI_AGENTS_MOUNT_PATH=${shellQuote(mountPath)}`,
            ...(entry.source
                ? [`export OPENAI_AGENTS_MOUNT_SOURCE=${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 SandboxMountError('azureBlobMount() requires account or accountName for Docker fuse mounts.', { mountType: entry.type }, 'mount_config_invalid');
    }
    if (entry.prefix) {
        throw new 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 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 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 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 -- ${shellQuote(mountPath)} ${shellQuote(cacheDir)}`,
        `rm -rf -- ${shellQuote(configDir)}`,
        `trap ${shellQuote(`rm -rf -- ${shellQuote(configDir)}`)} EXIT`,
        `install -d -m 700 -- ${shellQuote(configDir)}`,
        `(umask 077 && cat > ${shellQuote(configPath)})`,
        dockerEnableFuseAllowOtherCommand(),
        shellCommand(mountArgs),
        `rm -rf -- ${shellQuote(configDir)}`,
    ].join(' && '), `mount blobfuse ${entry.type}`, { input: configText });
}
function resolveDockerBlobfuseCacheDir(workspaceRoot, pattern, containerId, account, container) {
    const configuredCachePath = dockerFusePatternString(pattern, 'cachePath');
    if (configuredCachePath) {
        return joinSandboxLogicalPath(workspaceRoot, normalizeDockerBlobfuseCachePath(configuredCachePath));
    }
    return 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 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 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 SandboxMountError('DockerSandboxClient failed to resolve the rclone config file.', { mountType: entry.type }, 'mount_config_invalid');
        }
        const encodedConfig = await session.runDockerMountCommand(`base64 -- ${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 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 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 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 = readOptionalString(entry, 'accessId');
    const secretAccessKey = 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) {
    validateSandboxCredentialPair({
        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=${shellQuote(accessKeyId)}`,
        `AWS_SECRET_ACCESS_KEY=${shellQuote(secretAccessKey)}`,
        ...(sessionToken ? [`AWS_SESSION_TOKEN=${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: readOptionalString(options, 'prefix'),
        region: readOptionalString(options, 'region'),
        endpointUrl: readOptionalString(options, 'endpointUrl'),
    };
}
function dockerS3FilesPatternOptions(pattern) {
    const options = readRecord(pattern.options);
    return {
        mountTargetIp: readOptionalString(options, 'mountTargetIp'),
        accessPoint: readOptionalString(options, 'accessPoint'),
        region: 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 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 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 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 readOptionalString(pattern, key);
}
function rclonePatternStringArray(pattern, key) {
    const value = 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) => shellQuote(part)).join(' ');
}
function sanitizeDockerMountName(value) {
    return value.replace(/[^A-Za-z0-9_-]/gu, '_');
}
function dockerPathHash(path) {
    return 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 joinSandboxLogicalPath(manifestRoot, mountPath);
}
function resolveDockerMountWorkspaceRelativePath(manifestRoot, logicalPath, entry) {
    const target = resolveDockerMountPath(manifestRoot, logicalPath, entry);
    if (target === '/') {
        throw new SandboxUnsupportedFeatureError('DockerSandboxClient does not support mounting over the container root.');
    }
    try {
        const resolved = new 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 = 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 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 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 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 UserError(`Docker reported an unrecognized host binding for exposed port ${containerPort}: ${line}`);
    }
    const rawHost = match[1];
    return {
        host: normalizeDockerBindingHost(rawHost),
        port: 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.mjs.map