UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

852 lines (851 loc) 35.6 kB
import { D as resolveExpiresAtMsFromDurationMs, g as isFutureDateTimestampMs } from "./number-coercion-CLj0HTDM.js"; import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { r as defaultRuntime } from "./runtime-CF2WjnNZ.js"; import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js"; import { i as createLazyRuntimeNamedExport } from "./lazy-runtime-CgCh8H_K.js"; import { a as openRootFile } from "./boundary-file-read-uaJcf6X6.js"; import "./utils-P__uGsPB.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { r as withContainerEnvFile } from "./container-env-file-BC1o9FW_.js"; import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.js"; import { f as isSameSsrFPolicy } from "./ssrf-0QyXWOVG.js"; import { n as readWorkspaceBootstrapFile, t as MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES } from "./workspace-bootstrap-read-CUL2d6SJ.js"; import { a as DEFAULT_SOUL_FILENAME, n as DEFAULT_BOOTSTRAP_FILENAME, p as ensureAgentWorkspace, r as DEFAULT_IDENTITY_FILENAME, s as DEFAULT_USER_FILENAME, t as DEFAULT_AGENTS_FILENAME, x as publishBootstrapFile } from "./workspace-ConDEamr.js"; import { C as SANDBOX_DOCKER_CREATE_ARGS_EPOCH, b as SANDBOX_BROWSER_SECURITY_HASH_EPOCH, v as SANDBOX_BROWSER_IMAGE_CONTRACT_EPOCH } from "./constants-DE2EIeEP.js"; import { n as isToolAllowed } from "./tool-policy-WMCUEiT6.js"; import { i as resolveSandboxConfigForAgent, r as resolveSandboxBrowserDockerCreateConfig } from "./config-RoLkL_H5.js"; import { n as resolveSandboxRuntimeStatus } from "./runtime-status-6c8Jb1Kg.js"; import { n as toSandboxProvisioningError } from "./provisioning-error-CoscFnGb.js"; import { t as hashTextSha256 } from "./hash-DZK-8tRm.js"; import { a as computeSandboxBrowserConfigHash, t as resolveDockerEnvPolicyEpoch } from "./sanitize-env-vars-BMa_hV9u.js"; import { a as DEFAULT_OPENCLAW_BROWSER_COLOR, d as ensureBrowserControlAuth, f as resolveBrowserControlAuth, l as resolveBrowserConfig, n as DEFAULT_BROWSER_ACTION_TIMEOUT_MS, s as DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, u as resolveProfile } from "./browser-profiles-Bwwi3ExY.js"; import { f as readDockerContainerEnvVar, l as formatDockerDaemonUnavailableError, m as readDockerPort, p as readDockerContainerLabel, r as dockerContainerState, s as execDocker, t as buildSandboxCreateArgs, u as isDockerDaemonUnavailable } from "./docker-BzxWmWPm.js"; import { i as formatReadOnlyWorkspaceSkillMountHashState, n as appendWorkspaceMountArgs, o as resolveProtectedSkillMountContainerPaths, r as filterBindsConflictingWithProtectedMounts, s as resolveReadOnlyWorkspaceSkillMounts, t as appendReadOnlyWorkspaceSkillMountArgs } from "./workspace-mounts-CowGDwFc.js"; import { i as slugifySessionKey, n as resolveSandboxAgentId, r as resolveSandboxWorkspaceLayoutPaths, t as buildSandboxContainerName } from "./shared-DdMEwpA0.js"; import { i as readRegisteredSandboxRuntimeIds, l as updateBrowserRegistry, r as readBrowserRegistry, u as updateRegistry } from "./registry-Blki4jt_.js"; import { n as validateNetworkMode } from "./validate-sandbox-security-DZXlS8I-.js"; import { _ as assertSshSandboxSecretOwnerAvailable } from "./ssh-backend-Bi2dY2r6.js"; import { i as startBrowserBridgeServer, l as requireSandboxBackendFactory, n as stopCachedBrowserBridge, r as stopCachedBrowserBridgesForContainer, s as getSandboxBackendWorkdirResolver, t as BROWSER_BRIDGES } from "./browser-bridges-Cf8_qfWT.js"; import { t as createSandboxFsBridge } from "./fs-bridge-CNVCd86N.js"; import fs from "node:fs"; import path from "node:path"; import fs$1 from "node:fs/promises"; import crypto from "node:crypto"; //#region src/config/port-defaults.ts function isValidPort(port) { return Number.isFinite(port) && port > 0 && port <= 65535; } function clampPort(port, fallback) { return isValidPort(port) ? port : fallback; } function derivePort(base, offset, fallback) { return clampPort(base + offset, fallback); } /** Default browser-CDP sidecar port range used when no browser-control-relative range is safe. */ const DEFAULT_BROWSER_CDP_PORT_RANGE_START = 18800; /** Inclusive end of the default browser-CDP sidecar port range. */ const DEFAULT_BROWSER_CDP_PORT_RANGE_END = 18899; const DEFAULT_BROWSER_CDP_PORT_RANGE_SPAN = 99; /** Derives the browser-CDP sidecar range from the browser-control port when it fits. */ function deriveDefaultBrowserCdpPortRange(browserControlPort) { const start = derivePort(browserControlPort, 9, DEFAULT_BROWSER_CDP_PORT_RANGE_START); const end = start + DEFAULT_BROWSER_CDP_PORT_RANGE_SPAN; if (end <= 65535) return { start, end }; return { start: DEFAULT_BROWSER_CDP_PORT_RANGE_START, end: DEFAULT_BROWSER_CDP_PORT_RANGE_END }; } //#endregion //#region src/agents/sandbox/novnc-auth.ts /** * noVNC observer authentication helpers. * * Issues short-lived observer tokens and builds local noVNC URLs without exposing long-lived browser bridge state. */ const NOVNC_PASSWORD_ENV_KEY = "OPENCLAW_BROWSER_NOVNC_PASSWORD"; const NOVNC_TOKEN_TTL_MS = 6e4; const MAX_NOVNC_TOKEN_TTL_MS = NOVNC_TOKEN_TTL_MS; const NOVNC_PASSWORD_LENGTH = 8; const NOVNC_PASSWORD_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const NO_VNC_OBSERVER_TOKENS = /* @__PURE__ */ new Map(); function pruneExpiredNoVncObserverTokens(now) { for (const [token, entry] of NO_VNC_OBSERVER_TOKENS) if (!isFutureDateTimestampMs(entry.expiresAt, { nowMs: now })) NO_VNC_OBSERVER_TOKENS.delete(token); } function resolveNoVncObserverTokenExpiresAt(params) { return resolveExpiresAtMsFromDurationMs(params.ttlMs, { nowMs: params.nowMs, minRemainingMs: 1 }) ?? resolveExpiresAtMsFromDurationMs(NOVNC_TOKEN_TTL_MS, { nowMs: params.nowMs, minRemainingMs: 1 }); } function isNoVncEnabled(params) { return params.noVncEnabled && !params.headless; } function generateNoVncPassword() { let out = ""; for (let i = 0; i < NOVNC_PASSWORD_LENGTH; i += 1) out += NOVNC_PASSWORD_ALPHABET[crypto.randomInt(0, 62)]; return out; } function issueNoVncObserverToken(params) { const now = params.nowMs ?? Date.now(); pruneExpiredNoVncObserverTokens(now); const token = crypto.randomBytes(24).toString("hex"); const expiresAt = resolveNoVncObserverTokenExpiresAt({ ttlMs: typeof params.ttlMs === "number" && params.ttlMs <= MAX_NOVNC_TOKEN_TTL_MS ? params.ttlMs : void 0, nowMs: now }); if (expiresAt === void 0) return token; NO_VNC_OBSERVER_TOKENS.set(token, { noVncPort: params.noVncPort, password: normalizeOptionalString(params.password), expiresAt }); return token; } function consumeNoVncObserverToken(token, nowMs) { const now = nowMs ?? Date.now(); pruneExpiredNoVncObserverTokens(now); const normalized = token.trim(); if (!normalized) return null; const entry = NO_VNC_OBSERVER_TOKENS.get(normalized); if (!entry) return null; NO_VNC_OBSERVER_TOKENS.delete(normalized); if (!isFutureDateTimestampMs(entry.expiresAt, { nowMs: now })) return null; return { noVncPort: entry.noVncPort, password: entry.password }; } function buildNoVncObserverTokenUrl(baseUrl, token) { return `${baseUrl}/sandbox/novnc?${new URLSearchParams({ token }).toString()}`; } //#endregion //#region src/agents/sandbox/browser.ts /** * Sandbox browser container lifecycle. * * Starts or reuses Chrome/noVNC containers, exposes authenticated CDP/observer URLs, and tracks browser registry state. */ const HOT_BROWSER_WINDOW_MS = 3e5; const CDP_SOURCE_RANGE_ENV_KEY = "OPENCLAW_BROWSER_CDP_SOURCE_RANGE"; const CDP_AUTH_TOKEN_ENV_KEY = "OPENCLAW_BROWSER_CDP_AUTH_TOKEN"; const SANDBOX_BROWSER_IMAGE_CONTRACT_LABEL = "org.openclaw.sandbox-browser.contract"; const browserContainerLifecycleQueue = new KeyedAsyncQueue(); const browserNetworkLifecycleQueue = new KeyedAsyncQueue(); function buildSandboxCdpAuthHeader(token) { return `Basic ${Buffer.from(`openclaw:${token}`).toString("base64")}`; } function buildSandboxCdpUrl(params) { const url = new URL(`http://127.0.0.1:${params.cdpPort}`); url.username = "openclaw"; url.password = params.authToken; return url.toString().replace(/\/$/, ""); } async function waitForSandboxCdp(params) { const deadline = Date.now() + Math.max(0, params.timeoutMs); const url = `http://127.0.0.1:${params.cdpPort}/json/version`; while (Date.now() < deadline) { try { const requestTimeoutMs = Math.max(1, Math.min(1e3, deadline - Date.now())); const ctrl = new AbortController(); const t = setTimeout(ctrl.abort.bind(ctrl), requestTimeoutMs); try { const res = await fetch(url, { headers: { Authorization: buildSandboxCdpAuthHeader(params.authToken) }, signal: ctrl.signal }); await res.body?.cancel().catch(() => void 0); if (res.ok) return true; } finally { clearTimeout(t); } } catch {} const remainingMs = deadline - Date.now(); if (remainingMs <= 0) break; await new Promise((r) => { setTimeout(r, Math.min(150, remainingMs)); }); } return false; } function buildSandboxBrowserResolvedConfig(params) { const cdpHost = "127.0.0.1"; const cdpPortRange = deriveDefaultBrowserCdpPortRange(params.controlPort); return { enabled: true, evaluateEnabled: params.evaluateEnabled, controlPort: params.controlPort, cdpProtocol: "http", cdpHost, cdpIsLoopback: true, cdpPortRangeStart: cdpPortRange.start, cdpPortRangeEnd: cdpPortRange.end, remoteCdpTimeoutMs: 1500, remoteCdpHandshakeTimeoutMs: 3e3, localLaunchTimeoutMs: 15e3, localCdpReadyTimeoutMs: 8e3, actionTimeoutMs: DEFAULT_BROWSER_ACTION_TIMEOUT_MS, color: DEFAULT_OPENCLAW_BROWSER_COLOR, executablePath: void 0, headless: params.headless, noSandbox: false, attachOnly: true, defaultProfile: DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, extraArgs: [], tabCleanup: { enabled: true, idleMinutes: 120, maxTabsPerSession: 8, sweepMinutes: 5 }, profiles: { [DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME]: { cdpPort: params.cdpPort, cdpUrl: buildSandboxCdpUrl({ cdpPort: params.cdpPort, authToken: params.cdpAuthToken }), color: DEFAULT_OPENCLAW_BROWSER_COLOR } }, ssrfPolicy: params.ssrfPolicy }; } async function ensureSandboxBrowserImage(image) { const result = await execDocker([ "image", "inspect", "-f", `{{ index .Config.Labels "${SANDBOX_BROWSER_IMAGE_CONTRACT_LABEL}" }}`, image ], { allowFailure: true }); if (result.code === 0) { const contract = result.stdout.trim(); if (contract === "2026-05-12-cdp-relay-auth") return; throw new Error(`Sandbox browser image ${image} is stale or incompatible (contract=${contract && contract !== "<no value>" ? contract : "missing"}, expected=${SANDBOX_BROWSER_IMAGE_CONTRACT_EPOCH}). Rebuild it with scripts/sandbox-browser-setup.sh.`); } const stderr = result.stderr.trim(); if (isDockerDaemonUnavailable(stderr)) throw new Error(formatDockerDaemonUnavailableError(stderr)); throw new Error(`Sandbox browser image not found: ${image}. Build it with scripts/sandbox-browser-setup.sh.`); } async function ensureDockerNetwork(network, opts) { validateNetworkMode(network, { allowContainerNamespaceJoin: opts?.allowContainerNamespaceJoin === true }); const normalized = normalizeOptionalLowercaseString(network) ?? ""; if (!normalized || normalized === "bridge" || normalized === "none") return; await browserNetworkLifecycleQueue.enqueue(normalized, async () => { if ((await execDocker([ "network", "inspect", network ], { allowFailure: true })).code === 0) return; await execDocker([ "network", "create", "--driver", "bridge", network ]); }); } async function ensureSandboxBrowser(params) { if (!params.cfg.browser.enabled) return null; if (!isToolAllowed(params.cfg.tools, "browser")) return null; if (normalizeOptionalLowercaseString(params.cfg.browser.network) === "none") throw new Error("Sandbox browser network mode \"none\" is unsupported because browser control requires a host-reachable published CDP port. Use \"bridge\", a custom bridge network, or disable the sandbox browser."); const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(params.scopeKey); const containerName = buildSandboxContainerName(params.cfg.browser.containerPrefix, slug); return await browserContainerLifecycleQueue.enqueue(containerName, async () => { return await ensureSandboxBrowserContainer(params, containerName); }); } async function ensureSandboxBrowserContainer(params, containerName) { let existing = BROWSER_BRIDGES.get(params.scopeKey); const stopExistingForContainer = async () => { await stopCachedBrowserBridgesForContainer(containerName); existing = BROWSER_BRIDGES.get(params.scopeKey); }; const state = await dockerContainerState(containerName); const browserImage = params.cfg.browser.image ?? "openclaw-sandbox-browser:bookworm-slim"; const cdpSourceRange = normalizeOptionalString(params.cfg.browser.cdpSourceRange); const browserDockerCfg = resolveSandboxBrowserDockerCreateConfig({ docker: params.cfg.docker, browser: { ...params.cfg.browser, image: browserImage } }); const readOnlyWorkspaceSkillMounts = resolveReadOnlyWorkspaceSkillMounts({ workspaceDir: params.workspaceDir, agentWorkspaceDir: params.agentWorkspaceDir, skillsWorkspaceDir: params.skillsWorkspaceDir, workdir: params.cfg.docker.workdir, workspaceAccess: params.cfg.workspaceAccess }); const expectedHash = computeSandboxBrowserConfigHash({ docker: browserDockerCfg, dockerEnvPolicyEpoch: resolveDockerEnvPolicyEpoch(browserDockerCfg.env), browser: { cdpPort: params.cfg.browser.cdpPort, vncPort: params.cfg.browser.vncPort, noVncPort: params.cfg.browser.noVncPort, headless: params.cfg.browser.headless, noVncEnabled: params.cfg.browser.noVncEnabled, autoStartTimeoutMs: params.cfg.browser.autoStartTimeoutMs, cdpSourceRange }, securityEpoch: SANDBOX_BROWSER_SECURITY_HASH_EPOCH, workspaceAccess: params.cfg.workspaceAccess, workspaceDir: params.workspaceDir, agentWorkspaceDir: params.agentWorkspaceDir, mountFormatVersion: 4, createArgsEpoch: SANDBOX_DOCKER_CREATE_ARGS_EPOCH, readOnlyWorkspaceSkillMounts: formatReadOnlyWorkspaceSkillMountHashState(readOnlyWorkspaceSkillMounts) }); const now = Date.now(); let hasContainer = state.exists; let running = state.running; let currentHash = null; let hashMismatch = false; const noVncEnabled = isNoVncEnabled(params.cfg.browser); let noVncPassword; let cdpAuthToken; if (hasContainer) { if (noVncEnabled) noVncPassword = await readDockerContainerEnvVar(containerName, "OPENCLAW_BROWSER_NOVNC_PASSWORD") ?? void 0; cdpAuthToken = await readDockerContainerEnvVar(containerName, CDP_AUTH_TOKEN_ENV_KEY) ?? void 0; if (!cdpAuthToken) { defaultRuntime.log(`Removing stale sandbox browser container ${containerName} because it lacks the current CDP relay auth contract; it will be recreated.`); await stopExistingForContainer(); await execDocker([ "rm", "-f", containerName ], { allowFailure: true }); hasContainer = false; running = false; } } if (hasContainer) { const registryEntry = (await readBrowserRegistry()).entries.find((entry) => entry.containerName === containerName); currentHash = await readDockerContainerLabel(containerName, "openclaw.configHash"); hashMismatch = !currentHash || currentHash !== expectedHash; if (!currentHash) { currentHash = registryEntry?.configHash ?? null; hashMismatch = !currentHash || currentHash !== expectedHash; } if (hashMismatch) { const lastUsedAtMs = registryEntry?.lastUsedAtMs; if (running && (typeof lastUsedAtMs !== "number" || now - lastUsedAtMs < HOT_BROWSER_WINDOW_MS)) { const hint = (() => { if (params.cfg.scope === "session") return `openclaw sandbox recreate --browser --session ${params.scopeKey}`; if (params.cfg.scope === "agent") return `openclaw sandbox recreate --browser --agent ${resolveSandboxAgentId(params.scopeKey) ?? "main"}`; return "openclaw sandbox recreate --browser --all"; })(); defaultRuntime.log(`Sandbox browser config changed for ${containerName} (recently used). Recreate to apply: ${hint}`); } else { await stopExistingForContainer(); await execDocker([ "rm", "-f", containerName ], { allowFailure: true }); hasContainer = false; running = false; } } } if (!hasContainer) { if (noVncEnabled) noVncPassword = generateNoVncPassword(); cdpAuthToken = crypto.randomBytes(24).toString("hex"); await ensureDockerNetwork(browserDockerCfg.network, { allowContainerNamespaceJoin: browserDockerCfg.dangerouslyAllowContainerNamespaceJoin === true }); await ensureSandboxBrowserImage(browserImage); const { argv: args, env } = buildSandboxCreateArgs({ name: containerName, cfg: browserDockerCfg, scopeKey: params.scopeKey, labels: { "openclaw.sandboxBrowser": "1", "openclaw.browserConfigEpoch": SANDBOX_BROWSER_SECURITY_HASH_EPOCH }, configHash: expectedHash, includeBinds: false, bindSourceRoots: [params.workspaceDir, params.agentWorkspaceDir] }); appendWorkspaceMountArgs({ args, workspaceDir: params.workspaceDir, agentWorkspaceDir: params.agentWorkspaceDir, skillsWorkspaceDir: params.skillsWorkspaceDir, workdir: params.cfg.docker.workdir, workspaceAccess: params.cfg.workspaceAccess, readOnlyWorkspaceSkillMounts, includeReadOnlyWorkspaceSkillMounts: false }); if (browserDockerCfg.binds?.length) { const protectedPaths = resolveProtectedSkillMountContainerPaths(readOnlyWorkspaceSkillMounts); const safeBinds = protectedPaths.size > 0 ? filterBindsConflictingWithProtectedMounts(browserDockerCfg.binds, protectedPaths) : browserDockerCfg.binds; for (const bind of browserDockerCfg.binds) if (!safeBinds.includes(bind)) defaultRuntime.log(`sandbox browser: skipping user bind "${bind}" — container path conflicts with a protected read-only skill mount`); for (const bind of safeBinds) args.push("-v", bind); } appendReadOnlyWorkspaceSkillMountArgs({ args, readOnlyWorkspaceSkillMounts }); args.push("-p", `127.0.0.1::${params.cfg.browser.cdpPort}`); if (noVncEnabled) args.push("-p", `127.0.0.1::${params.cfg.browser.noVncPort}`); Object.assign(env, { OPENCLAW_BROWSER_HEADLESS: params.cfg.browser.headless ? "1" : "0", OPENCLAW_BROWSER_ENABLE_NOVNC: params.cfg.browser.noVncEnabled ? "1" : "0", OPENCLAW_BROWSER_CDP_PORT: String(params.cfg.browser.cdpPort), [CDP_AUTH_TOKEN_ENV_KEY]: cdpAuthToken, OPENCLAW_BROWSER_AUTO_START_TIMEOUT_MS: String(params.cfg.browser.autoStartTimeoutMs), OPENCLAW_BROWSER_VNC_PORT: String(params.cfg.browser.vncPort), OPENCLAW_BROWSER_NOVNC_PORT: String(params.cfg.browser.noVncPort), OPENCLAW_BROWSER_NO_SANDBOX: "1" }); if (cdpSourceRange) env[CDP_SOURCE_RANGE_ENV_KEY] = cdpSourceRange; if (noVncEnabled && noVncPassword) env[NOVNC_PASSWORD_ENV_KEY] = noVncPassword; await withContainerEnvFile(env, async (envFile) => { args.push("--env-file", envFile, browserImage); await execDocker(args); }); await execDocker(["start", containerName]); } else if (!running) await execDocker(["start", containerName]); const mappedCdp = await readDockerPort(containerName, params.cfg.browser.cdpPort); if (!mappedCdp) throw new Error(`Failed to resolve CDP port mapping for ${containerName}.`); if (!cdpAuthToken) throw new Error(`Failed to resolve CDP relay auth for ${containerName}.`); const cdpUrl = buildSandboxCdpUrl({ cdpPort: mappedCdp, authToken: cdpAuthToken }); const mappedNoVnc = noVncEnabled ? await readDockerPort(containerName, params.cfg.browser.noVncPort) : null; if (noVncEnabled && !noVncPassword) noVncPassword = await readDockerContainerEnvVar(containerName, "OPENCLAW_BROWSER_NOVNC_PASSWORD") ?? void 0; const existingProfile = existing ? resolveProfile(existing.bridge.state.resolved, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME) : null; const desiredEvaluateEnabled = params.evaluateEnabled ?? true; let desiredAuthToken = normalizeOptionalString(params.bridgeAuth?.token); let desiredAuthPassword = normalizeOptionalString(params.bridgeAuth?.password); if (!desiredAuthToken && !desiredAuthPassword) { desiredAuthToken = existing?.authToken; desiredAuthPassword = existing?.authPassword; if (!desiredAuthToken && !desiredAuthPassword) desiredAuthToken = crypto.randomBytes(24).toString("hex"); } const policyMatches = !existing || isSameSsrFPolicy(existing.bridge.state.resolved.ssrfPolicy, params.ssrfPolicy); const authMatches = !existing || existing.authToken === desiredAuthToken && existing.authPassword === desiredAuthPassword; const evaluateMatches = !existing || existing.bridge.state.resolved.evaluateEnabled === desiredEvaluateEnabled; const canReuse = Boolean(existing && existing.bridge.server.listening && existing.containerName === containerName && existingProfile?.cdpPort === mappedCdp && existingProfile?.cdpUrl === cdpUrl && policyMatches && authMatches && evaluateMatches); if (existing && !canReuse) await stopCachedBrowserBridge(params.scopeKey, existing); const bridge = canReuse ? existing?.bridge ?? null : null; const ensureBridge = async () => { if (bridge) return bridge; const onEnsureAttachTarget = params.cfg.browser.autoStart ? async () => { const currentState = await dockerContainerState(containerName); if (currentState.exists && !currentState.running) await execDocker(["start", containerName]); if (!await waitForSandboxCdp({ cdpPort: mappedCdp, authToken: cdpAuthToken, timeoutMs: params.cfg.browser.autoStartTimeoutMs })) { await execDocker([ "rm", "-f", containerName ], { allowFailure: true }); throw new Error(`Sandbox browser CDP did not become reachable on 127.0.0.1:${mappedCdp} within ${params.cfg.browser.autoStartTimeoutMs}ms. The hung container has been forcefully removed.`); } } : void 0; return await startBrowserBridgeServer({ resolved: buildSandboxBrowserResolvedConfig({ controlPort: 0, cdpPort: mappedCdp, cdpAuthToken, headless: params.cfg.browser.headless, evaluateEnabled: desiredEvaluateEnabled, ssrfPolicy: params.ssrfPolicy }), authToken: desiredAuthToken, authPassword: desiredAuthPassword, onEnsureAttachTarget, resolveSandboxNoVncToken: consumeNoVncObserverToken }); }; const resolvedBridge = await ensureBridge(); if (!bridge) BROWSER_BRIDGES.set(params.scopeKey, { bridge: resolvedBridge, containerName, authToken: desiredAuthToken, authPassword: desiredAuthPassword }); await updateBrowserRegistry({ containerName, sessionKey: params.scopeKey, createdAtMs: now, lastUsedAtMs: now, image: browserImage, configHash: hashMismatch && running ? currentHash ?? void 0 : expectedHash, cdpPort: mappedCdp, noVncPort: mappedNoVnc ?? void 0 }); const noVncUrl = mappedNoVnc && noVncEnabled ? (() => { const token = issueNoVncObserverToken({ noVncPort: mappedNoVnc, password: noVncPassword }); return buildNoVncObserverTokenUrl(resolvedBridge.baseUrl, token); })() : void 0; return { bridgeUrl: resolvedBridge.baseUrl, noVncUrl, containerName }; } //#endregion //#region src/agents/sandbox/docker-user.ts async function resolveSandboxDockerUser(params) { if (params.docker.user?.trim()) return params.docker; const backend = params.backend.trim().toLowerCase(); if (backend !== "docker" && backend !== "podman") return params.docker; const stat = params.stat ?? ((workspaceDir) => fs$1.stat(workspaceDir)); try { const workspaceStat = await stat(params.workspaceDir); const uid = Number.isInteger(workspaceStat.uid) ? workspaceStat.uid : null; const gid = Number.isInteger(workspaceStat.gid) ? workspaceStat.gid : null; if (uid === null || gid === null || uid < 0 || gid < 0) return params.docker; if (backend === "podman" && (uid === 0 || gid === 0)) return params.docker; return { ...params.docker, user: `${uid}:${gid}` }; } catch { return params.docker; } } //#endregion //#region src/agents/sandbox/workspace.ts /** * Sandbox workspace bootstrapper. * * Creates sandbox workspaces and seeds agent bootstrap files through root-boundary reads. */ const log = createSubsystemLogger("sandbox-workspace"); async function ensureSandboxWorkspace(workspaceDir, seedFrom, skipBootstrap, skipOptionalBootstrapFiles) { await fs$1.mkdir(workspaceDir, { recursive: true }); if (seedFrom) { const seed = resolveUserPath(seedFrom); const files = [ DEFAULT_AGENTS_FILENAME, DEFAULT_SOUL_FILENAME, DEFAULT_IDENTITY_FILENAME, DEFAULT_USER_FILENAME, DEFAULT_BOOTSTRAP_FILENAME ]; for (const name of files) { const src = path.join(seed, name); const dest = path.join(workspaceDir, name); if (await fs$1.access(dest).then(() => true, () => false)) continue; const opened = await openRootFile({ absolutePath: src, rootPath: seed, boundaryLabel: "sandbox seed workspace" }); if (!opened.ok) continue; let content; try { content = await readWorkspaceBootstrapFile(opened.fd); } catch (err) { if (err instanceof RangeError) { log.warn(`Ignoring oversized sandbox seed file ${src}: file exceeds the ${MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES}-byte limit`); continue; } throw err; } finally { fs.closeSync(opened.fd); } await publishBootstrapFile(dest, content); } } await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !skipBootstrap, skipOptionalBootstrapFiles }); } //#endregion //#region src/agents/sandbox/context.ts /** * Sandbox context resolver. * * Prepares workspace layout, backend handle, filesystem bridge, browser bridge, and registry state for one run. */ const sandboxLog = createSubsystemLogger("agent/sandbox"); const loadSyncWorkspaceSkills = createLazyRuntimeNamedExport(() => import("./workspace-skill-sync.runtime.js"), "syncWorkspaceSkills"); async function syncSandboxSkillsToWorkspace(params) { try { const [syncWorkspaceSkills, { getRemoteSkillEligibility }, { resolveNodeExecEligibility }] = await Promise.all([ loadSyncWorkspaceSkills(), import("./remote-Bba6aX84.js"), import("./exec-defaults-DemtH6eK.js") ]); const nodeSkills = resolveNodeExecEligibility({ cfg: params.config, sessionKey: params.rawSessionKey, agentId: params.agentId, execOverrides: params.execOverrides }); const eligibility = { nodeSkills, remote: getRemoteSkillEligibility({ advertiseExecNode: nodeSkills.canExec }) }; return { eligibility, skillUsagePaths: await syncWorkspaceSkills({ sourceWorkspaceDir: params.sourceWorkspaceDir, targetWorkspaceDir: params.targetWorkspaceDir, config: params.config, agentId: params.agentId, eligibility, skillsSnapshot: params.skillsSnapshot }) }; } catch (error) { const message = error instanceof Error ? error.message : JSON.stringify(error); defaultRuntime.error?.(`Sandbox skill sync failed: ${message}`); if (params.skillsSnapshot?.librarySelections?.length) throw error; return {}; } } async function ensureSandboxWorkspaceLayout(params) { const { cfg, rawSessionKey } = params; const { agentWorkspaceDir, sandboxWorkspaceDir, scopeKey, skillsWorkspaceDir, workspaceDir } = resolveSandboxWorkspaceLayoutPaths({ cfg, rawSessionKey, agentId: params.agentId, isolationSubject: params.isolationSubject, workspaceDir: params.workspaceDir }); let syncedSkills; if (cfg.workspaceAccess !== "rw") { await ensureSandboxWorkspace(sandboxWorkspaceDir, agentWorkspaceDir, params.config?.agents?.defaults?.skipBootstrap, params.config?.agents?.defaults?.skipOptionalBootstrapFiles); syncedSkills = await syncSandboxSkillsToWorkspace({ sourceWorkspaceDir: agentWorkspaceDir, targetWorkspaceDir: sandboxWorkspaceDir, config: params.config, agentId: params.agentId, rawSessionKey, execOverrides: params.execOverrides, skillsSnapshot: params.skillsSnapshot }); } else { await fs$1.mkdir(workspaceDir, { recursive: true }); syncedSkills = await syncSandboxSkillsToWorkspace({ sourceWorkspaceDir: agentWorkspaceDir, targetWorkspaceDir: skillsWorkspaceDir, config: params.config, agentId: params.agentId, rawSessionKey, execOverrides: params.execOverrides, skillsSnapshot: params.skillsSnapshot }); } return { agentWorkspaceDir, scopeKey, sandboxWorkspaceDir, skillsWorkspaceDir, ...syncedSkills.eligibility ? { skillsEligibility: syncedSkills.eligibility } : {}, ...syncedSkills.skillUsagePaths ? { skillUsagePaths: syncedSkills.skillUsagePaths } : {}, workspaceDir }; } function resolveSandboxSession(params) { const rawSessionKey = params.sessionKey?.trim(); if (!rawSessionKey) return null; const runtime = resolveSandboxRuntimeStatus({ cfg: params.config, agentId: params.agentId, sessionKey: rawSessionKey }); if (!runtime.sandboxed) return null; const configured = resolveSandboxConfigForAgent(params.config, runtime.agentId); const librarySelections = params.skillsSnapshot?.librarySelections; if (librarySelections?.length) runtime.isolationSubject = { kind: "session", sessionKey: `${rawSessionKey}:skills:${hashTextSha256(JSON.stringify(librarySelections))}` }; const configuredSandbox = librarySelections?.length ? { ...configured, scope: "agent" } : configured; if (!runtime.sandboxRequired) return { rawSessionKey, runtime, cfg: configuredSandbox }; if (configuredSandbox.workspaceAccess === "rw") sandboxLog.warn("Configured sandbox workspaceAccess \"rw\" is capped to \"ro\" for a role-required session; guests cannot share the writable agent workspace."); return { rawSessionKey, runtime, cfg: { ...configuredSandbox, scope: "agent", workspaceAccess: runtime.workspaceAccess } }; } function resolveSandboxWorkspaceInfoWorkdir(params) { return getSandboxBackendWorkdirResolver(params.cfg.backend)?.({ sessionKey: params.rawSessionKey, scopeKey: params.scopeKey, workspaceDir: params.workspaceDir, agentWorkspaceDir: params.agentWorkspaceDir, skillsWorkspaceDir: params.skillsWorkspaceDir, cfg: params.cfg }); } function assertSandboxSessionSecretOwnerAvailable(config, resolved) { if (resolved.cfg.backend !== "ssh") return; assertSshSandboxSecretOwnerAvailable({ config, scope: resolved.cfg.scope, agentId: resolved.runtime.agentId }); } async function resolveProvisionedSandboxContext(params, resolved) { const { rawSessionKey, cfg, runtime } = resolved; if (cfg.prune.idleHours !== 0 || cfg.prune.maxAgeDays !== 0) await (await import("./prune-Csam007y.js")).maybePruneSandboxes(cfg); const { agentWorkspaceDir, scopeKey, skillsEligibility, skillUsagePaths, skillsWorkspaceDir, workspaceDir } = await ensureSandboxWorkspaceLayout({ cfg, agentId: runtime.agentId, rawSessionKey, isolationSubject: runtime.isolationSubject, config: params.config, execOverrides: params.execOverrides, skillsSnapshot: params.skillsSnapshot, workspaceDir: params.workspaceDir }); const docker = await resolveSandboxDockerUser({ backend: cfg.backend, docker: cfg.docker, workspaceDir }); const resolvedCfg = docker === cfg.docker ? cfg : { ...cfg, docker }; const backendFactory = requireSandboxBackendFactory(resolvedCfg.backend); const registeredRuntimeIds = await readRegisteredSandboxRuntimeIds({ backendId: resolvedCfg.backend, scopeKey }); const backend = await backendFactory({ sessionKey: rawSessionKey, scopeKey, ...registeredRuntimeIds.length > 0 ? { registeredRuntimeIds } : {}, workspaceDir, agentWorkspaceDir, skillsWorkspaceDir, cfg: resolvedCfg, ...params.requireCurrentConfig !== void 0 ? { requireCurrentConfig: params.requireCurrentConfig } : {} }); await updateRegistry({ containerName: backend.runtimeId, backendId: backend.id, runtimeLabel: backend.runtimeLabel, sessionKey: scopeKey, createdAtMs: Date.now(), lastUsedAtMs: Date.now(), image: backend.configLabel ?? resolvedCfg.docker.image, configLabelKind: backend.configLabelKind ?? "Image" }); const resolvedBrowserConfig = resolvedCfg.browser.enabled ? resolveBrowserConfig(params.config?.browser, params.config) : void 0; const evaluateEnabled = resolvedBrowserConfig?.evaluateEnabled ?? true; const bridgeAuth = cfg.browser.enabled ? await (async () => { const cfgForAuth = params.config ?? (await import("./config/config.js")).getRuntimeConfig(); let browserAuth = resolveBrowserControlAuth(cfgForAuth); try { browserAuth = (await ensureBrowserControlAuth({ cfg: cfgForAuth })).auth; } catch (error) { const message = error instanceof Error ? error.message : JSON.stringify(error); defaultRuntime.error?.(`Sandbox browser auth ensure failed: ${message}`); } return browserAuth; })() : void 0; if (resolvedCfg.browser.enabled && backend.capabilities?.browser !== true) throw new Error(`Sandbox backend "${backend.id}" does not support browser sandboxes yet.`); const browser = resolvedCfg.browser.enabled && backend.capabilities?.browser === true ? await ensureSandboxBrowser({ scopeKey, workspaceDir, agentWorkspaceDir, skillsWorkspaceDir, cfg: resolvedCfg, evaluateEnabled, bridgeAuth, ssrfPolicy: resolvedBrowserConfig?.ssrfPolicy }) : null; const sandboxContext = { enabled: true, ...runtime.sandboxRequired ? { required: true } : {}, backendId: backend.id, sessionKey: rawSessionKey, workspaceDir, agentWorkspaceDir, skillsWorkspaceDir, ...skillsEligibility ? { skillsEligibility } : {}, ...skillUsagePaths ? { skillUsagePaths } : {}, workspaceAccess: resolvedCfg.workspaceAccess, runtimeId: backend.runtimeId, runtimeLabel: backend.runtimeLabel, containerName: backend.runtimeId, containerWorkdir: backend.workdir, docker: resolvedCfg.docker, tools: resolvedCfg.tools, browserAllowHostControl: resolvedCfg.browser.allowHostControl, browser: browser ?? void 0, backend }; sandboxContext.fsBridge = backend.createFsBridge?.({ sandbox: sandboxContext }) ?? createSandboxFsBridge({ sandbox: sandboxContext }); return sandboxContext; } async function resolveSandboxContext(params) { const resolved = resolveSandboxSession(params); if (!resolved) return null; try { assertSandboxSessionSecretOwnerAvailable(params.config, resolved); return await resolveProvisionedSandboxContext(params, resolved); } catch (error) { throw toSandboxProvisioningError(error, resolved.cfg.backend); } } async function ensureSandboxWorkspaceForSession(params) { const resolved = resolveSandboxSession(params); if (!resolved) return null; assertSandboxSessionSecretOwnerAvailable(params.config, resolved); const { rawSessionKey, cfg, runtime } = resolved; const { agentWorkspaceDir, scopeKey, skillsEligibility, skillUsagePaths, skillsWorkspaceDir, workspaceDir } = await ensureSandboxWorkspaceLayout({ cfg, agentId: runtime.agentId, rawSessionKey, isolationSubject: runtime.isolationSubject, config: params.config, skillsSnapshot: params.skillsSnapshot, workspaceDir: params.workspaceDir }); const containerWorkdir = resolveSandboxWorkspaceInfoWorkdir({ cfg, rawSessionKey, scopeKey, workspaceDir, agentWorkspaceDir, skillsWorkspaceDir }); return { workspaceDir, ...containerWorkdir ? { containerWorkdir } : {}, skillsWorkspaceDir, ...skillsEligibility ? { skillsEligibility } : {}, ...skillUsagePaths ? { skillUsagePaths } : {}, workspaceAccess: cfg.workspaceAccess }; } //#endregion export { resolveSandboxContext as n, ensureSandboxWorkspaceForSession as t };