Skip to content

Commit 9e50263

Browse files
committed
Address browser runtime review feedback
1 parent a06e2c5 commit 9e50263

7 files changed

Lines changed: 121 additions & 15 deletions

File tree

packages/lib/src/core/templates.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import type { TemplateConfig } from "./domain.js"
22
import type { ResolvedComposeResourceLimits } from "./resource-limits.js"
33
import { renderEntrypoint } from "./templates-entrypoint.js"
4-
import { type ComposeResourceLimits, renderDockerCompose } from "./templates/docker-compose.js"
4+
import {
5+
type ComposeResourceLimits,
6+
type DockerComposeRenderOptions,
7+
renderDockerCompose
8+
} from "./templates/docker-compose.js"
59
import { renderDockerfile } from "./templates/dockerfile.js"
610
import { renderPlaywrightBrowserRuntime } from "./templates/playwright-browser-runtime.js"
711
import { renderPlaywrightBrowserDockerfile, renderPlaywrightStartExtra } from "./templates/playwright.js"
@@ -10,6 +14,14 @@ export type FileSpec =
1014
| { readonly _tag: "File"; readonly relativePath: string; readonly contents: string; readonly mode?: number }
1115
| { readonly _tag: "Dir"; readonly relativePath: string }
1216

17+
export type TemplateRenderOptions = {
18+
readonly compose: DockerComposeRenderOptions
19+
}
20+
21+
const defaultTemplateRenderOptions: TemplateRenderOptions = {
22+
compose: { enableLocalDockerSocket: false }
23+
}
24+
1325
const renderGitignore = (): string =>
1426
`# docker-git project files
1527
# NOTE: bootstrap secrets stay local-only and should not be committed.
@@ -46,7 +58,8 @@ const renderConfigJson = (config: TemplateConfig): string =>
4658

4759
export const planFiles = (
4860
config: TemplateConfig,
49-
composeResourceLimits?: ResolvedComposeResourceLimits | ComposeResourceLimits
61+
composeResourceLimits?: ResolvedComposeResourceLimits | ComposeResourceLimits,
62+
options: TemplateRenderOptions = defaultTemplateRenderOptions
5063
): ReadonlyArray<FileSpec> => {
5164
const maybePlaywrightFiles = config.enableMcpPlaywright
5265
? ([
@@ -72,7 +85,7 @@ export const planFiles = (
7285
{
7386
_tag: "File",
7487
relativePath: "docker-compose.yml",
75-
contents: renderDockerCompose(config, composeResourceLimits)
88+
contents: renderDockerCompose(config, composeResourceLimits, options.compose)
7689
},
7790
{ _tag: "File", relativePath: ".dockerignore", contents: renderDockerignore() },
7891
{ _tag: "File", relativePath: "docker-git.json", contents: renderConfigJson(config) },

packages/lib/src/core/templates/docker-compose.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,16 @@ type PlaywrightFragments = Pick<
3030
"maybeDependsOn" | "maybeDockerSocketMount" | "maybePlaywrightEnv" | "maybeBrowserVolume"
3131
>
3232

33+
export type DockerComposeRenderOptions = {
34+
readonly enableLocalDockerSocket: boolean
35+
}
36+
3337
export type ComposeResourceLimits = {
3438
readonly main: ResolvedComposeResourceLimits | undefined
3539
readonly playwright: ResolvedComposeResourceLimits | undefined
3640
}
3741

42+
const defaultDockerComposeRenderOptions: DockerComposeRenderOptions = { enableLocalDockerSocket: false }
3843
const sharedCodexVolumeKey = "docker_git_shared_codex"
3944
const sharedCacheVolumeKey = "docker_git_shared_cache"
4045
const bootstrapVolumeKey = "docker_git_bootstrap"
@@ -81,7 +86,10 @@ const renderGpu = (gpu: TemplateConfig["gpu"]): string =>
8186

8287
const renderBootstrapMounts = (): string => ` - ${bootstrapVolumeKey}:/opt/docker-git/bootstrap/source:ro`
8388

84-
const renderOptionalDockerSocketMount = (): string => ` - /var/run/docker.sock:/var/run/docker.sock`
89+
const renderOptionalDockerSocketMount = (enableLocalDockerSocket: boolean): string =>
90+
enableLocalDockerSocket
91+
? ` - /var/run/docker.sock:/var/run/docker.sock`
92+
: ""
8593

8694
const renderEnvFiles = (config: TemplateConfig): string =>
8795
` env_file:\n - ${config.envGlobalPath}\n - ${config.envProjectPath}\n`
@@ -93,7 +101,8 @@ const renderBrowserLimitEnv = (
93101

94102
const buildPlaywrightFragments = (
95103
config: TemplateConfig,
96-
resourceLimits: ResolvedComposeResourceLimits | undefined
104+
resourceLimits: ResolvedComposeResourceLimits | undefined,
105+
options: DockerComposeRenderOptions
97106
): PlaywrightFragments => {
98107
if (!config.enableMcpPlaywright) {
99108
return {
@@ -110,7 +119,7 @@ const buildPlaywrightFragments = (
110119

111120
return {
112121
maybeDependsOn: "",
113-
maybeDockerSocketMount: renderOptionalDockerSocketMount(),
122+
maybeDockerSocketMount: renderOptionalDockerSocketMount(options.enableLocalDockerSocket),
114123
maybePlaywrightEnv:
115124
` MCP_PLAYWRIGHT_ENABLE: "1"\n MCP_PLAYWRIGHT_CDP_ENDPOINT: "http://127.0.0.1:9223"\n DOCKER_GIT_PROJECT_CONTAINER_NAME: "${config.containerName}"\n DOCKER_GIT_BROWSER_CONTAINER_NAME: "${browserContainerName}"\n DOCKER_GIT_BROWSER_IMAGE_NAME: "${browserImageName}"\n DOCKER_GIT_BROWSER_VOLUME_NAME: "${browserVolumeName}"\n${
116125
renderBrowserLimitEnv("DOCKER_GIT_BROWSER_CPU_LIMIT", resourceLimits?.cpuLimit)
@@ -137,7 +146,8 @@ const normalizeComposeResourceLimits = (
137146

138147
const buildComposeFragments = (
139148
config: TemplateConfig,
140-
resourceLimits: ComposeResourceLimits
149+
resourceLimits: ComposeResourceLimits,
150+
options: DockerComposeRenderOptions
141151
): ComposeFragments => {
142152
const networkMode = config.dockerNetworkMode
143153
const networkName = resolveComposeNetworkName(config)
@@ -151,7 +161,7 @@ const buildComposeFragments = (
151161
const maybeClaudeAuthLabelEnv = renderClaudeAuthLabelEnv(claudeAuthLabel)
152162
const maybeAgentModeEnv = renderAgentModeEnv(config.agentMode)
153163
const maybeAgentAutoEnv = renderAgentAutoEnv(config.agentAuto)
154-
const playwright = buildPlaywrightFragments(config, resourceLimits.playwright)
164+
const playwright = buildPlaywrightFragments(config, resourceLimits.playwright, options)
155165

156166
return {
157167
networkMode,
@@ -243,10 +253,11 @@ const renderComposeVolumes = (config: TemplateConfig, maybeBrowserVolume: string
243253

244254
export const renderDockerCompose = (
245255
config: TemplateConfig,
246-
resourceLimits?: ResolvedComposeResourceLimits | ComposeResourceLimits
256+
resourceLimits?: ResolvedComposeResourceLimits | ComposeResourceLimits,
257+
options: DockerComposeRenderOptions = defaultDockerComposeRenderOptions
247258
): string => {
248259
const limits = normalizeComposeResourceLimits(resourceLimits)
249-
const fragments = buildComposeFragments(config, limits)
260+
const fragments = buildComposeFragments(config, limits, options)
250261
return [
251262
`name: ${resolveComposeProjectName(config)}`,
252263
renderComposeServices(config, fragments, limits),

packages/lib/src/core/templates/playwright-browser-runtime.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,37 @@ docker_git_stop_playwright_browser() {
2424
docker rm -f "$container_name" >/dev/null 2>&1 || true
2525
}
2626
27+
docker_git_cleanup_orphaned_playwright_browsers() {
28+
if ! docker_git_browser_has_docker; then
29+
return 0
30+
fi
31+
32+
local browser_id
33+
while IFS= read -r browser_id; do
34+
if [[ -z "$browser_id" ]]; then
35+
continue
36+
fi
37+
38+
local project_container
39+
project_container="$(docker inspect --format '{{ index .Config.Labels "docker-git.project-container" }}' "$browser_id" 2>/dev/null || true)"
40+
41+
local project_running
42+
project_running="false"
43+
if [[ -n "$project_container" && "$project_container" != "<no value>" ]]; then
44+
project_running="$(docker inspect --format '{{ .State.Running }}' "$project_container" 2>/dev/null || true)"
45+
fi
46+
47+
if [[ "$project_running" == "true" ]]; then
48+
continue
49+
fi
50+
51+
local browser_name
52+
browser_name="$(docker inspect --format '{{ .Name }}' "$browser_id" 2>/dev/null | sed 's#^/##' || true)"
53+
docker_git_browser_log "removing orphaned browser container \${browser_name:-$browser_id}"
54+
docker rm -f "$browser_id" >/dev/null 2>&1 || true
55+
done < <(docker ps -a -q --filter "label=docker-git.browser=1" --filter "label=docker-git.project-container")
56+
}
57+
2758
docker_git_start_playwright_browser() {
2859
if [[ "\${MCP_PLAYWRIGHT_ENABLE:-0}" != "1" ]]; then
2960
docker_git_stop_playwright_browser || true
@@ -50,13 +81,15 @@ docker_git_start_playwright_browser() {
5081
return 0
5182
fi
5283
84+
docker_git_stop_playwright_browser || true
85+
docker_git_cleanup_orphaned_playwright_browsers || true
86+
5387
docker_git_browser_log "building $image_name"
5488
docker build -t "$image_name" -f "$context_dir/Dockerfile.browser" "$context_dir" >/var/log/docker-git-browser-build.log 2>&1 || {
5589
docker_git_browser_log "browser image build failed; see /var/log/docker-git-browser-build.log"
5690
return 0
5791
}
5892
59-
docker_git_stop_playwright_browser || true
6093
docker volume create "$volume_name" >/dev/null
6194
6295
local args=(
@@ -96,6 +129,6 @@ docker_git_start_playwright_browser() {
96129
// FORMAT THEOREM: start(main) -> running(browser) with network(browser) = container:main OR logged_warning
97130
// PURITY: SHELL
98131
// EFFECT: shell commands executed by generated entrypoint
99-
// INVARIANT: browser data volume is preserved; runtime cleanup removes only the browser container
100-
// COMPLEXITY: O(build + docker-run)/O(1)
132+
// INVARIANT: browser data volume is preserved; runtime cleanup removes only browser-labeled containers
133+
// COMPLEXITY: O(b + build + docker-run)/O(1), where b = browser-labeled containers
101134
export const renderPlaywrightBrowserRuntime = (): string => playwrightBrowserRuntimeScript

packages/lib/src/shell/files.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
withDefaultResourceLimitIntent
1212
} from "../core/resource-limits.js"
1313
import { type FileSpec, planFiles } from "../core/templates.js"
14+
import { resolveDockerEnvValue } from "./docker-auth.js"
1415
import { FileExistsError } from "./errors.js"
1516
import { resolveBaseDir } from "./paths.js"
1617
import { resolveWorkspaceRoot } from "./workspace-root.js"
@@ -45,6 +46,8 @@ const loadHostResources = (): Effect.Effect<
4546

4647
const isFileSpec = (spec: FileSpec): spec is Extract<FileSpec, { readonly _tag: "File" }> => spec._tag === "File"
4748

49+
const shouldMountLocalDockerSocket = (): boolean => resolveDockerEnvValue("DOCKER_GIT_PROJECT_DOCKER_HOST") === null
50+
4851
const resolveSpecPath = (
4952
path: Path.Path,
5053
baseDir: string,
@@ -242,7 +245,9 @@ export const writeProjectFiles = (
242245
main: resolveComposeResourceLimits(normalizedConfig, hostResources),
243246
playwright: resolvePlaywrightComposeResourceLimits(normalizedConfig, hostResources)
244247
}
245-
const specs = planFiles(normalizedConfig, composeResourceLimits)
248+
const specs = planFiles(normalizedConfig, composeResourceLimits, {
249+
compose: { enableLocalDockerSocket: shouldMountLocalDockerSocket() }
250+
})
246251
const created: Array<string> = []
247252
const existingFilePaths = force ? [] : yield* _(collectExistingFilePaths(fs, path, baseDir, specs))
248253
const existingSet = new Set(existingFilePaths)

packages/lib/tests/core/templates.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,14 +601,30 @@ describe("renderDockerCompose", () => {
601601
expect(compose).toContain('DOCKER_GIT_BROWSER_VOLUME_NAME: "dg-test-home-browser"')
602602
expect(compose).toContain('DOCKER_GIT_BROWSER_CPU_LIMIT: "${DOCKER_GIT_BROWSER_CPU_LIMIT:-1.5}"')
603603
expect(compose).toContain('DOCKER_GIT_BROWSER_RAM_LIMIT: "${DOCKER_GIT_BROWSER_RAM_LIMIT:-2g}"')
604-
expect(compose).toContain(" - /var/run/docker.sock:/var/run/docker.sock")
604+
expect(compose).not.toContain(" - /var/run/docker.sock:/var/run/docker.sock")
605605
expect(compose).toContain(" dg-test-home-browser:")
606606
expect(compose).not.toContain("\n dg-test-browser:\n")
607607
expect(compose).not.toContain("dg-test-browser:\n build:")
608608
expect(compose).not.toContain("restart:")
609609
expect((compose.match(/\n dns:\n/g) ?? []).length).toBe(1)
610610
})
611611

612+
it("renders local Docker socket mount only when explicitly enabled", () => {
613+
const compose = renderDockerCompose(
614+
makeTemplateConfig({
615+
enableMcpPlaywright: true,
616+
gpu: "none",
617+
}),
618+
{
619+
cpuLimit: 1.5,
620+
ramLimit: "2g"
621+
},
622+
{ enableLocalDockerSocket: true }
623+
)
624+
625+
expect(compose).toContain(" - /var/run/docker.sock:/var/run/docker.sock")
626+
})
627+
612628
it("applies separate resource limits for the nested browser runtime when provided", () => {
613629
const compose = renderDockerCompose(
614630
makeTemplateConfig({

packages/lib/tests/usecases/mcp-playwright.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ describe("enableMcpPlaywrightProjectFiles", () => {
159159
expect(startExtra).toContain("docker-git-cdp-guard")
160160
expect(startExtra).toContain("socat TCP-LISTEN:9223")
161161
expect(browserRuntime).toContain('--network "container:$main_container"')
162+
expect(browserRuntime).toContain("docker_git_cleanup_orphaned_playwright_browsers")
163+
expect(browserRuntime).toContain('--filter "label=docker-git.browser=1" --filter "label=docker-git.project-container"')
164+
expect(browserRuntime).toContain('docker inspect --format \'{{ .State.Running }}\' "$project_container"')
162165
expect(browserRuntime).toContain('args+=(--cpus "$DOCKER_GIT_BROWSER_CPU_LIMIT")')
163166
expect(browserRuntime).toContain('args+=(--memory "$DOCKER_GIT_BROWSER_RAM_LIMIT" --memory-swap "$DOCKER_GIT_BROWSER_RAM_LIMIT")')
164167

packages/lib/tests/usecases/prepare-files.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,31 @@ describe("prepareProjectFiles", () => {
306306
})
307307
).pipe(Effect.provide(NodeContext.layer)))
308308

309+
it.effect("omits local Docker socket mount when project Docker host is injected", () =>
310+
withTempDir((root) =>
311+
withPatchedEnv(
312+
{ DOCKER_GIT_PROJECT_DOCKER_HOST: "tcp://docker-controller:2375" },
313+
Effect.gen(function*(_) {
314+
const fs = yield* _(FileSystem.FileSystem)
315+
const path = yield* _(Path.Path)
316+
const outDir = path.join(root, "project-docker-host")
317+
const globalConfig = makeGlobalConfig(root, path)
318+
const projectConfig = makeProjectConfig(outDir, true, path)
319+
320+
yield* _(
321+
prepareProjectFiles(outDir, root, globalConfig, projectConfig, {
322+
force: false,
323+
forceEnv: false
324+
})
325+
)
326+
327+
const compose = yield* _(fs.readFileString(path.join(outDir, "docker-compose.yml")))
328+
expect(compose).toContain('DOCKER_GIT_PROJECT_DOCKER_HOST: "${DOCKER_GIT_PROJECT_DOCKER_HOST:-}"')
329+
expect(compose).not.toContain(" - /var/run/docker.sock:/var/run/docker.sock")
330+
})
331+
)
332+
).pipe(Effect.provide(NodeContext.layer)))
333+
309334
it.effect("renders project-scoped network when dockerNetworkMode=project", () =>
310335
withTempDir((root) =>
311336
Effect.gen(function*(_) {

0 commit comments

Comments
 (0)