openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
942 lines (941 loc) • 39.5 kB
JavaScript
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-CuVhdCBs.mjs";
import { i as createLazyRuntimeNamedExport } from "./lazy-runtime-CJrzFzqU.mjs";
import { r as defaultRuntime } from "./runtime-DlO3PTAd.mjs";
import { t as resolveIdentityPathViaExistingAncestorSync } from "./boundary-path-CAv_plX7.mjs";
import { t as createSubsystemLogger } from "./subsystem-I-uqdx4f.mjs";
import { c as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-DfifnSGp.mjs";
import { o as captureGatewayStateOwner } from "./gateway-state-owner-DmHycK5o.mjs";
import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.mjs";
import { r as resolveAgentConfig } from "./agent-scope-config-CDYpzA0r.mjs";
import { r as withContainerEnvFile } from "./container-env-file-BATVpQPA.mjs";
import { o as readActiveGatewayLockIdentity } from "./gateway-lock-B6mKYs2x.mjs";
import { l as isSameSsrFPolicy } from "./ssrf-v45DW-yR.mjs";
import { f as readAdmittedRunOperatorAuthority } from "./admitted-run-context-QJnFIxUZ.mjs";
import { n as ensureSandboxWorkspace } from "./workspace-BHjQqjjT.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-sQ6GWokQ.mjs";
import { n as isToolAllowed } from "./tool-policy-eD9SQrK2.mjs";
import { i as resolveSandboxConfigForAgent, r as resolveSandboxBrowserDockerCreateConfig } from "./config-DGIfx80_.mjs";
import { n as resolveSandboxRuntimeStatus } from "./runtime-status-ClNFv3m-.mjs";
import { r as toSandboxProvisioningError } from "./provisioning-error-bD2pXu_s.mjs";
import "./workspace-mounts-foAiMu-T.mjs";
import { c as DOCKER_SANDBOX_ENGINE } from "./podman-runtime-bNvz9prY.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-DeBQ6N6n.mjs";
import { t as hashTextSha256 } from "./hash-DV5OH4uO.mjs";
import { a as computeSandboxBrowserConfigHash, t as resolveDockerEnvPolicyEpoch } from "./sanitize-env-vars-Dk-EPdUs.mjs";
import { i as slugifySessionKey, r as resolveSandboxWorkspaceLayoutPaths, t as buildSandboxContainerName } from "./shared-2nFGF-qq.mjs";
import { a as readRegisteredSandboxRuntimeIds, i as readBrowserRegistry, p as updateBrowserRegistry } from "./registry-BmXc2WeW.mjs";
import { n as validateNetworkMode } from "./validate-sandbox-security-hKO_A4jd.mjs";
import { f as assertSshSandboxSecretOwnerAvailable } from "./ssh-backend-BwUE1Ule.mjs";
import { a as getSandboxBackendWorkdirResolver, t as createSandboxBackend } from "./backend-DTIyDfZM.mjs";
import { n as resolveSubagentSessionAttachmentRootDir, t as SANDBOX_SUBAGENT_ATTACHMENTS_MOUNT } from "./subagent-attachment-paths-0g-Tlw0v.mjs";
import { n as prepareRemoteSkillConnections } from "./remote-skills-CVJsA9ab.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-B1vfrmzn.mjs";
import { a as stopBrowserBridgeServer, i as startBrowserBridgeServer, n as stopCachedBrowserBridge, r as stopCachedBrowserBridgesForContainer, t as BROWSER_BRIDGES } from "./browser-bridges-BI_v0pGs.mjs";
import { t as createOneTimeTicketStore } from "./one-time-ticket-store-B8dK9Gfu.mjs";
import { t as resolveSandboxDockerUser } from "./docker-user-GobfiwVp.mjs";
import { t as createSandboxFsBridge } from "./fs-bridge-QId4ezR0.mjs";
import fs from "node:fs";
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, params.assertCurrent);
existing = BROWSER_BRIDGES.get(params.scopeKey);
};
const state = await dockerContainerState(containerName);
params.assertCurrent?.();
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
});
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;
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;
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 stopExistingForContainer();
params.assertCurrent?.();
await execDocker([
"rm",
"-f",
containerName
], { allowFailure: true });
hasContainer = false;
running = false;
}
}
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;
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
});
params.assertCurrent?.();
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;
}
}
}
const registryEntry = {
containerName,
sessionKey: params.scopeKey,
workspaceDir: params.workspaceDir,
createdAtMs: now,
lastUsedAtMs: now,
image: browserImage,
configHash: hashMismatch && 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,
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;
params.assertCurrent?.();
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);
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;
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 && !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 === desiredEvaluateEnabled);
if (existing && !canReuse) await stopCachedBrowserBridge(params.scopeKey, existing, params.assertCurrent);
let bridge = canReuse ? existing?.bridge ?? null : null;
let createdBridge;
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({
controlPort: 0,
cdpPort: mappedCdp,
cdpAuthToken,
headless: params.cfg.browser.headless,
evaluateEnabled: desiredEvaluateEnabled,
ssrfPolicy: params.ssrfPolicy
}),
authToken: desiredAuthToken,
authPassword: desiredAuthPassword,
onEnsureAttachTarget,
resolveSandboxNoVncToken: consumeNoVncObserverToken
});
createdBridge = bridge;
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 (createdBridge) try {
await stopBrowserBridgeServer(createdBridge.server);
if (BROWSER_BRIDGES.get(params.scopeKey)?.bridge === createdBridge) 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-DZj7JbbE.mjs"),
import("./exec-defaults-Cwc5LriW.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))}`
};
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-CO9-gKYh.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-gaz2kXwn.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-BYSTE723.mjs")).maybePruneSandboxes(void 0, params.assertCurrent, guard?.assertHost);
const { agentWorkspaceDir, scopeKey, skillsEligibility, skillUsagePaths, skillsWorkspaceDir, workspaceDir } = await 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 = localWorkspace ? await localWorkspace.provision(provisionBackend) : await 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-CO9-gKYh.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 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) {
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 };