fix(sandbox): include workspace dir in container name to prevent cross-instance collisions

When multiple OpenClaw instances run on the same host with different HOME
dirs, they all derived the same container name from the agent/scope key
alone (e.g. 'agent:main'). This caused every instance to share a single
Docker container, leaking file reads/writes across instances.

Mix workspaceDir (derived from HOME) into the slug hash so each instance
gets a distinct container name.

Fixes #51363
This commit is contained in:
Bryan Tegomoh 2026-03-20 21:24:08 -05:00
parent 5e417b44e1
commit 90fea3d714
3 changed files with 25 additions and 8 deletions

View File

@ -141,7 +141,13 @@ export async function ensureSandboxBrowser(params: {
return null;
}
const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(params.scopeKey);
// Include workspaceDir in the slug so co-hosted instances with different HOME
// dirs produce distinct container names (fixes #51363).
const slug = slugifySessionKey(
params.cfg.scope === "shared"
? `shared:${params.workspaceDir}`
: `${params.scopeKey}:${params.workspaceDir}`,
);
const name = `${params.cfg.browser.containerPrefix}${slug}`;
const containerName = name.slice(0, 63);
const state = await dockerContainerState(containerName);

View File

@ -3,6 +3,7 @@ import { Readable } from "node:stream";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { computeSandboxConfigHash } from "./config-hash.js";
import { ensureSandboxContainer } from "./docker.js";
import { slugifySessionKey } from "./shared.js";
import { collectDockerFlagValues } from "./test-args.js";
import type { SandboxConfig } from "./types.js";
@ -146,6 +147,7 @@ describe("ensureSandboxContainer config-hash recreation", () => {
it("recreates shared container when array-order change alters hash", async () => {
const workspaceDir = "/tmp/workspace";
const expectedName = `oc-test-${slugifySessionKey(`shared:${workspaceDir}`)}`.slice(0, 63);
const oldCfg = createSandboxConfig(["1.1.1.1", "8.8.8.8"]);
const newCfg = createSandboxConfig(["8.8.8.8", "1.1.1.1"]);
@ -167,7 +169,7 @@ describe("ensureSandboxContainer config-hash recreation", () => {
registryMocks.readRegistry.mockResolvedValue({
entries: [
{
containerName: "oc-test-shared",
containerName: expectedName,
sessionKey: "shared",
createdAtMs: 1,
lastUsedAtMs: 0,
@ -184,12 +186,11 @@ describe("ensureSandboxContainer config-hash recreation", () => {
cfg: newCfg,
});
expect(containerName).toBe("oc-test-shared");
expect(containerName).toBe(expectedName);
const dockerCalls = spawnState.calls.filter((call) => call.command === "docker");
expect(
dockerCalls.some(
(call) =>
call.args[0] === "rm" && call.args[1] === "-f" && call.args[2] === "oc-test-shared",
(call) => call.args[0] === "rm" && call.args[1] === "-f" && call.args[2] === expectedName,
),
).toBe(true);
const createCall = dockerCalls.find((call) => call.args[0] === "create");
@ -197,7 +198,7 @@ describe("ensureSandboxContainer config-hash recreation", () => {
expect(createCall?.args).toContain(`openclaw.configHash=${newHash}`);
expect(registryMocks.updateRegistry).toHaveBeenCalledWith(
expect.objectContaining({
containerName: "oc-test-shared",
containerName: expectedName,
configHash: newHash,
}),
);
@ -205,6 +206,10 @@ describe("ensureSandboxContainer config-hash recreation", () => {
it("applies custom binds after workspace mounts so overlapping binds can override", async () => {
const workspaceDir = "/tmp/workspace";
const expectedContainerName = `oc-test-${slugifySessionKey(`shared:${workspaceDir}`)}`.slice(
0,
63,
);
const cfg = createSandboxConfig(
["1.1.1.1"],
["/tmp/workspace-shared/USER.md:/workspace/USER.md:ro"],
@ -222,7 +227,7 @@ describe("ensureSandboxContainer config-hash recreation", () => {
registryMocks.readRegistry.mockResolvedValue({
entries: [
{
containerName: "oc-test-shared",
containerName: expectedContainerName,
sessionKey: "shared",
createdAtMs: 1,
lastUsedAtMs: 0,

View File

@ -496,7 +496,13 @@ export async function ensureSandboxContainer(params: {
cfg: SandboxConfig;
}) {
const scopeKey = resolveSandboxScopeKey(params.cfg.scope, params.sessionKey);
const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(scopeKey);
// Include workspaceDir in the slug so co-hosted instances with different HOME
// dirs produce distinct container names (fixes #51363).
const slug = slugifySessionKey(
params.cfg.scope === "shared"
? `shared:${params.workspaceDir}`
: `${scopeKey}:${params.workspaceDir}`,
);
const name = `${params.cfg.docker.containerPrefix}${slug}`;
const containerName = name.slice(0, 63);
const expectedHash = computeSandboxConfigHash({