mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 03:15:46 -06:00
docs: document sandbox backend bridges
This commit is contained in:
@@ -24,6 +24,8 @@ export type {
|
||||
|
||||
const SANDBOX_BACKEND_FACTORIES_STATE_KEY = Symbol.for("openclaw.sandboxBackendFactories");
|
||||
|
||||
// Process-wide sandbox backend registry. Tests and plugins can install temporary
|
||||
// factories while core still auto-registers the bundled Docker and SSH backends.
|
||||
function getSandboxBackendFactories(): Map<SandboxBackendId, RegisteredSandboxBackend> {
|
||||
const globalStore = globalThis as typeof globalThis & {
|
||||
[SANDBOX_BACKEND_FACTORIES_STATE_KEY]?: Map<SandboxBackendId, RegisteredSandboxBackend>;
|
||||
@@ -40,6 +42,7 @@ function normalizeSandboxBackendId(id: string): SandboxBackendId {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/** Register or replace a sandbox backend and return a restore callback. */
|
||||
export function registerSandboxBackend(
|
||||
id: string,
|
||||
registration: SandboxBackendRegistration,
|
||||
@@ -59,14 +62,17 @@ export function registerSandboxBackend(
|
||||
};
|
||||
}
|
||||
|
||||
/** Look up a sandbox backend factory by normalized backend id. */
|
||||
export function getSandboxBackendFactory(id: string): SandboxBackendFactory | null {
|
||||
return getSandboxBackendFactories().get(normalizeSandboxBackendId(id))?.factory ?? null;
|
||||
}
|
||||
|
||||
/** Look up optional lifecycle management hooks for a registered backend. */
|
||||
export function getSandboxBackendManager(id: string): SandboxBackendManager | null {
|
||||
return getSandboxBackendFactories().get(normalizeSandboxBackendId(id))?.manager ?? null;
|
||||
}
|
||||
|
||||
/** Resolve a backend factory or throw the user-facing configuration error. */
|
||||
export function requireSandboxBackendFactory(id: string): SandboxBackendFactory {
|
||||
const factory = getSandboxBackendFactory(id);
|
||||
if (factory) {
|
||||
|
||||
@@ -31,6 +31,7 @@ type RunCommandOptions = {
|
||||
|
||||
export type { SandboxFsBridge, SandboxFsStat, SandboxResolvedPath } from "./fs-bridge.types.js";
|
||||
|
||||
/** Create the filesystem bridge for local Docker-style mounted sandboxes. */
|
||||
export function createSandboxFsBridge(params: {
|
||||
sandbox: SandboxFsBridgeContext;
|
||||
}): SandboxFsBridge {
|
||||
@@ -48,6 +49,8 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
const mountsByContainer = [...this.mounts].toSorted(
|
||||
(a, b) => b.containerRoot.length - a.containerRoot.length,
|
||||
);
|
||||
// Longest mount first keeps nested agent/skill mounts from being claimed by
|
||||
// the broader workspace root during symlink and mutation safety checks.
|
||||
this.pathGuard = new SandboxFsPathGuard({
|
||||
mountsByContainer,
|
||||
runCommand: (script, options) => this.runCommand(script, options),
|
||||
@@ -253,6 +256,8 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
): Promise<SandboxBackendCommandResult> {
|
||||
await this.pathGuard.assertPathChecks(plan.checks);
|
||||
if (plan.recheckBeforeCommand) {
|
||||
// Mutations that can create or swap path parents re-run the anchored
|
||||
// checks immediately before command execution to close TOCTOU gaps.
|
||||
await this.pathGuard.assertPathChecks(plan.checks);
|
||||
}
|
||||
return await this.runCommand(plan.script, {
|
||||
|
||||
@@ -40,12 +40,14 @@ type MountInfo = {
|
||||
source: RemoteMountSource;
|
||||
};
|
||||
|
||||
/** Minimal remote shell contract used by the SSH filesystem bridge. */
|
||||
export type RemoteShellSandboxHandle = {
|
||||
remoteWorkspaceDir: string;
|
||||
remoteAgentWorkspaceDir: string;
|
||||
runRemoteShellScript(params: SandboxBackendCommandParams): Promise<SandboxBackendCommandResult>;
|
||||
};
|
||||
|
||||
/** Create the filesystem bridge for remote shell-backed sandbox runtimes. */
|
||||
export function createRemoteShellSandboxFsBridge(params: {
|
||||
sandbox: SandboxFsBridgeContext;
|
||||
runtime: RemoteShellSandboxHandle;
|
||||
@@ -283,6 +285,8 @@ class RemoteShellSandboxFsBridge implements SandboxFsBridge {
|
||||
});
|
||||
}
|
||||
if (this.sandbox.workspaceAccess === "rw") {
|
||||
// Skill directories inside writable remote workspaces stay protected when
|
||||
// the original host mount exists, matching local bridge read-only rules.
|
||||
mounts.push(
|
||||
...buildRemoteProtectedSkillMounts({
|
||||
localRoot: agentRoot,
|
||||
@@ -470,6 +474,8 @@ class RemoteShellSandboxFsBridge implements SandboxFsBridge {
|
||||
allowFinalSymlinkForUnlink?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<string> {
|
||||
// Canonicalize the nearest existing ancestor and append the missing suffix.
|
||||
// This lets create/write operations validate paths that do not exist yet.
|
||||
const script = [
|
||||
"set -eu",
|
||||
'target="$1"',
|
||||
@@ -507,6 +513,8 @@ class RemoteShellSandboxFsBridge implements SandboxFsBridge {
|
||||
action: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
// Remote mutation helpers pin by parent path. Rejecting hardlinked regular
|
||||
// files avoids editing another mount-visible name through the same inode.
|
||||
const result = await this.runRemoteScript({
|
||||
script: [
|
||||
'if [ ! -e "$1" ] && [ ! -L "$1" ]; then exit 0; fi',
|
||||
|
||||
@@ -37,6 +37,7 @@ type ResolvedSshRuntimePaths = {
|
||||
remoteAgentWorkspaceDir: string;
|
||||
};
|
||||
|
||||
/** SSH backend lifecycle hooks for probing and removing remote sandbox copies. */
|
||||
export const sshSandboxBackendManager: SandboxBackendManager = {
|
||||
async describeRuntime({ entry, config, agentId }) {
|
||||
const cfg = resolveSandboxConfigForAgent(config, agentId);
|
||||
@@ -100,6 +101,7 @@ export const sshSandboxBackendManager: SandboxBackendManager = {
|
||||
},
|
||||
};
|
||||
|
||||
/** Create an SSH sandbox backend that mirrors the workspace to a remote target. */
|
||||
export async function createSshSandboxBackend(
|
||||
params: CreateSandboxBackendParams,
|
||||
): Promise<SandboxBackendHandle> {
|
||||
@@ -188,6 +190,8 @@ class SshSandboxBackendImpl {
|
||||
if (this.ensurePromise) {
|
||||
return await this.ensurePromise;
|
||||
}
|
||||
// Concurrent exec/fs calls share one remote copy bootstrap; failures reset
|
||||
// the promise so the next call can retry after transient SSH errors.
|
||||
this.ensurePromise = this.ensureRuntimeInner();
|
||||
try {
|
||||
await this.ensurePromise;
|
||||
@@ -294,6 +298,8 @@ function resolveSshRuntimePaths(workspaceRoot: string, scopeKey: string): Resolv
|
||||
|
||||
function buildSshSandboxRuntimeId(scopeKey: string): string {
|
||||
const trimmed = scopeKey.trim() || "session";
|
||||
// Keep the path human-readable while hashing the original scope to avoid
|
||||
// collisions after normalization and truncation.
|
||||
const safe = normalizeLowercaseStringOrEmpty(trimmed)
|
||||
.replace(/[^a-z0-9._-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
|
||||
Reference in New Issue
Block a user