import os from "node:os" ;
import path from "node:path" ;
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" ;
import {
createSandboxBrowserConfig,
createSandboxPruneConfig,
createSandboxSshConfig,
} from "../../../test/helpers/sandbox-fixtures.js" ;
import type { OpenClawConfig } from "../../config/config.js" ;
import type { SandboxConfig } from "./types.js" ;
const sshMocks = vi.hoisted(() => ({
createSshSandboxSessionFromSettings: vi.fn(),
disposeSshSandboxSession: vi.fn(),
runSshSandboxCommand: vi.fn(),
uploadDirectoryToSshTarget: vi.fn(),
buildSshSandboxArgv: vi.fn(),
}));
vi.mock("./ssh.js" , async () => {
const actual = await vi.importActual<typeof import ("./ssh.js" )>("./ssh.js" );
return {
...actual,
createSshSandboxSessionFromSettings: sshMocks.createSshSandboxSessionFromSettings,
disposeSshSandboxSession: sshMocks.disposeSshSandboxSession,
runSshSandboxCommand: sshMocks.runSshSandboxCommand,
uploadDirectoryToSshTarget: sshMocks.uploadDirectoryToSshTarget,
buildSshSandboxArgv: sshMocks.buildSshSandboxArgv,
};
});
const { createSshSandboxBackend, sshSandboxBackendManager } = await import ("./ssh-backend.js" );
function createConfig(): OpenClawConfig {
return {
agents: {
defaults: {
sandbox: {
mode: "all" ,
backend: "ssh" ,
scope: "session" ,
workspaceAccess: "rw" ,
ssh: {
target: "peter@example.com:2222" ,
command: "ssh" ,
workspaceRoot: "/remote/openclaw" ,
strictHostKeyChecking: true ,
updateHostKeys: true ,
},
},
},
},
};
}
function createSession() {
return {
command: "ssh" ,
configPath: path.join(os.tmpdir(), "openclaw-test-ssh-config" ),
host: "openclaw-sandbox" ,
};
}
function createBackendSandboxConfig(params?: { binds?: string[]; target?: string }): SandboxConfig {
return {
mode: "all" ,
backend: "ssh" ,
scope: "session" ,
workspaceAccess: "rw" as const ,
workspaceRoot: "~/.openclaw/sandboxes" ,
docker: {
image: "img" ,
containerPrefix: "prefix-" ,
workdir: "/workspace" ,
readOnlyRoot: true ,
tmpfs: ["/tmp" ],
network: "none" ,
capDrop: ["ALL" ],
env: {},
...(params?.binds ? { binds: params.binds } : {}),
},
ssh: {
...createSandboxSshConfig(
"/remote/openclaw" ,
params?.target ? { target: params.target } : {},
),
},
browser: createSandboxBrowserConfig({
image: "img" ,
containerPrefix: "prefix-" ,
cdpPort: 1 ,
vncPort: 2 ,
noVncPort: 3 ,
autoStartTimeoutMs: 1 ,
}),
tools: { allow: [], deny: [] },
prune: createSandboxPruneConfig(),
};
}
async function expectBackendCreationToReject(params: {
binds?: string[];
target?: string;
error: string;
}) {
await expect(
createSshSandboxBackend({
sessionKey: "s" ,
scopeKey: "s" ,
workspaceDir: "/tmp/workspace" ,
agentWorkspaceDir: "/tmp/workspace" ,
cfg: createBackendSandboxConfig({
binds: params.binds,
target: params.target,
}),
}),
).rejects.toThrow(params.error);
}
describe("ssh sandbox backend" , () => {
const originalEnv = { ...process.env };
beforeEach(() => {
vi.clearAllMocks();
sshMocks.createSshSandboxSessionFromSettings.mockResolvedValue(createSession());
sshMocks.disposeSshSandboxSession.mockResolvedValue(undefined);
sshMocks.runSshSandboxCommand.mockResolvedValue({
stdout: Buffer.from("1\n" ),
stderr: Buffer.alloc(0 ),
code: 0 ,
});
sshMocks.uploadDirectoryToSshTarget.mockResolvedValue(undefined);
sshMocks.buildSshSandboxArgv.mockImplementation(({ session, remoteCommand, tty }) => [
session.command,
"-F" ,
session.configPath,
tty ? "-tt" : "-T" ,
session.host,
remoteCommand,
]);
});
afterEach(() => {
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) {
delete process.env[key];
}
}
Object.assign(process.env, originalEnv);
vi.restoreAllMocks();
});
it("describes runtimes via the configured ssh target" , async () => {
const result = await sshSandboxBackendManager.describeRuntime({
entry: {
containerName: "openclaw-ssh-worker-abcd1234" ,
backendId: "ssh" ,
runtimeLabel: "openclaw-ssh-worker-abcd1234" ,
sessionKey: "agent:worker" ,
createdAtMs: 1 ,
lastUsedAtMs: 1 ,
image: "peter@example.com:2222" ,
configLabelKind: "Target" ,
},
config: createConfig(),
});
expect(result).toEqual({
running: true ,
actualConfigLabel: "peter@example.com:2222" ,
configLabelMatch: true ,
});
expect(sshMocks.createSshSandboxSessionFromSettings).toHaveBeenCalledWith(
expect.objectContaining({
target: "peter@example.com:2222" ,
workspaceRoot: "/remote/openclaw" ,
}),
);
expect(sshMocks.runSshSandboxCommand).toHaveBeenCalledWith(
expect.objectContaining({
remoteCommand: expect.stringContaining("/remote/openclaw/openclaw-ssh-agent-worker" ),
}),
);
});
it("removes runtimes by deleting the remote scope root" , async () => {
await sshSandboxBackendManager.removeRuntime({
entry: {
containerName: "openclaw-ssh-worker-abcd1234" ,
backendId: "ssh" ,
runtimeLabel: "openclaw-ssh-worker-abcd1234" ,
sessionKey: "agent:worker" ,
createdAtMs: 1 ,
lastUsedAtMs: 1 ,
image: "peter@example.com:2222" ,
configLabelKind: "Target" ,
},
config: createConfig(),
});
expect(sshMocks.runSshSandboxCommand).toHaveBeenCalledWith(
expect.objectContaining({
allowFailure: true ,
remoteCommand: expect.stringContaining('rm -rf -- "$1"' ),
}),
);
});
it("creates a remote-canonical backend that seeds once and reuses ssh exec" , async () => {
sshMocks.runSshSandboxCommand
.mockResolvedValueOnce({
stdout: Buffer.from("0\n" ),
stderr: Buffer.alloc(0 ),
code: 0 ,
})
.mockResolvedValueOnce({
stdout: Buffer.alloc(0 ),
stderr: Buffer.alloc(0 ),
code: 0 ,
})
.mockResolvedValueOnce({
stdout: Buffer.alloc(0 ),
stderr: Buffer.alloc(0 ),
code: 0 ,
});
const backend = await createSshSandboxBackend({
sessionKey: "agent:worker:task" ,
scopeKey: "agent:worker" ,
workspaceDir: "/tmp/workspace" ,
agentWorkspaceDir: "/tmp/agent" ,
cfg: {
mode: "all" ,
backend: "ssh" ,
scope: "session" ,
workspaceAccess: "rw" ,
workspaceRoot: "~/.openclaw/sandboxes" ,
docker: {
image: "openclaw-sandbox:bookworm-slim" ,
containerPrefix: "openclaw-sbx-" ,
workdir: "/workspace" ,
readOnlyRoot: true ,
tmpfs: ["/tmp" ],
network: "none" ,
capDrop: ["ALL" ],
env: { LANG: "C.UTF-8" },
},
ssh: {
target: "peter@example.com:2222" ,
command: "ssh" ,
workspaceRoot: "/remote/openclaw" ,
strictHostKeyChecking: true ,
updateHostKeys: true ,
},
browser: {
enabled: false ,
image: "openclaw-browser" ,
containerPrefix: "openclaw-browser-" ,
network: "bridge" ,
cdpPort: 9222 ,
vncPort: 5900 ,
noVncPort: 6080 ,
headless: true ,
enableNoVnc: false ,
allowHostControl: false ,
autoStart: false ,
autoStartTimeoutMs: 1000 ,
},
tools: { allow: [], deny: [] },
prune: { idleHours: 24 , maxAgeDays: 7 },
},
});
const execSpec = await backend.buildExecSpec({
command: "pwd" ,
env: { TEST_TOKEN: "1" },
usePty: false ,
});
expect(execSpec.argv).toEqual(
expect.arrayContaining(["ssh" , "-F" , createSession().configPath, "-T" , createSession().host]),
);
expect(execSpec.argv.at(-1 )).toContain("/remote/openclaw/openclaw-ssh-agent-worker" );
expect(sshMocks.uploadDirectoryToSshTarget).toHaveBeenCalledTimes(2 );
expect(sshMocks.uploadDirectoryToSshTarget).toHaveBeenNthCalledWith(
1 ,
expect.objectContaining({
localDir: "/tmp/workspace" ,
remoteDir: expect.stringContaining("/workspace" ),
}),
);
expect(sshMocks.uploadDirectoryToSshTarget).toHaveBeenNthCalledWith(
2 ,
expect.objectContaining({
localDir: "/tmp/agent" ,
remoteDir: expect.stringContaining("/agent" ),
}),
);
await backend.finalizeExec?.({
status: "completed" ,
exitCode: 0 ,
timedOut: false ,
token: execSpec.finalizeToken,
});
expect(sshMocks.disposeSshSandboxSession).toHaveBeenCalled();
});
it("filters blocked secrets from exec subprocess env" , async () => {
process.env.OPENAI_API_KEY = "sk-test-secret" ;
process.env.LANG = "en_US.UTF-8" ;
const backend = await createSshSandboxBackend({
sessionKey: "agent:worker:task" ,
scopeKey: "agent:worker" ,
workspaceDir: "/tmp/workspace" ,
agentWorkspaceDir: "/tmp/agent" ,
cfg: createBackendSandboxConfig({
target: "peter@example.com:2222" ,
}),
});
const execSpec = await backend.buildExecSpec({
command: "pwd" ,
env: {},
usePty: false ,
});
expect(execSpec.env?.OPENAI_API_KEY).toBeUndefined();
expect(execSpec.env?.LANG).toBe("en_US.UTF-8" );
});
it("rejects docker binds and missing ssh target" , async () => {
await expectBackendCreationToReject({
binds: ["/tmp:/tmp:rw" ],
target: "peter@example.com:22" ,
error: "does not support sandbox.docker.binds" ,
});
await expectBackendCreationToReject({
error: "requires agents.defaults.sandbox.ssh.target" ,
});
});
});
Messung V0.5 in Prozent C=98 H=98 G=97
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland