/**
 * DockerSandboxStrategy — full isolation via Docker containers.
 *
 * Provides deterministic filesystem, network control, and resource limits.
 * Falls back to TempDirSandboxStrategy when Docker is unavailable.
 *
 * Docker interaction uses the `docker` CLI via `execFileSync` (array form,
 * no shell) to prevent shell injection from task-supplied values like
 * image names or task IDs.
 *
 * @see docs/archive/exec-plans/architecture-overhaul/phase-4-agent-harness.md
 */
import { randomUUID } from "crypto";
import { execFileSync } from "child_process";
import { mkdirSync } from "fs";
import { tmpdir } from "os";
import { resolve } from "path";
const DEFAULT_IMAGE = "node:22-slim";
const DEFAULT_WORKDIR = "/workspace";
/** Only allow official base images to prevent pulling from untrusted registries. */
const ALLOWED_IMAGE_PATTERN = /^(node|python|ubuntu|alpine|debian|rust|golang|mcr\.microsoft\.com\/[a-z]+)(:[a-zA-Z0-9._-]+)?$/;
function validateDockerImage(image) {
    if (!ALLOWED_IMAGE_PATTERN.test(image)) {
        throw new Error(`Docker image "${image}" is not in the allowlist. ` +
            `Only official base images (node, python, ubuntu, alpine, debian, rust, golang) are permitted.`);
    }
}
export class DockerSandboxStrategy {
    name = "Docker Container";
    type = "docker";
    async isAvailable() {
        try {
            execFileSync("docker", ["info"], { stdio: "ignore", timeout: 5000 });
            return true;
        }
        catch {
            return false;
        }
    }
    async provision(options) {
        const image = options.image ?? DEFAULT_IMAGE;
        validateDockerImage(image);
        const id = `ailf-${randomUUID().slice(0, 12)}`;
        // Create a local staging directory for fixture injection
        const stagingDir = resolve(tmpdir(), `${id}-staging`);
        mkdirSync(stagingDir, { recursive: true });
        // Build docker create command as array (no shell, prevents injection)
        const args = ["create", "--name", id, "--workdir", DEFAULT_WORKDIR];
        // Security hardening — defense-in-depth against container escape
        args.push("--cap-drop", "ALL");
        args.push("--security-opt", "no-new-privileges");
        args.push("--read-only");
        args.push("--tmpfs", "/tmp:rw,noexec,nosuid,size=100m");
        // Resource limits
        if (options.limits) {
            if (options.limits.cpus) {
                args.push("--cpus", String(options.limits.cpus));
            }
            if (options.limits.memoryBytes) {
                args.push("--memory", String(options.limits.memoryBytes));
            }
            if (options.limits.networkAccess === false) {
                args.push("--network", "none");
            }
        }
        // Bind mount staging directory
        args.push("-v", `${stagingDir}:${DEFAULT_WORKDIR}`);
        args.push(image);
        args.push("sleep", "infinity"); // Keep container alive
        try {
            const containerId = execFileSync("docker", args, {
                encoding: "utf-8",
                timeout: 30_000,
            }).trim();
            // Start the container
            execFileSync("docker", ["start", id], {
                stdio: "ignore",
                timeout: 10_000,
            });
            return {
                id,
                workingDir: stagingDir,
                strategy: "docker",
                containerId: containerId || id,
                createdAt: new Date().toISOString(),
            };
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            throw new Error(`Failed to provision Docker sandbox "${id}": ${msg}`, {
                cause: err,
            });
        }
    }
    async collectArtifacts(sandbox) {
        const modifiedFiles = [];
        try {
            // Get list of modified files via docker diff (array form)
            const diff = execFileSync("docker", ["diff", sandbox.id], {
                encoding: "utf-8",
                timeout: 10_000,
            }).trim();
            if (diff) {
                for (const line of diff.split("\n")) {
                    // docker diff output: C /workspace/file.ts (C=changed, A=added, D=deleted)
                    const match = /^[ACD]\s+(.+)$/.exec(line.trim());
                    if (match && match[1].startsWith(DEFAULT_WORKDIR)) {
                        modifiedFiles.push(match[1].replace(`${DEFAULT_WORKDIR}/`, ""));
                    }
                }
            }
            return {
                modifiedFiles,
                diff: diff || undefined,
                durationMs: Date.now() - new Date(sandbox.createdAt).getTime(),
            };
        }
        catch {
            return {
                modifiedFiles,
                durationMs: Date.now() - new Date(sandbox.createdAt).getTime(),
            };
        }
    }
    async teardown(sandbox) {
        try {
            execFileSync("docker", ["rm", "-f", sandbox.id], {
                stdio: "ignore",
                timeout: 10_000,
            });
        }
        catch {
            // Best-effort cleanup
        }
    }
}
