/**
 * Resolve caller-provided SandboxConfig into concrete SandboxState.
 *
 * Converts Windows paths to Linux /mnt/ paths and probes the WSL
 * filesystem so that buildBwrapArgs() needs no I/O.
 */
import { spawnSync } from 'child_process';
import { ETC_ENTRIES_FOR_NETWORK } from './bwrap.js';
import { toLinuxPath } from './paths.js';
export const DEFAULT_DISTRO = 'shortcutxl';
/**
 * Paths always write-protected, even within writable regions.
 *
 * Relative paths are expanded against each writable mount.
 * These close real escape vectors: shell config injection,
 * git hook planting, credential theft, config tampering.
 */
const MANDATORY_DENY_PATHS = [
    // Shell configs — code execution on next shell open
    '.bashrc',
    '.profile',
    '.zshrc',
    // Git — hook injection, repo redirect
    '.gitconfig',
    '.git/hooks',
    // SSH — key exfiltration, authorized_keys injection
    '.ssh',
    // Environment — credential theft via auto-sourcing
    '.env',
    '.envrc',
    // MCP — direct sandbox escape via server config tampering
    '.mcp.json'
];
/**
 * Resolve sandbox config + cwd into ready-to-use sandbox state.
 *
 * Probes the WSL distro for /lib64, /etc entries, resolv.conf,
 * and deny path existence so that subsequent buildBwrapArgs() calls are pure.
 */
export function resolveSandboxConfig(config, cwd) {
    const distro = config.distro ?? DEFAULT_DISTRO;
    const networkMode = config.networkMode ?? 'none';
    const needsNetwork = networkMode !== 'none';
    const rawReadMounts = (config.readPaths ?? []).map(toLinuxPath);
    const rawWriteMounts = (config.writePaths ?? []).map(toLinuxPath);
    const denyPatterns = config.denyPaths ?? MANDATORY_DENY_PATHS;
    // Resolve symlinks and reject paths that escape allowed boundaries.
    // Allowed boundaries = union of all read + write paths.
    const allBoundaries = [...rawReadMounts, ...rawWriteMounts];
    const readMounts = resolveAndValidateMounts(distro, rawReadMounts, allBoundaries);
    const writeMounts = resolveAndValidateMounts(distro, rawWriteMounts, allBoundaries);
    // Probe for seccomp infrastructure (apply-seccomp binary + BPF filter).
    // Both must exist for seccomp to be active. Graceful degradation if missing.
    const seccompEnabled = config.seccomp !== false;
    const SECCOMP_FILTER = '/etc/seccomp/unix-block.bpf';
    const SECCOMP_BINARY = '/usr/local/bin/apply-seccomp';
    const seccompFilterPath = seccompEnabled && existsInWsl(distro, SECCOMP_FILTER) && existsInWsl(distro, SECCOMP_BINARY)
        ? SECCOMP_FILTER
        : null;
    return {
        distro,
        readMounts,
        writeMounts,
        workdir: toLinuxPath(cwd),
        networkMode,
        env: config.env ?? {},
        denyMounts: resolveDenyMounts(distro, writeMounts, denyPatterns),
        seccompFilterPath,
        hasLib64: existsInWsl(distro, '/lib64'),
        resolvedResolvConf: needsNetwork ? resolveWslPath(distro, '/etc/resolv.conf') : null,
        existingEtcEntries: needsNetwork
            ? ETC_ENTRIES_FOR_NETWORK.filter((e) => existsInWsl(distro, e))
            : []
    };
}
// ---------------------------------------------------------------------------
// Symlink boundary checking
// ---------------------------------------------------------------------------
/**
 * Batch-resolve symlinks via `readlink -f` and filter out paths that
 * escape their allowed boundaries.
 *
 * A mount is valid if its resolved (real) path either:
 *   1. Equals the original path (not a symlink, or symlink to itself), OR
 *   2. Falls within one of the allowed boundary paths.
 *
 * Broken symlinks and non-existent paths are rejected (readlink -f fails).
 *
 * @param distro - WSL distro name
 * @param mounts - Linux mount paths to validate
 * @param allowedBoundaries - All configured mount paths (read + write) that
 *   define the allowed scope. A resolved path is accepted if it starts with
 *   any of these boundaries.
 * @returns Only the mounts that pass boundary checking (using resolved paths).
 */
function resolveAndValidateMounts(distro, mounts, allowedBoundaries) {
    if (mounts.length === 0)
        return [];
    const resolved = batchResolveSymlinks(distro, mounts);
    const validated = new Set();
    for (const original of mounts) {
        const real = resolved.get(original);
        if (real == null)
            continue; // broken symlink or doesn't exist — reject
        // Accept if resolved path equals original (no symlink)
        if (real === original) {
            validated.add(original);
            continue;
        }
        // Accept if resolved path falls within any allowed boundary
        const withinBoundary = allowedBoundaries.some((boundary) => real === boundary || real.startsWith(boundary + '/'));
        if (withinBoundary) {
            validated.add(real); // use the resolved path
        }
        // Otherwise: symlink escapes boundaries — silently rejected
    }
    return [...validated];
}
/**
 * Batch-resolve symlinks for multiple paths in a single WSL call.
 * Returns a Map from original path to resolved path (null if resolution failed).
 */
