/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { isKnownSafeCommand as isMacSafeCommand, isDangerousCommand as isMacDangerousCommand, } from '../sandbox/utils/commandSafety.js';
import { isKnownSafeCommand as isWindowsSafeCommand, isDangerousCommand as isWindowsDangerousCommand, } from '../sandbox/windows/commandSafety.js';
import { isNodeError } from '../utils/errors.js';
import { sanitizeEnvironment, getSecureSanitizationConfig, } from './environmentSanitization.js';
/**
 * Files that represent the governance or "constitution" of the repository
 * and should be write-protected in any sandbox.
 */
export const GOVERNANCE_FILES = [
    { path: '.gitignore', isDirectory: false },
    { path: '.geminiignore', isDirectory: false },
    { path: '.git', isDirectory: true },
];
/**
 * Files that contain sensitive secrets or credentials and should be
 * completely hidden (deny read/write) in any sandbox.
 */
export const SECRET_FILES = [
    { pattern: '.env' },
    { pattern: '.env.*' },
];
/**
 * Checks if a given file name matches any of the secret file patterns.
 */
export function isSecretFile(fileName) {
    return SECRET_FILES.some((s) => {
        if (s.pattern.endsWith('*')) {
            const prefix = s.pattern.slice(0, -1);
            return fileName.startsWith(prefix);
        }
        return fileName === s.pattern;
    });
}
/**
 * Returns arguments for the Linux 'find' command to locate secret files.
 */
export function getSecretFileFindArgs() {
    const args = ['('];
    SECRET_FILES.forEach((s, i) => {
        if (i > 0)
            args.push('-o');
        args.push('-name', s.pattern);
    });
    args.push(')');
    return args;
}
/**
 * Finds all secret files in a directory up to a certain depth.
 * Default is shallow scan (depth 1) for performance.
 */
export async function findSecretFiles(baseDir, maxDepth = 1) {
    const secrets = [];
    const skipDirs = new Set([
        'node_modules',
        '.git',
        '.venv',
        '__pycache__',
        'dist',
        'build',
        '.next',
        '.idea',
        '.vscode',
    ]);
    async function walk(dir, depth) {
        if (depth > maxDepth)
            return;
        try {
            const entries = await fs.readdir(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(dir, entry.name);
                if (entry.isDirectory()) {
                    if (!skipDirs.has(entry.name)) {
                        await walk(fullPath, depth + 1);
                    }
                }
                else if (entry.isFile()) {
                    if (isSecretFile(entry.name)) {
                        secrets.push(fullPath);
                    }
                }
            }
        }
        catch {
            // Ignore read errors
        }
    }
    await walk(baseDir, 1);
    return secrets;
}
/**
 * A no-op implementation of SandboxManager that silently passes commands
 * through while applying environment sanitization.
 */
export class NoopSandboxManager {
    options;
    constructor(options) {
        this.options = options;
    }
    /**
     * Prepares a command by sanitizing the environment and passing through
     * the original program and arguments.
     */
    async prepareCommand(req) {
        const sanitizationConfig = getSecureSanitizationConfig(req.policy?.sanitizationConfig);
        const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig);
        return {
            program: req.command,
            args: req.args,
            env: sanitizedEnv,
        };
    }
    isKnownSafeCommand(args) {
        return os.platform() === 'win32'
            ? isWindowsSafeCommand(args)
            : isMacSafeCommand(args);
    }
    isDangerousCommand(args) {
        return os.platform() === 'win32'
            ? isWindowsDangerousCommand(args)
            : isMacDangerousCommand(args);
    }
    parseDenials() {
        return undefined;
    }
    getWorkspace() {
        return this.options?.workspace ?? process.cwd();
    }
    getOptions() {
        return this.options;
    }
}
/**
 * A SandboxManager implementation that just runs locally (no sandboxing yet).
 */
export class LocalSandboxManager {
    options;
    constructor(options) {
        this.options = options;
    }
    async prepareCommand(_req) {
        throw new Error('Tool sandboxing is not yet implemented.');
    }
    isKnownSafeCommand(_args) {
        return false;
    }
    isDangerousCommand(_args) {
        return false;
    }
    parseDenials() {
        return undefined;
    }
    getWorkspace() {
        return this.options?.workspace ?? process.cwd();
    }
    getOptions() {
        return this.options;
    }
}
/**
 * Resolves sanitized allowed and forbidden paths for a request.
 * Filters the workspace from allowed paths and ensures forbidden paths take precedence.
 */
export async function resolveSandboxPaths(options, req) {
    const forbidden = sanitizePaths(await options.forbiddenPaths?.());
    const allowed = sanitizePaths(req.policy?.allowedPaths);
    const workspaceIdentity = getPathIdentity(options.workspace);
    const forbiddenIdentities = new Set(forbidden.map(getPathIdentity));
    const filteredAllowed = allowed.filter((p) => {
        const identity = getPathIdentity(p);
        return identity !== workspaceIdentity && !forbiddenIdentities.has(identity);
    });
    return {
        allowed: filteredAllowed,
        forbidden,
    };
}
/**
 * Sanitizes an array of paths by deduplicating them and ensuring they are absolute.
 * Always returns an array (empty if input is null/undefined).
 */
export function sanitizePaths(paths) {
    if (!paths || paths.length === 0)
        return [];
    const uniquePathsMap = new Map();
    for (const p of paths) {
        if (!path.isAbsolute(p)) {
            throw new Error(`Sandbox path must be absolute: ${p}`);
        }
        const key = getPathIdentity(p);
        if (!uniquePathsMap.has(key)) {
            uniquePathsMap.set(key, p);
        }
    }
    return Array.from(uniquePathsMap.values());
}
/** Returns a normalized identity for a path, stripping trailing slashes and handling case sensitivity. */
export function getPathIdentity(p) {
    let norm = path.normalize(p);
    // Strip trailing slashes (except for root paths)
    if (norm.length > 1 && (norm.endsWith('/') || norm.endsWith('\\'))) {
        norm = norm.slice(0, -1);
    }
    const platform = os.platform();
    const isCaseInsensitive = platform === 'win32' || platform === 'darwin';
    return isCaseInsensitive ? norm.toLowerCase() : norm;
}
/**
 * Resolves symlinks for a given path to prevent sandbox escapes.
 * If a file does not exist (ENOENT), it recursively resolves the parent directory.
 * Other errors (e.g. EACCES) are re-thrown.
 */
export async function tryRealpath(p) {
    try {
        return await fs.realpath(p);
    }
    catch (e) {
        if (isNodeError(e) && e.code === 'ENOENT') {
            const parentDir = path.dirname(p);
            if (parentDir === p) {
                return p;
            }
            return path.join(await tryRealpath(parentDir), path.basename(p));
        }
        throw e;
    }
}
export { createSandboxManager } from './sandboxManagerFactory.js';
//# sourceMappingURL=sandboxManager.js.map