From 233d16b76f5406640ce34e6de7646ee91f599223 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 29 Jul 2026 12:59:38 +0800 Subject: [PATCH] fix(agents): preserve sandbox scope in container names (#115571) --- src/agents/sandbox/browser.ts | 5 ++--- src/agents/sandbox/docker.ts | 5 ++--- src/agents/sandbox/shared.test.ts | 33 +++++++++++++++++++++++++++++++ src/agents/sandbox/shared.ts | 11 +++++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/agents/sandbox/shared.test.ts diff --git a/src/agents/sandbox/browser.ts b/src/agents/sandbox/browser.ts index f245bb546264..ac1d2c51b009 100644 --- a/src/agents/sandbox/browser.ts +++ b/src/agents/sandbox/browser.ts @@ -53,7 +53,7 @@ import { issueNoVncObserverToken, } from "./novnc-auth.js"; import { readBrowserRegistry, updateBrowserRegistry } from "./registry.js"; -import { resolveSandboxAgentId, slugifySessionKey } from "./shared.js"; +import { buildSandboxContainerName, resolveSandboxAgentId, slugifySessionKey } from "./shared.js"; import { isToolAllowed } from "./tool-policy.js"; import type { SandboxBrowserContext, SandboxConfig } from "./types.js"; import { validateNetworkMode } from "./validate-sandbox-security.js"; @@ -242,8 +242,7 @@ export async function ensureSandboxBrowser(params: { } const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(params.scopeKey); - const name = `${params.cfg.browser.containerPrefix}${slug}`; - const containerName = name.slice(0, 63); + const containerName = buildSandboxContainerName(params.cfg.browser.containerPrefix, slug); let existing = BROWSER_BRIDGES.get(params.scopeKey); const stopExistingForContainer = async () => { await stopCachedBrowserBridgesForContainer(containerName); diff --git a/src/agents/sandbox/docker.ts b/src/agents/sandbox/docker.ts index ef95c12e5a16..a7a9f9d58c22 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -95,7 +95,7 @@ import { } from "./constants.js"; import { handleHotSandboxConfigMismatch } from "./current-config.js"; import { readRegistryEntry, updateRegistry } from "./registry.js"; -import { resolveSandboxScopeKey, slugifySessionKey } from "./shared.js"; +import { buildSandboxContainerName, resolveSandboxScopeKey, slugifySessionKey } from "./shared.js"; import type { SandboxConfig, SandboxDockerConfig, SandboxWorkspaceAccess } from "./types.js"; import { validateSandboxSecurity } from "./validate-sandbox-security.js"; import { @@ -503,8 +503,7 @@ export async function ensureSandboxContainer(params: { }) { const scopeKey = resolveSandboxScopeKey(params.cfg.scope, params.sessionKey); const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(scopeKey); - const name = `${params.cfg.docker.containerPrefix}${slug}`; - const containerName = name.slice(0, 63); + const containerName = buildSandboxContainerName(params.cfg.docker.containerPrefix, slug); const readOnlyWorkspaceSkillMounts = resolveReadOnlyWorkspaceSkillMounts({ workspaceDir: params.workspaceDir, agentWorkspaceDir: params.agentWorkspaceDir, diff --git a/src/agents/sandbox/shared.test.ts b/src/agents/sandbox/shared.test.ts new file mode 100644 index 000000000000..2d47973397e6 --- /dev/null +++ b/src/agents/sandbox/shared.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { buildSandboxContainerName, slugifySessionKey } from "./shared.js"; + +describe("buildSandboxContainerName", () => { + it("preserves scope identity when a custom prefix exceeds the Docker name limit", () => { + const commonPrefix = "custom-prefix-".repeat(6); + const first = buildSandboxContainerName( + `${commonPrefix}first`, + slugifySessionKey("session:first"), + ); + const second = buildSandboxContainerName( + `${commonPrefix}second`, + slugifySessionKey("session:second"), + ); + const sameScopeDifferentPrefix = buildSandboxContainerName( + `${commonPrefix}second`, + slugifySessionKey("session:first"), + ); + const oversizedSlug = buildSandboxContainerName( + commonPrefix, + slugifySessionKey("session:".concat("x".repeat(200))), + ); + + expect(first).not.toBe(second); + expect(first).not.toBe(sameScopeDifferentPrefix); + expect(first).toHaveLength(63); + expect(second).toHaveLength(63); + expect(oversizedSlug).toHaveLength(63); + expect(first).toMatch(/-[0-9a-f]{12}$/); + expect(second).toMatch(/-[0-9a-f]{12}$/); + expect(oversizedSlug).toMatch(/-[0-9a-f]{12}$/); + }); +}); diff --git a/src/agents/sandbox/shared.ts b/src/agents/sandbox/shared.ts index 1fd631ffd8d6..619b37877fab 100644 --- a/src/agents/sandbox/shared.ts +++ b/src/agents/sandbox/shared.ts @@ -25,6 +25,17 @@ export function slugifySessionKey(value: string) { return `${base}-${hash}`; } +/** Builds a bounded Docker name without truncating the scope-identity slug. */ +export function buildSandboxContainerName(prefix: string, slug: string): string { + const maxLength = 63; + const fullName = `${prefix}${slug}`; + if (fullName.length <= maxLength) { + return fullName; + } + const identitySuffix = `-${hashTextSha256(fullName).slice(0, 12)}`; + return `${fullName.slice(0, maxLength - identitySuffix.length)}${identitySuffix}`; +} + /** Resolves the per-session sandbox workspace directory under the configured sandbox root. */ function resolveSandboxWorkspaceDir(root: string, sessionKey: string) { const resolvedRoot = resolveUserPath(root);