function batchResolveSymlinks(distro, paths) {
    const results = new Map();
    const uncached = [];
    for (const p of paths) {
        const key = `resolve:${distro}:${p}`;
        if (wslPathCache.has(key)) {
            results.set(p, wslPathCache.get(key));
        }
        else {
            uncached.push(p);
        }
    }
    if (uncached.length === 0)
        return results;
    // Batch: "readlink -f '/p1' 2>/dev/null || echo '::FAIL::'; readlink -f '/p2' ..."
    // Each readlink outputs one line on success, or ::FAIL:: on failure.
    const script = uncached
        .map((p) => {
        const escaped = shellEscape(p);
        return `readlink -f '${escaped}' 2>/dev/null || echo '::FAIL::'`;
    })
        .join('; ');
    try {
        const result = spawnSync('wsl', ['-d', distro, '--', 'sh', '-c', script], {
            timeout: 5000,
            encoding: 'utf-8',
            windowsHide: true
        });
        const lines = (result.stdout ?? '').split('\n').map((l) => l.trim());
        for (let i = 0; i < uncached.length; i++) {
            const line = lines[i] ?? '::FAIL::';
            const resolved = line === '::FAIL::' || line === '' ? null : line;
            const key = `resolve:${distro}:${uncached[i]}`;
            wslPathCache.set(key, resolved);
            results.set(uncached[i], resolved);
        }
    }
    catch {
        for (const p of uncached) {
            wslPathCache.set(`resolve:${distro}:${p}`, null);
            results.set(p, null);
        }
    }
    return results;
}
// ---------------------------------------------------------------------------
// Deny path resolution
// ---------------------------------------------------------------------------
/**
 * Expand relative deny patterns against each writable mount and
 * return only the paths that exist in WSL (bwrap can't overlay non-existent paths).
 *
 * If a deny path is a symlink, both the symlink and its resolved target
 * are included so both the alias and the real path are protected.
 *
 * Non-existent deny paths are a known gap — sandboxed code could create them.
 * This will be addressed by Phase 6 (seccomp).
 */
function resolveDenyMounts(distro, writeMounts, denyPatterns) {
    if (denyPatterns.length === 0 || writeMounts.length === 0)
        return [];
    const candidates = [];
    for (const mount of writeMounts) {
        for (const pattern of denyPatterns) {
            if (pattern.startsWith('/')) {
                if (pattern.startsWith(mount + '/') || pattern === mount) {
                    candidates.push(pattern);
                }
            }
            else {
                candidates.push(`${mount}/${pattern}`);
            }
        }
    }
    const unique = [...new Set(candidates)];
    if (unique.length === 0)
        return [];
    // Check which candidates exist, then resolve symlinks
    const existing = batchExistsInWsl(distro, unique);
    const resolved = batchResolveSymlinks(distro, existing);
    const result = new Set();
    for (const original of existing) {
        result.add(original);
        const real = resolved.get(original);
        if (real != null && real !== original) {
            result.add(real); // also protect the resolved target
        }
    }
    return [...result];
}
/** Escape a string for use inside single quotes in sh. */
function shellEscape(s) {
    // In single-quoted strings, the only character that can't appear is '.
    // Replace each ' with the sequence: end quote, escaped quote, start quote.
    return s.replace(/'/g, "'\\''");
}
/**
 * Check multiple paths in a single WSL call.
 * Returns the subset of paths that exist.
 */
function batchExistsInWsl(distro, paths) {
    // Check cache first — return cached results and only probe uncached paths
    const results = [];
    const uncached = [];
    for (const p of paths) {
        const key = `exists:${distro}:${p}`;
        if (wslPathCache.has(key)) {
            if (wslPathCache.get(key) !== null)
                results.push(p);
        }
        else {
            uncached.push(p);
        }
    }
    if (uncached.length === 0)
        return results;
    // Batch check: "test -e 'P1' && echo 'P1'; test -e 'P2' && echo 'P2'; ..."
    const script = uncached
        .map((p) => {
        const escaped = shellEscape(p);
        return `test -e '${escaped}' && echo '${escaped}'`;
    })
        .join('; ');
    try {
        const result = spawnSync('wsl', ['-d', distro, '--', 'sh', '-c', script], {
            timeout: 5000,
            encoding: 'utf-8',
            windowsHide: true
        });
        const found = new Set((result.stdout ?? '')
            .split('\n')
            .map((l) => l.trim())
            .filter(Boolean));
        for (const p of uncached) {
            const exists = found.has(p);
            wslPathCache.set(`exists:${distro}:${p}`, exists ? 'true' : null);
            if (exists)
                results.push(p);
        }
    }
    catch {
        // On failure, mark all as non-existent
        for (const p of uncached) {
            wslPathCache.set(`exists:${distro}:${p}`, null);
        }
    }
    return results;
}
// ---------------------------------------------------------------------------
// WSL filesystem probes (cached per session)
// ---------------------------------------------------------------------------
const wslPathCache = new Map();
/** Resolve a potentially-symlinked path inside the WSL distro to its real path. */
function resolveWslPath(distro, path) {
    const key = `resolve:${distro}:${path}`;
    if (wslPathCache.has(key))
        return wslPathCache.get(key);
    try {
        const result = spawnSync('wsl', ['-d', distro, '--', 'readlink', '-f', path], {
            timeout: 3000,
            encoding: 'utf-8',
            windowsHide: true
        });
        const resolved = result.status === 0 ? (result.stdout ?? '').trim() || null : null;
        wslPathCache.set(key, resolved);
        return resolved;
    }
    catch {
        wslPathCache.set(key, null);
        return null;
    }
}
/** Check if a path exists inside the WSL distro. Cached per session. */
function existsInWsl(distro, path) {
    const key = `exists:${distro}:${path}`;
    if (wslPathCache.has(key))
        return wslPathCache.get(key) !== null;
    try {
        const result = spawnSync('wsl', ['-d', distro, '--', 'test', '-e', path], {
            timeout: 3000,
            windowsHide: true
        });
        wslPathCache.set(key, result.status === 0 ? 'true' : null);
        return result.status === 0;
    }
    catch {
        wslPathCache.set(key, null);
        return false;
    }
}
//# sourceMappingURL=resolve.js.map