UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

872 lines (871 loc) • 36.4 kB
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString } from "./string-coerce-6kR-86XC.mjs"; import { D as resolveExpiresAtMsFromDurationMs, c as asFiniteNumberInRange, o as asDateTimestampMs } from "./number-coercion-Dq1l5muL.mjs"; import { o as resolveUserPath } from "./home-dir-CjKH62jd.mjs"; import { r as defaultRuntime } from "./runtime-BC29JSZp.mjs"; import { i as createLazyRuntimeNamedExport } from "./lazy-runtime-CJrzFzqU.mjs"; import "./utils-DUAwk2p5.mjs"; import { a as openRootFile } from "./boundary-file-read-CCth2Hwg.mjs"; import { t as createSubsystemLogger } from "./subsystem-BlrgtxyM.mjs"; import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.mjs"; import { r as withContainerEnvFile } from "./container-env-file-D374GzwT.mjs"; import { f as isSameSsrFPolicy } from "./ssrf-D5TD9F1s.mjs"; import { f as readAdmittedRunOperatorAuthority } from "./admitted-run-context-CbCp9bj_.mjs"; import { a as DEFAULT_SOUL_FILENAME, n as DEFAULT_BOOTSTRAP_FILENAME, r as DEFAULT_IDENTITY_FILENAME, s as DEFAULT_USER_FILENAME, t as DEFAULT_AGENTS_FILENAME } from "./workspace-bootstrap-policy-DaS4N8nC.mjs"; import { n as readWorkspaceBootstrapFile, t as MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES } from "./workspace-bootstrap-read-B5B8MlNt.mjs"; import { t as ensureAgentWorkspace, u as publishBootstrapFile } from "./workspace-BUeMloSh.mjs"; import { C as SANDBOX_DOCKER_CREATE_ARGS_EPOCH, b as SANDBOX_BROWSER_SECURITY_HASH_EPOCH, v as SANDBOX_BROWSER_IMAGE_CONTRACT_EPOCH } from "./constants-CtO6U-a6.mjs"; import { n as isToolAllowed } from "./tool-policy-DKjEP0Qw.mjs"; import { i as resolveSandboxConfigForAgent, r as resolveSandboxBrowserDockerCreateConfig } from "./config-BwPxe8HY.mjs"; import { n as resolveSandboxRuntimeStatus } from "./runtime-status-Bfel3YpH.mjs"; import { r as toSandboxProvisioningError } from "./provisioning-error-CmuMzC-H.mjs"; import "./workspace-mounts-DN4AQsV5.mjs"; import { C as readDockerPort, S as readDockerContainerLabel, d as prepareSandboxMountPlan, i as execDocker, m as handleHotSandboxConfigMismatch, o as formatDockerDaemonUnavailableError, p as sandboxMountPlanMatchesContainer, s as isDockerDaemonUnavailable, t as buildSandboxCreateArgs, w as DOCKER_SANDBOX_ENGINE, x as readDockerContainerEnvVar, y as dockerContainerState } from "./docker-GD9F7iwg.mjs"; import { t as hashTextSha256 } from "./hash-DZK-8tRm.mjs"; import { a as computeSandboxBrowserConfigHash, t as resolveDockerEnvPolicyEpoch } from "./sanitize-env-vars-CzpOJE28.mjs"; import { i as slugifySessionKey, r as resolveSandboxWorkspaceLayoutPaths, t as buildSandboxContainerName } from "./shared-C46YXvaP.mjs"; import { a as readRegisteredSandboxRuntimeIds, i as readBrowserRegistry, p as updateBrowserRegistry } from "./registry-CMX-4QgM.mjs"; import { n as validateNetworkMode } from "./validate-sandbox-security-DZ8vrtZ4.mjs"; import { f as assertSshSandboxSecretOwnerAvailable } from "./ssh-backend-C6zHb4bM.mjs"; import { a as getSandboxBackendWorkdirResolver, t as createSandboxBackend } from "./backend-CXpXXfXv.mjs"; import { r as resolveSubagentSessionAttachmentRootDir, t as SANDBOX_SUBAGENT_ATTACHMENTS_MOUNT } from "./subagent-attachment-paths-CpdBk7rj.mjs"; import { n as prepareRemoteSkillConnections } from "./remote-skills-ml3IDwsA.mjs"; 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-BlskEDR-.mjs"; import { i as startBrowserBridgeServer, n as stopCachedBrowserBridge, r as stopCachedBrowserBridgesForContainer, t as BROWSER_BRIDGES } from "./browser-bridges-DTmX8xVd.mjs"; import { t as createOneTimeTicketStore } from "./one-time-ticket-store-B8dK9Gfu.mjs"; import { t as createSandboxFsBridge } from "./fs-bridge-B5VrKdSe.mjs"; 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 /** 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 = asFiniteNumberInRange(browserControlPort + 9, { min: 0, minExclusive: true, max: 65535 }) ?? 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 = createOneTimeTicketStore({ ttlMs: NOVNC_TOKEN_TTL_MS }); 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(); 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 crypto.randomBytes(24).toString("hex"); return NO_VNC_OBSERVER_TOKENS.mint({ noVncPort: params.noVncPort, password: normalizeOptionalString(params.password) }, { ttlMs: expiresAt - now, nowMs: now }).token; } function consumeNoVncObserverToken(token, nowMs) { const now = asDateTimestampMs(nowMs ?? Date.now()); if (now === void 0) return null; return NO_VNC_OBSERVER_TOKENS.consume(token, now) ?? null; } function buildNoVncObserverTokenUrl(baseUrl, token) { return `${baseUrl}/sandbox/novnc?${new URLSearchParams({ token }).toString()}`; } //#endregion //#region src/agents/sandbox/browser.ts 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; opts?.assertCurrent?.(); 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); let provisioning = true; const withWorkspace = params.withWorkspace; const provision = () => browserContainerLifecycleQueue.enqueue(containerName, async () => await ensureSandboxBrowserContainer({ ...params, withWorkspace: withWorkspace ? (operation) => provisioning ? operation() : withWorkspace(operation) : void 0 }, containerName)); try { return await (withWorkspace ? withWorkspace(provision) : provision()); } finally { provisioning = false; } } 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 mountPlan = await prepareSandboxMountPlan({ engine: DOCKER_SANDBOX_ENGINE, workspaceDir: params.workspaceDir, ...params.withWorkspace ? { workspaceSource: "managed-worktree" } : {}, assertCurrent: params.assertCurrent, agentWorkspaceDir: params.agentWorkspaceDir, skillsWorkspaceDir: params.skillsWorkspaceDir, workdir: params.cfg.docker.workdir, workspaceAccess: params.cfg.workspaceAccess, binds: browserDockerCfg.binds, tmpfs: browserDockerCfg.tmpfs }); 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, managedMounts: mountPlan.binds }); 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(); params.assertCurrent?.(); 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"); 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 mountsMatch = await sandboxMountPlanMatchesContainer({ engine: DOCKER_SANDBOX_ENGINE, containerName, plan: mountPlan }); handleHotSandboxConfigMismatch({ containerName, scope: params.cfg.scope, sessionKey: params.scopeKey, browser: true, mountsChanged: !mountsMatch }); } else { await stopExistingForContainer(); params.assertCurrent?.(); await execDocker([ "rm", "-f", containerName ], { allowFailure: true }); hasContainer = false; running = false; } } } if (params.withWorkspace) await updateBrowserRegistry({ containerName, sessionKey: params.scopeKey, workspaceDir: params.workspaceDir, createdAtMs: now, lastUsedAtMs: now, image: browserImage, configHash: hashMismatch && running ? currentHash ?? void 0 : expectedHash, cdpPort: 0 }); if (!hasContainer) { if (noVncEnabled) noVncPassword = generateNoVncPassword(); cdpAuthToken = crypto.randomBytes(24).toString("hex"); await ensureDockerNetwork(browserDockerCfg.network, { assertCurrent: params.assertCurrent, 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] }); for (const bind of mountPlan.skippedBinds) defaultRuntime.log(`sandbox browser: skipping user bind "${bind}" — container path conflicts with a protected read-only skill mount`); for (const bind of mountPlan.binds) args.push("-v", bind); 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); params.assertCurrent?.(); await execDocker(args); }); params.assertCurrent?.(); await execDocker(["start", containerName]); } else if (!running) { params.assertCurrent?.(); 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 canReuse = Boolean(!params.withWorkspace && existing && existing.bridge.server.listening && existing.containerName === containerName && existingProfile?.cdpPort === mappedCdp && existingProfile?.cdpUrl === cdpUrl && isSameSsrFPolicy(existing.bridge.state.resolved.ssrfPolicy, params.ssrfPolicy) && existing.authToken === desiredAuthToken && existing.authPassword === desiredAuthPassword && existing.bridge.state.resolved.evaluateEnabled === desiredEvaluateEnabled); if (existing && !canReuse) await stopCachedBrowserBridge(params.scopeKey, existing); let bridge = canReuse ? existing?.bridge ?? null : null; if (!bridge) { const startTarget = async () => { const currentState = await dockerContainerState(containerName); if (currentState.exists && !currentState.running) { params.assertCurrent?.(); await execDocker(["start", containerName]); } if (!await waitForSandboxCdp({ cdpPort: mappedCdp, authToken: cdpAuthToken, timeoutMs: params.cfg.browser.autoStartTimeoutMs })) { params.assertCurrent?.(); 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.`); } }; const onEnsureAttachTarget = params.cfg.browser.autoStart ? () => params.withWorkspace ? params.withWorkspace(startTarget) : startTarget() : void 0; bridge = 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 }); BROWSER_BRIDGES.set(params.scopeKey, { bridge, containerName, authToken: desiredAuthToken, authPassword: desiredAuthPassword }); } await updateBrowserRegistry({ containerName, workspaceDir: params.workspaceDir, 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 ? buildNoVncObserverTokenUrl(bridge.baseUrl, issueNoVncObserverToken({ noVncPort: mappedNoVnc, password: noVncPassword })) : void 0; return { bridgeUrl: bridge.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 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-B6plhnYr.mjs"), import("./exec-defaults-C2O-_0dm.mjs") ]); await prepareRemoteSkillConnections(); 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, selected, execOverrides) { const { rawSessionKey, runtime, localWorkspace } = selected; const cfg = localWorkspace ? { ...selected.cfg, workspaceAccess: "rw" } : selected.cfg; const { agentWorkspaceDir, sandboxWorkspaceDir, scopeKey, skillsWorkspaceDir, workspaceDir } = resolveSandboxWorkspaceLayoutPaths({ cfg, rawSessionKey, agentId: runtime.agentId, isolationSubject: localWorkspace && runtime.isolationSubject?.kind !== "session" ? { kind: "session", sessionKey: rawSessionKey } : runtime.isolationSubject, workspaceDir: localWorkspace?.workspaceDir ?? params.workspaceDir }); if (cfg.workspaceAccess !== "rw") await ensureSandboxWorkspace(sandboxWorkspaceDir, agentWorkspaceDir, params.config?.agents?.defaults?.skipBootstrap, params.config?.agents?.defaults?.skipOptionalBootstrapFiles); else await fs$1.mkdir(workspaceDir, { recursive: true }); const syncedSkills = await syncSandboxSkillsToWorkspace({ sourceWorkspaceDir: agentWorkspaceDir, targetWorkspaceDir: cfg.workspaceAccess === "rw" ? skillsWorkspaceDir : sandboxWorkspaceDir, config: params.config, agentId: runtime.agentId, rawSessionKey, 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 = params.preparedRuntimeStatus ? { ...params.preparedRuntimeStatus } : resolveSandboxRuntimeStatus({ cfg: params.config, agentId: params.agentId, sessionKey: rawSessionKey }); if (!runtime.sandboxed) return null; const configured = resolveSandboxConfigForAgent(params.config, runtime.agentId); const sessionAttachmentRoot = resolveSubagentSessionAttachmentRootDir({ agentId: runtime.agentId, childSessionKey: rawSessionKey }); try { if (fs.statSync(sessionAttachmentRoot).isDirectory()) runtime.isolationSubject = { kind: "session", sessionKey: rawSessionKey }; } catch {} 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 }; return { rawSessionKey, runtime, cfg: { ...configuredSandbox, scope: "agent", workspaceAccess: runtime.workspaceAccess } }; } function assertSandboxSessionSecretOwnerAvailable(config, resolved) { if (resolved.cfg.backend !== "ssh") return; assertSshSandboxSecretOwnerAvailable({ config, scope: resolved.cfg.scope, agentId: resolved.runtime.agentId }); } async function prepareSandboxWorkspaceSelection(params, resolved) { const { rawSessionKey, runtime } = resolved; const localWorkspace = params.config ? await (await import("./local-workspace-Ho90GTrf.mjs")).prepareLocalSandboxWorkspace({ cfg: params.config, agentId: runtime.agentId, sessionKey: rawSessionKey, workspaceDir: params.workspaceDir, backend: resolved.cfg.backend, assertCurrent: params.assertCurrent }) : void 0; const cfg = localWorkspace ? { ...resolved.cfg, scope: "session", workspaceAccess: resolveSandboxConfigForAgent(params.config, runtime.agentId).workspaceAccess === "ro" ? "ro" : "rw" } : resolved.cfg; if (!localWorkspace && runtime.sandboxRequired && resolveSandboxConfigForAgent(params.config, runtime.agentId).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, localWorkspace }; } async function resolveProvisionedSandboxContext(params, resolved) { const selected = await prepareSandboxWorkspaceSelection(params, resolved); const { rawSessionKey, runtime, cfg, localWorkspace } = selected; if (cfg.prune.idleHours !== 0 || cfg.prune.maxAgeDays !== 0) await (await import("./prune-DxGEFDak.mjs")).maybePruneSandboxes(); const { agentWorkspaceDir, scopeKey, skillsEligibility, skillUsagePaths, skillsWorkspaceDir, workspaceDir } = await ensureSandboxWorkspaceLayout(params, selected, params.execOverrides); localWorkspace?.assertCurrent(); const docker = await resolveSandboxDockerUser({ backend: cfg.backend, docker: cfg.docker, workspaceDir }); const resolvedCfg = docker === cfg.docker ? cfg : { ...cfg, docker }; const readOnlyResourceMounts = resolvedCfg.scope === "shared" ? void 0 : await (async () => { const hostPath = resolveSubagentSessionAttachmentRootDir({ agentId: runtime.agentId, childSessionKey: rawSessionKey }); try { if (!(await fs$1.stat(hostPath)).isDirectory()) return; return [{ hostPath: await fs$1.realpath(hostPath), containerPath: SANDBOX_SUBAGENT_ATTACHMENTS_MOUNT }]; } catch { return; } })(); const registeredRuntimeIds = await readRegisteredSandboxRuntimeIds({ backendId: resolvedCfg.backend, scopeKey }); const provisionBackend = () => createSandboxBackend({ sessionKey: rawSessionKey, scopeKey, ...registeredRuntimeIds.length > 0 ? { registeredRuntimeIds } : {}, workspaceDir, ...localWorkspace ? { workspaceSource: "managed-worktree", assertRuntimeCurrent: localWorkspace.assertCurrent } : {}, agentWorkspaceDir, skillsWorkspaceDir, readOnlyResourceMounts, cfg: resolvedCfg, ...params.requireCurrentConfig !== void 0 ? { requireCurrentConfig: params.requireCurrentConfig } : {} }, readAdmittedRunOperatorAuthority(params.admittedRunContext)); const backend = localWorkspace ? await localWorkspace.provision(provisionBackend) : await provisionBackend(); 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 provisionBrowser = () => ensureSandboxBrowser({ scopeKey, workspaceDir, agentWorkspaceDir, skillsWorkspaceDir, cfg: resolvedCfg, evaluateEnabled, bridgeAuth, ssrfPolicy: resolvedBrowserConfig?.ssrfPolicy, withWorkspace: localWorkspace?.provision, assertCurrent: localWorkspace?.assertCurrent }); const browser = resolvedCfg.browser.enabled && backend.capabilities?.browser === true ? await provisionBrowser() : null; const sandboxContext = { enabled: true, ...runtime.sandboxRequired ? { required: true } : {}, ...localWorkspace ? { workspaceSource: "managed-worktree", workspaceCwd: localWorkspace.workspaceCwd } : {}, backendId: backend.id, sessionKey: rawSessionKey, workspaceDir, agentWorkspaceDir, skillsWorkspaceDir, ...skillsEligibility ? { skillsEligibility } : {}, ...skillUsagePaths ? { skillUsagePaths } : {}, ...readOnlyResourceMounts ? { readOnlyResourceMounts } : {}, 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 }); if (localWorkspace) { localWorkspace.assertCurrent(); (await import("./local-workspace-Ho90GTrf.mjs")).bindLocalSandboxWorkspace(sandboxContext, localWorkspace); } 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 selected = await prepareSandboxWorkspaceSelection(params, resolved); const { rawSessionKey, cfg } = selected; const { agentWorkspaceDir, scopeKey, skillsEligibility, skillUsagePaths, skillsWorkspaceDir, workspaceDir } = await ensureSandboxWorkspaceLayout(params, selected); const containerWorkdir = getSandboxBackendWorkdirResolver(cfg.backend)?.({ cfg, sessionKey: 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 };