{"version":3,"file":"sandbox-utils-Dxvw0gtM.mjs","names":[],"sources":["../node_modules/.pnpm/@anthropic-ai+sandbox-runtime@0.0.70/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js"],"sourcesContent":["import { homedir } from 'os';\nimport * as path from 'path';\nimport * as fs from 'fs';\nimport { getPlatform } from '../utils/platform.js';\nimport { logForDebugging } from '../utils/debug.js';\n/**\n * Dangerous files that should be protected from writes.\n * These files can be used for code execution or data exfiltration.\n */\nexport const DANGEROUS_FILES = [\n    '.gitconfig',\n    '.gitmodules',\n    '.bashrc',\n    '.bash_profile',\n    '.zshrc',\n    '.zprofile',\n    '.profile',\n    '.ripgreprc',\n    '.mcp.json',\n];\n/**\n * Dangerous directories that should be protected from writes.\n * These directories contain sensitive configuration or executable files.\n */\nexport const DANGEROUS_DIRECTORIES = ['.git', '.vscode', '.idea'];\n/**\n * Get the list of dangerous directories to deny writes to.\n * Excludes .git since we need it writable for git operations -\n * instead we block specific paths within .git (hooks and config).\n */\nexport function getDangerousDirectories() {\n    return [\n        ...DANGEROUS_DIRECTORIES.filter(d => d !== '.git'),\n        '.claude/commands',\n        '.claude/agents',\n    ];\n}\n/**\n * Normalizes a path for case-insensitive comparison.\n * This prevents bypassing security checks using mixed-case paths on case-insensitive\n * filesystems (macOS/Windows) like `.cLauDe/Settings.locaL.json`.\n *\n * We always normalize to lowercase regardless of platform for consistent security.\n * @param path The path to normalize\n * @returns The lowercase path for safe comparison\n */\nexport function normalizeCaseForComparison(pathStr) {\n    return pathStr.toLowerCase();\n}\n/**\n * Check if a path pattern contains glob characters\n */\nexport function containsGlobChars(pathPattern) {\n    return (pathPattern.includes('*') ||\n        pathPattern.includes('?') ||\n        pathPattern.includes('[') ||\n        pathPattern.includes(']'));\n}\n/**\n * Windows-specific glob-char check. `[` and `]` are NOT\n * metachars here — they are legal in Win32 filenames, so a\n * literal `C:\\app\\[prod].env` must route to the literal-path\n * branch, not glob expansion (where it would match nothing and\n * be silently dropped). Only `*` and `?` trigger expansion.\n */\nexport function containsGlobCharsWin(p) {\n    return p.includes('*') || p.includes('?');\n}\n/** Platform-appropriate glob-char check. */\nfunction containsGlobCharsForPlatform(p) {\n    return getPlatform() === 'windows'\n        ? containsGlobCharsWin(p)\n        : containsGlobChars(p);\n}\n/**\n * Strip the Win32 `\\\\?\\` extended-path prefix so the residue is\n * a conventional absolute path (drive-letter or UNC) with no `?`\n * for the glob-char check to misclassify. `\\\\?\\UNC\\srv\\share\\f`\n * → `\\\\srv\\share\\f`; `\\\\?\\C:\\f` → `C:\\f`; anything else → input.\n * The UNC marker is matched case-insensitively (Windows accepts\n * `\\\\?\\unc\\…` in any casing; a case-sensitive check would fall\n * through to the 4-char strip and yield a cwd-relative residue).\n */\nexport function stripExtendedPathPrefix(p) {\n    if (/^\\\\\\\\\\?\\\\unc\\\\/i.test(p))\n        return '\\\\\\\\' + p.slice(8);\n    if (p.startsWith('\\\\\\\\?\\\\'))\n        return p.slice(4);\n    return p;\n}\n/**\n * True for a Windows UNC path in any spelling — `\\\\server\\share\\…`,\n * extended-length `\\\\?\\UNC\\server\\share\\…`, device-namespace\n * `\\\\.\\UNC\\server\\share\\…` (all any casing, either separator).\n * Delegates path-form normalization to\n * `path.win32.toNamespacedPath` — every UNC spelling canonicalizes\n * to `\\\\?\\UNC\\…` — so the check is \"namespaced form starts with\n * `\\\\?\\UNC\\` (or `\\\\.\\UNC\\`)\". Drive-local forms (`C:\\…`,\n * `\\\\?\\C:\\…`), other device paths (`\\\\.\\pipe\\…`), relative paths\n * (resolved against a local cwd), and server-only `\\\\srv` (no\n * share) are all false.\n *\n * The broker uses this to skip `stat`/`realpath` on UNC **literals**\n * (see {@link normalizePathForSandbox}): any such call is an SMB\n * request carrying the **real user's** NTLM credentials to whatever\n * host the path names — a forced-auth / path-encoded-exfil channel\n * if the path is model-influenced. Literals pass through raw\n * (resolution failures surface at `srt-win` stamp/grant time); a\n * UNC **glob** still walks the share with real-user credentials —\n * the user consented by naming their own share in the config.\n * Defense-in-depth: the primary embedder already gates\n * model-provided cwd/paths upstream.\n */\nexport function isUncPath(p) {\n    const ns = path.win32.toNamespacedPath(p);\n    // Already-namespaced input passes through `toNamespacedPath`\n    // verbatim (casing and separators preserved), so match the UNC\n    // marker case-insensitively with either separator. `[?.]` also\n    // catches the device-namespace `\\\\.\\UNC\\…` form — that is a real\n    // network access, not a local device.\n    return /^[\\\\/]{2}[?.][\\\\/]unc[\\\\/]/i.test(ns);\n}\n/**\n * Remove trailing /** glob suffix from a path pattern\n * Used to normalize path patterns since /** just means \"directory and everything under it\"\n */\nexport function removeTrailingGlobSuffix(pathPattern) {\n    const stripped = pathPattern.replace(/\\/\\*\\*$/, '');\n    return stripped || '/';\n}\n/**\n * Check if a symlink resolution crosses expected path boundaries.\n *\n * When resolving symlinks for sandbox path normalization, we need to ensure\n * the resolved path doesn't unexpectedly broaden the scope. This function\n * returns true if the resolved path is an ancestor of the original path\n * or resolves to a system root, which would indicate the symlink points\n * outside expected boundaries.\n *\n * @param originalPath - The original path before symlink resolution\n * @param resolvedPath - The path after fs.realpathSync() resolution\n * @returns true if the resolved path is outside expected boundaries\n */\nexport function isSymlinkOutsideBoundary(originalPath, resolvedPath) {\n    const normalizedOriginal = path.normalize(originalPath);\n    const normalizedResolved = path.normalize(resolvedPath);\n    // Same path after normalization - OK\n    if (normalizedResolved === normalizedOriginal) {\n        return false;\n    }\n    // Handle macOS /tmp -> /private/tmp canonical resolution\n    // This is a legitimate system symlink that should be allowed\n    // /tmp/claude -> /private/tmp/claude is OK\n    // /var/folders/... -> /private/var/folders/... is OK\n    if (normalizedOriginal.startsWith('/tmp/') &&\n        normalizedResolved === '/private' + normalizedOriginal) {\n        return false;\n    }\n    if (normalizedOriginal.startsWith('/var/') &&\n        normalizedResolved === '/private' + normalizedOriginal) {\n        return false;\n    }\n    // Also handle the reverse: /private/tmp/... resolving to itself\n    if (normalizedOriginal.startsWith('/private/tmp/') &&\n        normalizedResolved === normalizedOriginal) {\n        return false;\n    }\n    if (normalizedOriginal.startsWith('/private/var/') &&\n        normalizedResolved === normalizedOriginal) {\n        return false;\n    }\n    // If resolved path is \"/\" it's outside expected boundaries\n    if (normalizedResolved === '/') {\n        return true;\n    }\n    // If resolved path is very short (single component like /tmp, /usr, /var),\n    // it's likely outside expected boundaries\n    const resolvedParts = normalizedResolved.split('/').filter(Boolean);\n    if (resolvedParts.length <= 1) {\n        return true;\n    }\n    // If original path starts with resolved path, the resolved path is an ancestor\n    // e.g., /tmp/claude -> /tmp means the symlink points to a broader scope\n    if (normalizedOriginal.startsWith(normalizedResolved + '/')) {\n        return true;\n    }\n    // Also check the canonical form of the original path for macOS\n    // e.g., /tmp/claude should also be checked as /private/tmp/claude\n    let canonicalOriginal = normalizedOriginal;\n    if (normalizedOriginal.startsWith('/tmp/')) {\n        canonicalOriginal = '/private' + normalizedOriginal;\n    }\n    else if (normalizedOriginal.startsWith('/var/')) {\n        canonicalOriginal = '/private' + normalizedOriginal;\n    }\n    if (canonicalOriginal !== normalizedOriginal &&\n        canonicalOriginal.startsWith(normalizedResolved + '/')) {\n        return true;\n    }\n    // STRICT CHECK: Only allow resolutions that stay within the expected path tree\n    // The resolved path must either:\n    // 1. Start with the original path (deeper/same) - already covered by returning false below\n    // 2. Start with the canonical original (deeper/same under canonical form)\n    // 3. BE the canonical form of the original (e.g., /tmp/x -> /private/tmp/x)\n    // Any other resolution (e.g., /tmp/claude -> /Users/dworken) is outside expected bounds\n    const resolvedStartsWithOriginal = normalizedResolved.startsWith(normalizedOriginal + '/');\n    const resolvedStartsWithCanonical = canonicalOriginal !== normalizedOriginal &&\n        normalizedResolved.startsWith(canonicalOriginal + '/');\n    const resolvedIsCanonical = canonicalOriginal !== normalizedOriginal &&\n        normalizedResolved === canonicalOriginal;\n    const resolvedIsSame = normalizedResolved === normalizedOriginal;\n    // If resolved path is not within expected tree, it's outside boundary\n    if (!resolvedIsSame &&\n        !resolvedIsCanonical &&\n        !resolvedStartsWithOriginal &&\n        !resolvedStartsWithCanonical) {\n        return true;\n    }\n    // Allow resolution to same directory level or deeper within expected tree\n    return false;\n}\n/**\n * Expand a leading `~` to the home directory. Handles bare `~`,\n * `~/…`, and (on Windows only) the `~\\…` form so callers don't each\n * open-code the variants. `~\\` is gated to Windows because `\\` is a\n * valid POSIX filename byte — `~\\foo` is a legal relative filename\n * on Linux/macOS and must NOT tilde-expand there.\n */\nexport function expandTilde(p) {\n    if (p === '~')\n        return homedir();\n    if (p.startsWith('~/') ||\n        (process.platform === 'win32' && p.startsWith('~\\\\'))) {\n        return homedir() + p.slice(1);\n    }\n    return p;\n}\n/**\n * Expand Windows-style `%USERPROFILE%` / `%HOMEDRIVE%` / `%HOMEPATH%`\n * references to the real user's home directory. Case-insensitive;\n * idempotent. Applied by {@link normalizePathForSandbox}'s Windows\n * pre-processing so every filesystem-config path field\n * (`allowRead`/`allowWrite`/`denyRead`/`denyWrite`) accepts these\n * forms uniformly.\n *\n * `%HOMEPATH%` is drive-RELATIVE (`\\Users\\name`) and `%HOMEDRIVE%` is\n * the drive-only (`C:`) — the split matches how cmd.exe defines them,\n * so `%HOMEDRIVE%%HOMEPATH%` composes to the full home path.\n */\nexport function expandWindowsEnvRefs(p) {\n    const home = homedir();\n    const drive = /^[A-Za-z]:/.test(home) ? home.slice(0, 2) : '';\n    const homePath = drive ? home.slice(2) : home;\n    return p\n        .replace(/%USERPROFILE%/gi, home)\n        .replace(/%HOMEDRIVE%/gi, drive)\n        .replace(/%HOMEPATH%/gi, homePath);\n}\n/**\n * Normalize a path for use in sandbox configurations\n * Handles:\n * - Tilde (~) expansion for home directory\n * - Relative paths (./foo, ../foo, etc.) converted to absolute\n * - Absolute paths remain unchanged\n * - Symlinks are resolved to their real paths for non-glob patterns\n * - Glob patterns preserve wildcards after path normalization\n *\n * Returns the absolute path with symlinks resolved (or normalized glob pattern)\n */\nexport function normalizePathForSandbox(pathPattern) {\n    const cwd = process.cwd();\n    // Windows pre-processing: expand `%USERPROFILE%` / `%HOMEDRIVE%` /\n    // `%HOMEPATH%`, strip the `\\\\?\\` / `\\\\?\\UNC\\` extended prefix (its\n    // `?` is a literal, not a glob char), and uppercase the drive\n    // letter so `c:\\…` and `C:\\…` normalize identically.\n    if (getPlatform() === 'windows') {\n        pathPattern = stripExtendedPathPrefix(expandWindowsEnvRefs(pathPattern));\n        if (/^[a-z]:/.test(pathPattern)) {\n            pathPattern = pathPattern[0].toUpperCase() + pathPattern.slice(1);\n        }\n        // UNC literal: return as-is (separators normalised only) — no\n        // stat/realpath. A UNC *glob* falls through to the glob walk\n        // below (user-trusted share). See {@link isUncPath}.\n        if (isUncPath(pathPattern) && !containsGlobCharsWin(pathPattern)) {\n            return path.win32.normalize(pathPattern);\n        }\n    }\n    let normalizedPath = expandTilde(pathPattern);\n    if (normalizedPath !== pathPattern) {\n        // tilde was expanded above\n    }\n    else if (pathPattern.startsWith('./') || pathPattern.startsWith('../')) {\n        // Convert relative to absolute based on current working directory\n        normalizedPath = path.resolve(cwd, pathPattern);\n    }\n    else if (!path.isAbsolute(pathPattern)) {\n        // Handle other relative paths (e.g., \".\", \"..\", \"foo/bar\")\n        normalizedPath = path.resolve(cwd, pathPattern);\n    }\n    // For glob patterns, resolve symlinks for the directory portion only\n    if (containsGlobCharsForPlatform(normalizedPath)) {\n        // Extract the static directory prefix before glob characters\n        // (on Windows, `[`/`]` are literal so only split on `*`/`?`).\n        const splitRe = getPlatform() === 'windows' ? /[*?]/ : /[*?[\\]]/;\n        const staticPrefix = normalizedPath.split(splitRe)[0];\n        if (staticPrefix && staticPrefix !== '/') {\n            // Get the directory containing the glob pattern\n            // If staticPrefix ends with /, remove it to get the directory\n            const baseDir = staticPrefix.endsWith('/')\n                ? staticPrefix.slice(0, -1)\n                : path.dirname(staticPrefix);\n            // Try to resolve symlinks for the base directory\n            try {\n                const resolvedBaseDir = fs.realpathSync(baseDir);\n                // Validate that resolution stays within expected boundaries\n                if (!isSymlinkOutsideBoundary(baseDir, resolvedBaseDir)) {\n                    // Reconstruct the pattern with the resolved directory\n                    const patternSuffix = normalizedPath.slice(baseDir.length);\n                    return resolvedBaseDir + patternSuffix;\n                }\n                // If resolution would broaden scope, keep original pattern\n            }\n            catch {\n                // If directory doesn't exist or can't be resolved, keep the original pattern\n            }\n        }\n        return normalizedPath;\n    }\n    // Resolve symlinks to real paths to avoid bwrap issues\n    // Validate that the resolution stays within expected boundaries\n    try {\n        const resolvedPath = fs.realpathSync(normalizedPath);\n        // Only use resolved path if it doesn't cross boundary (e.g., symlink to parent dir)\n        if (isSymlinkOutsideBoundary(normalizedPath, resolvedPath)) {\n            // Symlink points outside expected boundaries - keep original path\n        }\n        else {\n            normalizedPath = resolvedPath;\n        }\n    }\n    catch {\n        // If path doesn't exist or can't be resolved, keep the normalized path\n    }\n    return normalizedPath;\n}\n/**\n * Get recommended system paths that should be writable for commands to work properly\n *\n * WARNING: These default paths are intentionally broad for compatibility but may\n * allow access to files from other processes. In highly security-sensitive\n * environments, you should configure more restrictive write paths.\n */\nexport function getDefaultWritePaths() {\n    const homeDir = homedir();\n    const recommendedPaths = [\n        '/dev/stdout',\n        '/dev/stderr',\n        '/dev/null',\n        '/dev/tty',\n        '/dev/dtracehelper',\n        '/dev/autofs_nowait',\n        '/tmp/claude',\n        '/private/tmp/claude',\n        path.join(homeDir, '.npm/_logs'),\n        path.join(homeDir, '.claude/debug'),\n    ];\n    return recommendedPaths;\n}\n/**\n * Generate proxy environment variables for sandboxed processes\n */\n/**\n * Per-tool trust-store env vars set to the TLS-termination CA cert path so\n * HTTPS clients in the sandboxed child accept proxy-minted certs.\n */\nexport const CA_TRUST_VARS = [\n    'NODE_EXTRA_CA_CERTS',\n    'SSL_CERT_FILE',\n    'CURL_CA_BUNDLE',\n    'REQUESTS_CA_BUNDLE',\n    'PIP_CERT',\n    'GIT_SSL_CAINFO',\n    'AWS_CA_BUNDLE',\n    'CARGO_HTTP_CAINFO',\n    'DENO_CERT',\n    // gcloud ignores SSL_CERT_FILE/REQUESTS_CA_BUNDLE; this is its dedicated\n    // override (maps to core/custom_ca_certs_file).\n    'CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE',\n    // Nix-built binaries are patched to read this instead of SSL_CERT_FILE, and\n    // it's typically pre-set to the Nix system bundle in the parent env, so we\n    // must override it explicitly.\n    'NIX_SSL_CERT_FILE',\n];\nexport function generateProxyEnvVars(httpProxyPort, socksProxyPort, caCertPath, proxyAuthToken, skipTmpdir, encodedCommand) {\n    // When the proxy requires auth, embed the credential in the URL so clients\n    // send Proxy-Authorization automatically. Only the sandbox child sees this\n    // env, so the token never reaches host processes.\n    //\n    // The username carries the per-command encodedCommand so the proxy can\n    // attribute denials to a specific invocation (see\n    // SandboxViolationStore). Standard base64 is percent-encoded in the URL so\n    // `+/=` survive userinfo parsing; clients URL-decode before building the\n    // Basic header / RFC 1929 frame, so the proxy receives the raw base64.\n    const userRaw = proxyUsernameFor(encodedCommand);\n    const userPct = userRaw === PROXY_AUTH_USER ? userRaw : encodeURIComponent(userRaw);\n    const auth = proxyAuthToken ? `${userPct}:${proxyAuthToken}@` : '';\n    const envVars = [`SANDBOX_RUNTIME=1`];\n    // TMPDIR is overridden so temp-file writers land in a path the FS sandbox\n    // allows (getDefaultWritePaths). When filesystem policy is disabled\n    // (writeConfig === undefined → skipTmpdir), the host TMPDIR is already\n    // writable and /tmp/claude may not exist, so leave it untouched.\n    // CLAUDE_CODE_TMPDIR is the current name; CLAUDE_TMPDIR is kept for\n    // backwards compatibility (#141).\n    if (!skipTmpdir) {\n        const tmpdir = process.env.CLAUDE_CODE_TMPDIR ||\n            process.env.CLAUDE_TMPDIR ||\n            '/tmp/claude';\n        envVars.push(`TMPDIR=${tmpdir}`);\n    }\n    // When TLS termination is configured, the child only ever sees proxy-minted\n    // certs signed by the configured CA. Point the common per-tool trust-store\n    // env vars at it so HTTPS clients accept those certs.\n    if (caCertPath) {\n        for (const v of CA_TRUST_VARS) {\n            envVars.push(`${v}=${caCertPath}`);\n        }\n    }\n    // If no proxy ports provided, return minimal env vars\n    if (!httpProxyPort && !socksProxyPort) {\n        return envVars;\n    }\n    // Always set NO_PROXY to exclude localhost and private networks from\n    // proxying. *.local is intentionally absent: under network restriction the\n    // child has no usable resolver/routes (bwrap --unshare-net on Linux,\n    // loopback-only under seatbelt), so a NO_PROXY match makes the client try\n    // direct getaddrinfo() and fail. Routing .local hostnames through the proxy\n    // lets the parent resolve them (e.g. Kubernetes *.svc.cluster.local).\n    const noProxyAddresses = [\n        'localhost',\n        '127.0.0.1',\n        '::1',\n        '169.254.0.0/16', // Link-local\n        '10.0.0.0/8', // Private network\n        '172.16.0.0/12', // Private network\n        '192.168.0.0/16', // Private network\n    ].join(',');\n    envVars.push(`NO_PROXY=${noProxyAddresses}`);\n    envVars.push(`no_proxy=${noProxyAddresses}`);\n    if (httpProxyPort) {\n        envVars.push(`HTTP_PROXY=http://${auth}localhost:${httpProxyPort}`);\n        envVars.push(`HTTPS_PROXY=http://${auth}localhost:${httpProxyPort}`);\n        // Lowercase versions for compatibility with some tools\n        envVars.push(`http_proxy=http://${auth}localhost:${httpProxyPort}`);\n        envVars.push(`https_proxy=http://${auth}localhost:${httpProxyPort}`);\n        if (proxyAuthToken) {\n            // Pre-send Basic so git never gets a 407 and never invokes a\n            // credential helper for the proxy URL (Windows GCM intercepts the\n            // challenge and the URL-embedded password doesn't survive it).\n            envVars.push(`GIT_CONFIG_PARAMETERS='http.proxyAuthMethod=basic'`);\n        }\n    }\n    // The URL to advertise to clients that need a general-purpose,\n    // CONNECT-capable proxy. The mux serves HTTP CONNECT and SOCKS on the same\n    // advertised port, so http:// works for everyone: clients that only speak\n    // CONNECT get a URL they understand, and anything that actually speaks\n    // SOCKS to it still reaches the SOCKS handler. Falls back to socks5h://\n    // only when no HTTP proxy port exists, which no current caller configures.\n    const connectProxyUrl = httpProxyPort\n        ? `http://${auth}localhost:${httpProxyPort}`\n        : `socks5h://${auth}localhost:${socksProxyPort}`;\n    // ALL_PROXY: prefer the HTTP proxy URL over SOCKS. httpx (and similar\n    // Python clients) eagerly import `socksio` at client construction when\n    // ALL_PROXY is a socks5h:// URL and crash with ImportError in envs that\n    // lack the package — before any bytes hit the wire, so the mux's\n    // protocol sniffing can't help.\n    envVars.push(`ALL_PROXY=${connectProxyUrl}`);\n    envVars.push(`all_proxy=${connectProxyUrl}`);\n    // gRPC-based tools. gRPC C-core (every google-cloud-* Python client, and\n    // grpc-js) only understands HTTP CONNECT proxies. Given a socks5h:// URL it\n    // logs \"'socks5h' scheme not supported in proxy URI\", ignores the var, and\n    // resolves the target directly via c-ares — which the sandbox blocks, so\n    // the client dies with \"address lookup failed / Could not contact DNS\n    // servers\" instead of falling back to https_proxy. Not gated on\n    // socksProxyPort: the value no longer depends on it, and a gRPC client in\n    // an HTTP-only sandbox needs this var just as much.\n    envVars.push(`GRPC_PROXY=${connectProxyUrl}`);\n    envVars.push(`grpc_proxy=${connectProxyUrl}`);\n    if (socksProxyPort) {\n        // Configure Git to use SSH through the proxy so DNS resolution happens outside the sandbox.\n        // ControlMaster/ControlPath are disabled because SSH connection multiplexing breaks inside\n        // the sandbox: the mux socket path from the user's ssh config (typically under ~/.ssh) is\n        // not an allowed Unix socket path, and OpenSSH treats a mux listener bind failure as fatal\n        // even with ControlMaster=auto — it exits right after authentication, before running the\n        // git command. Command-line options take precedence over ssh_config, so this neutralizes\n        // any user ControlMaster setup. ControlPath=none is needed in addition to ControlMaster=no:\n        // with ControlMaster=no alone, ssh still tries to connect to an existing mux socket at the\n        // configured ControlPath.\n        const sshMuxOverride = '-o ControlMaster=no -o ControlPath=none';\n        const platform = getPlatform();\n        if (platform === 'macos') {\n            // macOS: use BSD nc SOCKS5 proxy support (-X 5 -x). nc has no SOCKS5\n            // auth, so when proxyAuthToken is set, git-over-ssh fails at the SOCKS\n            // handshake — use git-over-https (HTTP_PROXY carries the credential).\n            envVars.push(`GIT_SSH_COMMAND=ssh ${sshMuxOverride} -o ProxyCommand='nc -X 5 -x localhost:${socksProxyPort} %h %p'`);\n        }\n        else if (platform === 'linux' && httpProxyPort) {\n            // Linux: use socat HTTP CONNECT via the HTTP proxy bridge.\n            // socat is already a required Linux sandbox dependency, and PROXY: is\n            // portable across all socat versions (unlike SOCKS5-CONNECT which needs >= 1.8.0).\n            const socatAuth = proxyAuthToken\n                ? `,proxyauth=${userRaw}:${proxyAuthToken}`\n                : '';\n            envVars.push(`GIT_SSH_COMMAND=ssh ${sshMuxOverride} -o ProxyCommand='socat - PROXY:localhost:%h:%p,proxyport=${httpProxyPort}${socatAuth}'`);\n        }\n        // FTP proxy support (use socks5h for DNS resolution through proxy).\n        // Deliberately not connectProxyUrl: given an http:// ftp_proxy, curl does\n        // not CONNECT-tunnel by default, it gateways the transfer as\n        // `GET ftp://host/path HTTP/1.1` to the proxy — which the mux does not\n        // implement, and an env var can't ask curl for --proxytunnel. socks5h is\n        // transparent at the TCP layer, so it is the value that works here.\n        envVars.push(`FTP_PROXY=socks5h://${auth}localhost:${socksProxyPort}`);\n        envVars.push(`ftp_proxy=socks5h://${auth}localhost:${socksProxyPort}`);\n        // rsync proxy support — RSYNC_PROXY is host:port only, no userinfo. With\n        // proxy auth on, rsync via this var fails at the CONNECT (407); use SSH\n        // transport or wrap with proxychains instead.\n        envVars.push(`RSYNC_PROXY=localhost:${socksProxyPort}`);\n        // Database tools NOTE: Most database clients don't have built-in proxy support\n        // You typically need to use SSH tunneling or a SOCKS wrapper like tsocks/proxychains\n        // Docker CLI uses HTTP for the API\n        // This makes Docker use the HTTP proxy for registry operations\n        envVars.push(`DOCKER_HTTP_PROXY=http://${auth}localhost:${httpProxyPort || socksProxyPort}`);\n        envVars.push(`DOCKER_HTTPS_PROXY=http://${auth}localhost:${httpProxyPort || socksProxyPort}`);\n        // Kubernetes kubectl - uses standard HTTPS_PROXY\n        // kubectl respects HTTPS_PROXY which we already set above\n        // AWS CLI - uses standard HTTPS_PROXY (v2 supports it well)\n        // AWS CLI v2 respects HTTPS_PROXY which we already set above\n        // Google Cloud SDK - has specific proxy settings.\n        // proxy/type names the protocol the *proxy* speaks, not the traffic it\n        // tunnels. Our HTTP CONNECT proxy carries TLS to Google APIs, so the\n        // correct value is \"http\" (gcloud only accepts http, http_no_tunnel,\n        // socks4, socks5; \"https\" is rejected at startup).\n        if (httpProxyPort) {\n            envVars.push(`CLOUDSDK_PROXY_TYPE=http`);\n            envVars.push(`CLOUDSDK_PROXY_ADDRESS=localhost`);\n            envVars.push(`CLOUDSDK_PROXY_PORT=${httpProxyPort}`);\n            if (proxyAuthToken) {\n                envVars.push(`CLOUDSDK_PROXY_USERNAME=${userRaw}`);\n                envVars.push(`CLOUDSDK_PROXY_PASSWORD=${proxyAuthToken}`);\n            }\n        }\n        // Azure CLI - uses HTTPS_PROXY\n        // Azure CLI respects HTTPS_PROXY which we already set above\n        // Terraform - uses standard HTTP/HTTPS proxy vars\n        // Terraform respects HTTP_PROXY/HTTPS_PROXY which we already set above\n        // gRPC: see GRPC_PROXY above, emitted outside this guard.\n    }\n    // Do not set HTTP_PROXY/HTTPS_PROXY to SOCKS URLs in the SOCKS-only path:\n    // most HTTP clients reject socks*:// in those vars. ALL_PROXY (above)\n    // already carries the route for clients that read it.\n    return envVars;\n}\n/**\n * `safe.directory` entries above this count collapse to a single\n * `safe.directory=*`. Keeps `GIT_CONFIG_COUNT` (and the argv it rides\n * on) bounded when the safe-dir set is wide.\n */\nconst SAFE_DIRECTORY_WILDCARD_THRESHOLD = 8;\n/**\n * Build the `GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_<n>` /\n * `GIT_CONFIG_VALUE_<n>` env-var set for the sandboxed child.\n *\n * Emits:\n *   - `safe.directory=<dir>` for each entry in `safeDirs` (or one\n *     `safe.directory=*` when the list is long) — inside the sandbox\n *     the working tree is owned by a different user (Windows: the\n *     real user vs `srt-sandbox`; Linux: unmapped uid under\n *     `bwrap --unshare-user`), so git refuses with \"detected dubious\n *     ownership\" without it.\n *   - `http.schannelUseSSLCAInfo=true` and\n *     `http.schannelCheckRevoke=false` when `schannelCa` (Windows\n *     only) — makes git's schannel backend honor `GIT_SSL_CAINFO`\n *     without `-c http.sslBackend=openssl`. Revocation is disabled\n *     because CryptoAPI CRL/OCSP fetches ignore proxy env and would\n *     be WFP-fenced.\n *\n * Composes with an existing `GIT_CONFIG_COUNT` in `baseEnv` by\n * continuing its numbering; the returned `GIT_CONFIG_COUNT` is the\n * new total. `baseEnv` should reflect what the child will actually\n * see: on Windows the two-hop launch means the broker's own\n * `process.env` never reaches the child, so `baseEnv` is the caller\n * overlay (`WindowsSandboxParams.setEnvVars`); on Linux/macOS the\n * child inherits `process.env`, so callers use\n * {@link buildPosixGitSafeDirEnv} (which folds in `process.env`,\n * `unsetEnvVars`, and `setEnvVars`).\n *\n * Paths are emitted with forward slashes so the value survives\n * msys2's env conversion untouched and native git accepts it (a\n * no-op on POSIX paths).\n */\nexport function buildGitConfigEnv(opts) {\n    // An explicit `GIT_CONFIG_COUNT=0` in baseEnv is an opt-out (\"no\n    // env-level git config\") — respect it rather than overwriting.\n    if (opts.baseEnv?.GIT_CONFIG_COUNT === '0')\n        return {};\n    const parsed = Number.parseInt(opts.baseEnv?.GIT_CONFIG_COUNT ?? '', 10);\n    const start = Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;\n    let n = start;\n    const out = {};\n    const emit = (key, value) => {\n        out[`GIT_CONFIG_KEY_${n}`] = key;\n        out[`GIT_CONFIG_VALUE_${n}`] = value;\n        n++;\n    };\n    const dirs = [\n        ...new Set(opts.safeDirs\n            .filter((d) => !!d)\n            .map(d => {\n            const fwd = d.replace(/\\\\/g, '/');\n            const stripped = fwd.replace(/\\/+$/, '');\n            // Don't strip the trailing slash off a bare root: `C:`\n            // is drive-relative-cwd (git wants `C:/`), and `` is\n            // git's list-reset sentinel for safe.directory (would\n            // wipe preceding entries) — POSIX `/` must stay `/`.\n            if (stripped === '' || /^[A-Za-z]:$/.test(stripped)) {\n                return `${stripped}/`;\n            }\n            return stripped;\n        })),\n    ];\n    if (dirs.length > SAFE_DIRECTORY_WILDCARD_THRESHOLD) {\n        emit('safe.directory', '*');\n    }\n    else {\n        // git matches safe.directory against the REPO TOP-LEVEL exactly,\n        // so a workspace root doesn't cover a nested repo. Emit both the\n        // exact path and the `<dir>/*` glob (git ≥2.46) so any repo\n        // at-or-under a granted dir is trusted. Roots keep their trailing\n        // `/`; don't double it in the glob (`//*` never wildmatches).\n        for (const d of dirs) {\n            emit('safe.directory', d);\n            emit('safe.directory', d.endsWith('/') ? `${d}*` : `${d}/*`);\n        }\n    }\n    if (opts.schannelCa) {\n        emit('http.schannelUseSSLCAInfo', 'true');\n        emit('http.schannelCheckRevoke', 'false');\n    }\n    if (n === start)\n        return {};\n    out.GIT_CONFIG_COUNT = String(n);\n    return out;\n}\n/**\n * POSIX-side wrapper over {@link buildGitConfigEnv} that constructs\n * the correct `baseEnv` for a Linux/macOS sandbox: the child inherits\n * `process.env` under bwrap/sandbox-exec, then `unsetEnvVars` are\n * dropped and `setEnvVars` overlaid — so numbering must continue from\n * whatever `GIT_CONFIG_COUNT` survives that. Shared by\n * `wrapCommandWithSandbox{Linux,MacOS}`.\n */\nexport function buildPosixGitSafeDirEnv(opts) {\n    const baseEnv = {\n        GIT_CONFIG_COUNT: process.env.GIT_CONFIG_COUNT,\n    };\n    for (const k of opts.unsetEnvVars ?? [])\n        delete baseEnv[k];\n    Object.assign(baseEnv, opts.setEnvVars ?? {});\n    return buildGitConfigEnv({ safeDirs: opts.safeDirs, baseEnv });\n}\n/**\n * Encode a command for sandbox monitoring\n * Truncates to 100 chars and base64 encodes to avoid parsing issues\n */\nexport function encodeSandboxedCommand(command) {\n    const truncatedCommand = command.slice(0, 100);\n    return Buffer.from(truncatedCommand).toString('base64');\n}\n/**\n * Decode a base64-encoded command from sandbox monitoring\n */\nexport function decodeSandboxedCommand(encodedCommand) {\n    return Buffer.from(encodedCommand, 'base64').toString('utf8');\n}\n/** Base proxy username; the auth token is the credential, this is a label. */\nexport const PROXY_AUTH_USER = 'srt';\n/**\n * Build the proxy username for a sandboxed command: `srt.<encodedCommand>`\n * so the proxy can attribute a denial to the invocation that triggered it,\n * or bare `srt` when there is nothing to attribute. RFC 1929 caps the\n * SOCKS5 username at 255 bytes; a multibyte command whose 100-code-unit\n * truncation still base64s past that would fail the SOCKS handshake, so\n * fall back to bare `srt` (attribution is lost, connectivity is not).\n */\nexport function proxyUsernameFor(encodedCommand) {\n    if (!encodedCommand)\n        return PROXY_AUTH_USER;\n    const user = `${PROXY_AUTH_USER}.${encodedCommand}`;\n    return Buffer.byteLength(user) <= 255 ? user : PROXY_AUTH_USER;\n}\n/**\n * Inverse of {@link proxyUsernameFor}: extract the encodedCommand suffix\n * from `srt.<encodedCommand>`, or undefined for bare `srt` / anything else.\n * The username is client-controlled inside the sandbox, so a forged suffix\n * can only misattribute a denial in the violation report — it cannot\n * authenticate (the token does that) or reach another command's data.\n */\nexport function encodedCommandFromProxyUser(username) {\n    if (!username || !username.startsWith(`${PROXY_AUTH_USER}.`))\n        return undefined;\n    const suffix = username.slice(PROXY_AUTH_USER.length + 1);\n    return suffix || undefined;\n}\n/**\n * Convert a glob pattern to a regular expression\n *\n * This implements gitignore-style pattern matching to match the behavior of the\n * `ignore` library used by the permission system.\n *\n * Supported patterns:\n * - * matches any characters except / (e.g., *.ts matches foo.ts but not foo/bar.ts)\n * - ** matches any characters including / (e.g., src/**\\/*.ts matches all .ts files in src/)\n * - ? matches any single character except / (e.g., file?.txt matches file1.txt)\n * - [abc] matches any character in the set (e.g., file[0-9].txt matches file3.txt)\n *\n * Exported for testing and shared between macOS sandbox profiles and Linux glob expansion.\n */\nexport function globToRegex(globPattern) {\n    return ('^' +\n        globPattern\n            // Escape regex special characters (except glob chars * ? [ ])\n            .replace(/[.^$+{}()|\\\\]/g, '\\\\$&')\n            // Escape unclosed brackets (no matching ])\n            .replace(/\\[([^\\]]*?)$/g, '\\\\[$1')\n            // Convert glob patterns to regex (order matters - ** before *)\n            .replace(/\\*\\*\\//g, '__GLOBSTAR_SLASH__') // Placeholder for **/\n            .replace(/\\*\\*/g, '__GLOBSTAR__') // Placeholder for **\n            .replace(/\\*/g, '[^/]*') // * matches anything except /\n            .replace(/\\?/g, '[^/]') // ? matches single character except /\n            // Restore placeholders\n            .replace(/__GLOBSTAR_SLASH__/g, '(.*/)?') // **/ matches zero or more dirs\n            .replace(/__GLOBSTAR__/g, '.*') + // ** matches anything including /\n        '$');\n}\n/**\n * Expand a glob pattern into concrete file paths.\n *\n * Used on Linux (where bubblewrap doesn't support glob patterns\n * natively) and Windows (point-in-time expansion before `srt-win\n * acl stamp`). Resolves the static directory prefix, lists files\n * recursively, and filters using {@link globToRegex}.\n *\n * @param globPath - A path pattern containing glob characters (e.g., ~/test/*.env)\n * @returns Array of absolute paths matching the glob pattern\n */\nexport function expandGlobPattern(globPath, opts = {}) {\n    // Normalize to `/` separators throughout so {@link globToRegex}\n    // (which treats `/` as the segment boundary) and the static-prefix\n    // split work on Windows paths. Gated to win32: `\\` is a valid\n    // filename byte on POSIX, so rewriting it there would change the\n    // path (e.g. a Linux directory literally named `app\\creds`).\n    const toFwd = (s) => process.platform === 'win32' ? s.replace(/\\\\/g, '/') : s;\n    const normalizedPattern = toFwd(normalizePathForSandbox(globPath));\n    // Extract the static directory prefix before any glob characters\n    const staticPrefix = normalizedPattern.split(/[*?[\\]]/)[0];\n    if (!staticPrefix || staticPrefix === '/') {\n        logForDebugging(`[Sandbox] Glob pattern too broad, skipping: ${globPath}`);\n        return [];\n    }\n    // Get the base directory from the static prefix\n    const baseDir = staticPrefix.endsWith('/')\n        ? staticPrefix.slice(0, -1)\n        : path.dirname(staticPrefix);\n    if (!fs.existsSync(baseDir)) {\n        logForDebugging(`[Sandbox] Base directory for glob does not exist: ${baseDir}`);\n        return [];\n    }\n    // Build regex from the normalized glob pattern\n    const regex = new RegExp(globToRegex(normalizedPattern), opts.caseInsensitive ? 'i' : '');\n    // List all entries recursively under the base directory\n    const results = [];\n    try {\n        const entries = fs.readdirSync(baseDir, {\n            recursive: true,\n            withFileTypes: true,\n        });\n        for (const entry of entries) {\n            // Build the full path for this entry\n            // entry.parentPath is the directory containing this entry (available in Node 20+/Bun)\n            // For compatibility, fall back to entry.path if parentPath is not available\n            const parentDir = entry.parentPath ??\n                entry.path ??\n                baseDir;\n            const fullPath = path.join(parentDir, entry.name);\n            if (regex.test(toFwd(fullPath))) {\n                results.push(fullPath);\n            }\n        }\n    }\n    catch (err) {\n        logForDebugging(`[Sandbox] Error expanding glob pattern ${globPath}: ${err}`);\n    }\n    return results;\n}\n//# sourceMappingURL=sandbox-utils.js.map"],"x_google_ignoreList":[0],"mappings":";;;;;;;;;;;;;;;;;;AAstBA,SAAgB,YAAY,aAAa;CACrC,OAAQ,MACJ,YAEK,QAAQ,kBAAkB,MAAM,EAEhC,QAAQ,iBAAiB,OAAO,EAEhC,QAAQ,WAAW,oBAAoB,EACvC,QAAQ,SAAS,cAAc,EAC/B,QAAQ,OAAO,OAAO,EACtB,QAAQ,OAAO,MAAM,EAErB,QAAQ,uBAAuB,QAAQ,EACvC,QAAQ,iBAAiB,IAAI,IAClC;AACR"}