UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

951 lines (950 loc) • 40.2 kB
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { D as resolveExpiresAtMsFromDurationMs, c as asFiniteNumberInRange, o as asDateTimestampMs } from "./number-coercion-CkhMiRw8.mjs"; import { r as defaultRuntime } from "./runtime-BRg2NubB.mjs"; import { t as sleep } from "./sleep-dMoNLoKC.mjs"; import { t as resolveIdentityPathViaExistingAncestorSync } from "./boundary-path-CAv_plX7.mjs"; import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs"; import { c as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-CmKkMYdo.mjs"; import { o as captureGatewayStateOwner } from "./gateway-state-owner-eDtEELE9.mjs"; import { i as createLazyRuntimeNamedExport } from "./lazy-runtime-CJrzFzqU.mjs"; import { r as resolveAgentConfig } from "./agent-scope-config-BEk6-ThS.mjs"; import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.mjs"; import { r as withContainerEnvFile } from "./container-env-file-BtM4d167.mjs"; import { i as loadBundledPluginPublicSurfaceModuleSyncCore } from "./facade-loader-5qt3zpWf.mjs"; import { o as readActiveGatewayLockIdentity } from "./gateway-lock-Dw3jf-HK.mjs"; import { l as isSameSsrFPolicy } from "./ssrf-BUqrVNdU.mjs"; import { m as readAdmittedRunOperatorAuthority } from "./admitted-run-context-D0yzk2io.mjs"; import { n as ensureSandboxWorkspace } from "./workspace-C4gwB73P.mjs"; import { t as hashTextSha256 } from "./hash-DV5OH4uO.mjs"; import { a as computeSandboxBrowserConfigHash, t as resolveDockerEnvPolicyEpoch } from "./sanitize-env-vars-gtiti2lO.mjs"; import { t as captureChannelReadAuthority } from "./channel-read-authority-DJtFPb9x.mjs"; import { C as SANDBOX_DOCKER_CREATE_ARGS_EPOCH, b as SANDBOX_BROWSER_SECURITY_HASH_EPOCH, v as SANDBOX_BROWSER_IMAGE_CONTRACT_EPOCH, w as SANDBOX_GITHUB_CONFIG_DIR } from "./constants-B3lt5mRy.mjs"; import { n as isToolAllowed } from "./tool-policy-BQO0kfjo.mjs"; import { n as resolveSandboxBrowserDockerCreateConfig, r as resolveSandboxConfigForAgent } from "./config-CqeBZkfZ.mjs"; import { i as withSandboxRuntimeStatusInWorker, n as resolveSandboxRuntimeStatus } from "./runtime-status-BBzhhE6k.mjs"; import { r as toSandboxProvisioningError } from "./provisioning-error-z8JcKnZ7.mjs"; import "./workspace-mounts-pLi-RJAq.mjs"; import { n as resolveSubagentSessionAttachmentRootDir, t as SANDBOX_SUBAGENT_ATTACHMENTS_MOUNT } from "./subagent-attachment-paths-CBAfNBVu.mjs"; import { c as DOCKER_SANDBOX_ENGINE } from "./podman-runtime-B7hhCHYt.mjs"; import { b as readDockerPort, c as prepareSandboxMountPlan, d as handleHotSandboxConfigMismatch, g as dockerContainerState, i as execDocker, o as formatDockerDaemonUnavailableError, s as isDockerDaemonUnavailable, t as buildSandboxCreateArgs, u as sandboxMountPlanMatchesContainer, v as readDockerContainerEnvVar, y as readDockerContainerLabel } from "./docker-DML2JD9P.mjs"; import { i as slugifySessionKey, r as resolveSandboxWorkspaceLayoutPaths, t as buildSandboxContainerName } from "./shared-BC5l3Ktx.mjs"; import { a as readRegisteredSandboxRuntimeIds, i as readBrowserRegistry, p as updateBrowserRegistry } from "./registry-By6hiBqz.mjs"; import { n as validateNetworkMode } from "./validate-sandbox-security-OHEqWxOG.mjs"; import { a as getSandboxBackendWorkdirResolver, t as createSandboxBackend } from "./backend-D0O1Xvqr.mjs"; import { l as assertSshSandboxSecretOwnerAvailable } from "./ssh-backend-Cl4rzfBf.mjs"; import { a as withWorktreePreparationTiming, i as timeWorktreePreparationPhase } from "./preparation-timing-B2Li8Sr2.mjs"; import { n as prepareRemoteSkillConnections } from "./remote-skills-Bx0Z_N0C.mjs"; import { a as DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, o as resolveBrowserConfig, r as DEFAULT_OPENCLAW_BROWSER_COLOR, s as resolveProfile, t as DEFAULT_BROWSER_ACTION_TIMEOUT_MS } from "./browser-profiles-QVdSwN2O.mjs"; import { a as stopBrowserBridgeServer, i as startBrowserBridgeServer, n as stopCachedBrowserBridge, r as stopCachedBrowserBridgesForContainer, t as BROWSER_BRIDGES } from "./browser-bridges-DHvbUuMy.mjs"; import { t as createOneTimeTicketStore } from "./one-time-ticket-store-hVfYCIYG.mjs"; import { t as resolveSandboxDockerUser } from "./docker-user-GobfiwVp.mjs"; import { t as createSandboxFsBridge } from "./fs-bridge-SwZM89fd.mjs"; import fs from "node:fs"; import fs$1 from "node:fs/promises"; import crypto from "node:crypto"; //#region src/plugin-sdk/browser-control-auth.ts function loadBrowserControlAuthSurface() { return loadBundledPluginPublicSurfaceModuleSyncCore({ dirName: "browser", artifactBasename: "browser-control-auth.js" }); } /** Resolves browser control auth from config/env without generating new credentials. */ function resolveBrowserControlAuth(cfg, env = process.env) { return loadBrowserControlAuthSurface().resolveBrowserControlAuth(cfg, env); } /** Ensures browser control auth exists, returning any token generated during the call. */ async function ensureBrowserControlAuth(params) { return await loadBrowserControlAuthSurface().ensureBrowserControlAuth(params); } //#endregion //#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 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 requestedTtlMs = typeof params.ttlMs === "number" && params.ttlMs <= MAX_NOVNC_TOKEN_TTL_MS ? params.ttlMs : void 0; const expiresAt = resolveExpiresAtMsFromDurationMs(requestedTtlMs, { nowMs: now, minRemainingMs: 1 }) ?? resolveExpiresAtMsFromDurationMs(NOVNC_TOKEN_TTL_MS, { nowMs: now, minRemainingMs: 1 }); 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 sleep(Math.min(150, remainingMs)); } return false; } function buildSandboxBrowserResolvedConfig(params) { const cdpHost = "127.0.0.1"; const cdpPortRange = deriveDefaultBrowserCdpPortRange(0); return { enabled: true, evaluateEnabled: params.evaluateEnabled, controlPort: 0, 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 state = await dockerContainerState(containerName); params.assertCurrent?.(); const browserImage = params.cfg.browser.image; 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 }); params.assertCurrent?.(); 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; const noVncEnabled = isNoVncEnabled(params.cfg.browser); let noVncPassword; let cdpAuthToken; const removeExistingContainer = async () => { await stopCachedBrowserBridgesForContainer(containerName, params.assertCurrent); existing = BROWSER_BRIDGES.get(params.scopeKey); params.assertCurrent?.(); await execDocker([ "rm", "-f", containerName ], { allowFailure: true }); hasContainer = false; running = false; }; if (hasContainer) { if (noVncEnabled) { noVncPassword = await readDockerContainerEnvVar(containerName, "OPENCLAW_BROWSER_NOVNC_PASSWORD") ?? void 0; params.assertCurrent?.(); } cdpAuthToken = await readDockerContainerEnvVar(containerName, CDP_AUTH_TOKEN_ENV_KEY) ?? void 0; params.assertCurrent?.(); if (!cdpAuthToken) { defaultRuntime.log(`Removing stale sandbox browser container ${containerName} because it lacks the current CDP relay auth contract; it will be recreated.`); await removeExistingContainer(); } } if (hasContainer) { const registry = await readBrowserRegistry(); params.assertCurrent?.(); const registryEntry = registry.entries.find((entry) => entry.containerName === containerName); currentHash = await readDockerContainerLabel(containerName, "openclaw.configHash"); params.assertCurrent?.(); if (!currentHash) currentHash = registryEntry?.configHash ?? null; if (!currentHash || currentHash !== expectedHash) { 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 }); params.assertCurrent?.(); handleHotSandboxConfigMismatch({ containerName, scope: params.cfg.scope, sessionKey: params.scopeKey, browser: true, mountsChanged: !mountsMatch }); } else await removeExistingContainer(); } } const registryEntry = { containerName, sessionKey: params.scopeKey, workspaceDir: params.workspaceDir, createdAtMs: now, lastUsedAtMs: now, image: browserImage, configHash: currentHash !== expectedHash && running ? currentHash ?? void 0 : expectedHash }; if (params.withWorkspace) { params.assertCurrent?.(); await updateBrowserRegistry({ ...registryEntry, cdpPort: 0 }, params.assertCurrent); params.assertCurrent?.(); } 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); params.assertCurrent?.(); 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, 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; params.assertCurrent?.(); await withContainerEnvFile(env, async (envFile) => { args.push("--env-file", envFile, browserImage); params.assertCurrent?.(); await execDocker(args); }); } if (!hasContainer || !running) { params.assertCurrent?.(); await execDocker(["start", containerName]); } const mappedCdp = await readDockerPort(containerName, params.cfg.browser.cdpPort); params.assertCurrent?.(); 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; params.assertCurrent?.(); if (noVncEnabled && !noVncPassword) { noVncPassword = await readDockerContainerEnvVar(containerName, "OPENCLAW_BROWSER_NOVNC_PASSWORD") ?? void 0; params.assertCurrent?.(); } const existingProfile = existing ? resolveProfile(existing.bridge.state.resolved, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME) : null; 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 && !params.assertCurrent && 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 === params.evaluateEnabled); if (existing && !canReuse) await stopCachedBrowserBridge(params.scopeKey, existing, params.assertCurrent); let bridge = canReuse ? existing?.bridge ?? null : null; try { if (!bridge) { const startTarget = async () => { const currentState = await dockerContainerState(containerName); params.assertCurrent?.(); if (currentState.exists && !currentState.running) { params.assertCurrent?.(); await execDocker(["start", containerName]); } const ok = await waitForSandboxCdp({ cdpPort: mappedCdp, authToken: cdpAuthToken, timeoutMs: params.cfg.browser.autoStartTimeoutMs }); params.assertCurrent?.(); if (!ok) { 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; params.assertCurrent?.(); bridge = await startBrowserBridgeServer({ resolved: buildSandboxBrowserResolvedConfig({ cdpPort: mappedCdp, cdpAuthToken, headless: params.cfg.browser.headless, evaluateEnabled: params.evaluateEnabled, ssrfPolicy: params.ssrfPolicy }), authToken: desiredAuthToken, authPassword: desiredAuthPassword, onEnsureAttachTarget, resolveSandboxNoVncToken: consumeNoVncObserverToken }); params.assertCurrent?.(); BROWSER_BRIDGES.set(params.scopeKey, { bridge, containerName, authToken: desiredAuthToken, authPassword: desiredAuthPassword }); } params.assertCurrent?.(); await updateBrowserRegistry({ ...registryEntry, cdpPort: mappedCdp, noVncPort: mappedNoVnc ?? void 0 }, params.assertCurrent); params.assertCurrent?.(); const noVncUrl = mappedNoVnc && noVncEnabled ? buildNoVncObserverTokenUrl(bridge.baseUrl, issueNoVncObserverToken({ noVncPort: mappedNoVnc, password: noVncPassword })) : void 0; return { bridgeUrl: bridge.baseUrl, noVncUrl, containerName }; } catch (error) { if (!canReuse && bridge) try { await stopBrowserBridgeServer(bridge.server); if (BROWSER_BRIDGES.get(params.scopeKey)?.bridge === bridge) BROWSER_BRIDGES.delete(params.scopeKey); } catch (cleanupError) { defaultRuntime.error?.(`Sandbox browser bridge cleanup failed: ${String(cleanupError)}`); } throw error; } } //#endregion //#region src/agents/sandbox/state-owner.ts var SandboxStateOwnerRequiredError = class extends Error { constructor(cause) { super("Sandbox workspace preparation cannot run alongside a foreign live owner of the selected state root, or when that ownership cannot be verified. Run this SDK call inside the owning Gateway plugin/runtime, or stop the Gateway through its service owner and wait for any embedded run to finish, then retry offline. Inspect openclaw gateway status; this call does not acquire ownership or route remotely.", { cause }); this.code = "GATEWAY_STATE_OWNER_REQUIRED"; this.name = "SandboxStateOwnerRequiredError"; } }; /** Retain hosted custody; standalone SDK preparation stays local when the root is offline. */ async function captureSandboxStateOwner() { const resolveTarget = () => resolveIdentityPathViaExistingAncestorSync(resolveOpenClawStateSqlitePath()); try { const env = { ...process.env }; const databasePath = resolveTarget(); const owner = captureGatewayStateOwner(databasePath); if (owner && owner.role !== "gateway" && owner.role !== "agent-embedded") throw new SandboxStateOwnerRequiredError(); if (!owner && await readActiveGatewayLockIdentity({ env, includeEmbedded: true, requireInspection: true })) throw new SandboxStateOwnerRequiredError(); const assertCurrent = () => { try { if (resolveTarget() !== databasePath) throw new SandboxStateOwnerRequiredError(); owner?.assertCurrent(); } catch (error) { if (error instanceof SandboxStateOwnerRequiredError) throw error; throw new SandboxStateOwnerRequiredError(error); } }; assertCurrent(); return assertCurrent; } catch (error) { if (error instanceof SandboxStateOwnerRequiredError) throw error; throw new SandboxStateOwnerRequiredError(error); } } //#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-C4GRl8SC.mjs"), import("./exec-defaults-CxEb7R51.mjs") ]); params.assertCurrent?.(); await prepareRemoteSkillConnections(); params.assertCurrent?.(); const nodeSkills = resolveNodeExecEligibility({ cfg: params.config, sessionKey: params.rawSessionKey, agentId: params.agentId, execOverrides: params.execOverrides }); const eligibility = { nodeSkills, remote: getRemoteSkillEligibility({ advertiseExecNode: nodeSkills.canExec }) }; const skillUsagePaths = await syncWorkspaceSkills({ sourceWorkspaceDir: params.sourceWorkspaceDir, targetWorkspaceDir: params.targetWorkspaceDir, config: params.config, agentId: params.agentId, eligibility, skillsSnapshot: params.skillsSnapshot, assertCurrent: params.assertCurrent }); params.assertCurrent?.(); return { eligibility, skillUsagePaths }; } catch (error) { params.assertCurrent?.(); 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, guard) { const { rawSessionKey, runtime, localWorkspace } = selected; const assertCurrent = localWorkspace?.assertCurrent ?? params.assertCurrent; 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 }); assertCurrent?.(); if (cfg.workspaceAccess !== "rw") await ensureSandboxWorkspace(sandboxWorkspaceDir, agentWorkspaceDir, params.config?.agents?.defaults?.skipBootstrap, params.config?.agents?.defaults?.skipOptionalBootstrapFiles, guard); else await fs$1.mkdir(workspaceDir, { recursive: true }); assertCurrent?.(); const syncedSkills = await syncSandboxSkillsToWorkspace({ sourceWorkspaceDir: agentWorkspaceDir, targetWorkspaceDir: cfg.workspaceAccess === "rw" ? skillsWorkspaceDir : sandboxWorkspaceDir, config: params.config, agentId: runtime.agentId, rawSessionKey, execOverrides: params.execOverrides, skillsSnapshot: params.skillsSnapshot, assertCurrent }); return { agentWorkspaceDir, scopeKey, 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))}` }; return { rawSessionKey, runtime, cfg: runtime.sandboxRequired ? { ...configured, scope: "agent", workspaceAccess: runtime.workspaceAccess } : librarySelections?.length ? { ...configured, scope: "agent" } : configured }; } 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-2-eEJVch.mjs")).prepareLocalSandboxWorkspace({ cfg: params.config, agentId: runtime.agentId, sessionKey: rawSessionKey, workspaceDir: params.workspaceDir, sandbox: resolved.cfg, signal: readAdmittedRunOperatorAuthority(params.admittedRunContext)?.signal, 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, guard) { const selected = await prepareSandboxWorkspaceSelection(params, resolved); params.assertCurrent?.(); const { rawSessionKey, runtime, cfg, localWorkspace } = selected; const config = params.config; const allowGitHub = config && resolveAgentConfig(config, runtime.agentId)?.tools?.github?.allowInSandbox === true; if (allowGitHub && cfg.scope === "shared") { const message = `GitHub identity for agent "${runtime.agentId}" cannot enter a shared sandbox; use agent or session scope.`; sandboxLog.warn(message); throw new Error(message); } const githubIdentity = allowGitHub ? (await import("./github-tool-identity-CTSBYXGV.mjs")).prepareGitHubToolEnvironment({ config, agentId: runtime.agentId }) : void 0; const githubMount = githubIdentity ? await (async () => { const hostPath = githubIdentity.localIdentityEnv.GH_CONFIG_DIR; const stat = hostPath ? await fs$1.lstat(hostPath).catch(() => void 0) : void 0; if (!hostPath || !stat?.isDirectory() || stat.isSymbolicLink()) throw new Error("Sandbox GitHub identity profile is unavailable; reconnect GitHub Identity."); return { hostPath: await fs$1.realpath(hostPath), containerPath: SANDBOX_GITHUB_CONFIG_DIR }; })() : void 0; if (cfg.prune.idleHours !== 0 || cfg.prune.maxAgeDays !== 0) await (await import("./prune-DkddXB2i.mjs")).maybePruneSandboxes(void 0, params.assertCurrent, guard?.assertHost); const { agentWorkspaceDir, scopeKey, skillsEligibility, skillUsagePaths, skillsWorkspaceDir, workspaceDir } = await timeWorktreePreparationPhase("workspaceLayout", () => ensureSandboxWorkspaceLayout(params, selected, guard)); localWorkspace?.assertCurrent(); const docker = await resolveSandboxDockerUser({ backend: cfg.backend, docker: cfg.docker, workspaceDir }); const resolvedCfg = docker === cfg.docker ? cfg : { ...cfg, docker }; const executionCfg = githubIdentity ? { ...resolvedCfg, docker: { ...docker, env: { ...docker.env, ...githubIdentity.credentialScrubEnv, ...githubIdentity.localIdentityEnv, GH_CONFIG_DIR: SANDBOX_GITHUB_CONFIG_DIR } } } : resolvedCfg; let 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; } })(); if (githubMount) (readOnlyResourceMounts ??= []).push(githubMount); const registeredRuntimeIds = await readRegisteredSandboxRuntimeIds({ backendId: resolvedCfg.backend, scopeKey }); const provisionBackend = () => { params.assertCurrent?.(); return createSandboxBackend({ sessionKey: rawSessionKey, scopeKey, ...registeredRuntimeIds.length > 0 ? { registeredRuntimeIds } : {}, workspaceDir, ...localWorkspace ? { workspaceSource: "managed-worktree" } : {}, assertRuntimeCurrent: localWorkspace?.assertCurrent ?? params.assertCurrent, agentWorkspaceDir, skillsWorkspaceDir, readOnlyResourceMounts, cfg: executionCfg, ...params.requireCurrentConfig !== void 0 ? { requireCurrentConfig: params.requireCurrentConfig } : {} }, readAdmittedRunOperatorAuthority(params.admittedRunContext), githubIdentity, guard); }; const backend = await timeWorktreePreparationPhase("containerStart", () => localWorkspace ? localWorkspace.provision(provisionBackend) : provisionBackend()); params.assertCurrent?.(); 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 { params.assertCurrent?.(); const ensured = await ensureBrowserControlAuth({ cfg: cfgForAuth }); params.assertCurrent?.(); browserAuth = ensured.auth; } catch (error) { params.assertCurrent?.(); 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, withWorkspace: localWorkspace?.provision, assertCurrent: localWorkspace?.assertCurrent ?? params.assertCurrent }) : 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: executionCfg.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-2-eEJVch.mjs")).bindLocalSandboxWorkspace(sandboxContext, localWorkspace); } return sandboxContext; } async function resolveSandboxContext(params) { const resolved = resolveSandboxSession(params); if (!resolved) return null; const assertStateOwner = await captureSandboxStateOwner(); const assertCallerCurrent = params.assertCurrent; const assertCurrent = () => { assertStateOwner(); assertCallerCurrent?.(); assertStateOwner(); }; const ownedParams = { ...params, assertCurrent }; try { assertSandboxSessionSecretOwnerAvailable(params.config, resolved); const context = await withWorktreePreparationTiming("sandbox", () => resolveProvisionedSandboxContext(ownedParams, resolved, { assertHost: assertStateOwner, beforeLegacyApply: assertCurrent })); assertStateOwner(); return context; } catch (error) { if (error instanceof SandboxStateOwnerRequiredError) throw error; throw toSandboxProvisioningError(error, resolved.cfg.backend); } } async function ensureSandboxWorkspaceForSession(params, readSource) { const readAuthority = captureChannelReadAuthority(); const assertCurrent = () => readAuthority?.(); const ownedParams = { ...params, assertCurrent }; return withSandboxRuntimeStatusInWorker({ cfg: params.config, agentId: params.agentId, sessionKey: params.sessionKey }, { env: process.env, cwd: process.cwd(), assertCurrent, readSource }, async (preparedRuntimeStatus) => { const resolved = resolveSandboxSession({ ...params, preparedRuntimeStatus }); if (!resolved) return null; assertSandboxSessionSecretOwnerAvailable(params.config, resolved); const selected = await prepareSandboxWorkspaceSelection(ownedParams, resolved); assertCurrent(); const { rawSessionKey, cfg } = selected; const { agentWorkspaceDir, scopeKey, workspaceDir, ...workspace } = await ensureSandboxWorkspaceLayout(ownedParams, selected); assertCurrent(); const containerWorkdir = getSandboxBackendWorkdirResolver(cfg.backend)?.({ cfg, sessionKey: rawSessionKey, scopeKey, workspaceDir, agentWorkspaceDir, skillsWorkspaceDir: workspace.skillsWorkspaceDir }); return { workspaceDir, ...containerWorkdir ? { containerWorkdir } : {}, ...workspace, workspaceAccess: cfg.workspaceAccess }; }); } //#endregion export { resolveSandboxContext as n, ensureSandboxWorkspaceForSession as t };