Skip to content

Commit ed78338

Browse files
authored
Merge pull request #264 from konard/issue-259-9a9eea9aba5c
feat(docker-git): add resource limits for MCP Playwright sidecar
2 parents 5962fd6 + f9f01a5 commit ed78338

30 files changed

Lines changed: 723 additions & 257 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@prover-coder-ai/docker-git": minor
3+
"@effect-template/lib": minor
4+
---
5+
6+
Add configurable CPU and RAM limits for the MCP Playwright sidecar container, separate from the main service container. Exposed via `--playwright-cpu`/`--playwright-cpus` and `--playwright-ram`/`--playwright-memory` CLI flags. Defaults to 30% of host resources, falling back to the main service limits when not set.

packages/app/src/docker-git/cli/parser-apply.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export const parseApply = (
2222
const { projectDir, raw } = yield* _(parseProjectDirWithOptions(args))
2323
const cpuLimit = yield* _(normalizeCpuLimit(raw.cpuLimit, "--cpu"))
2424
const ramLimit = yield* _(normalizeRamLimit(raw.ramLimit, "--ram"))
25+
const playwrightCpuLimit = yield* _(normalizeCpuLimit(raw.playwrightCpuLimit, "--playwright-cpu"))
26+
const playwrightRamLimit = yield* _(normalizeRamLimit(raw.playwrightRamLimit, "--playwright-ram"))
2527
return {
2628
_tag: "Apply",
2729
projectDir,
@@ -31,6 +33,8 @@ export const parseApply = (
3133
claudeTokenLabel: raw.claudeTokenLabel,
3234
cpuLimit,
3335
ramLimit,
36+
playwrightCpuLimit,
37+
playwrightRamLimit,
3438
enableMcpPlaywright: raw.enableMcpPlaywright
3539
}
3640
})

packages/app/src/docker-git/cli/parser-options.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ interface ValueOptionSpec {
2222
| "codexHome"
2323
| "cpuLimit"
2424
| "ramLimit"
25+
| "playwrightCpuLimit"
26+
| "playwrightRamLimit"
2527
| "dockerNetworkMode"
2628
| "dockerSharedNetworkName"
2729
| "archivePath"
@@ -60,6 +62,10 @@ const valueOptionSpecs: ReadonlyArray<ValueOptionSpec> = [
6062
{ flag: "--cpus", key: "cpuLimit" },
6163
{ flag: "--ram", key: "ramLimit" },
6264
{ flag: "--memory", key: "ramLimit" },
65+
{ flag: "--playwright-cpu", key: "playwrightCpuLimit" },
66+
{ flag: "--playwright-cpus", key: "playwrightCpuLimit" },
67+
{ flag: "--playwright-ram", key: "playwrightRamLimit" },
68+
{ flag: "--playwright-memory", key: "playwrightRamLimit" },
6369
{ flag: "--network-mode", key: "dockerNetworkMode" },
6470
{ flag: "--shared-network", key: "dockerSharedNetworkName" },
6571
{ flag: "--archive", key: "archivePath" },
@@ -118,6 +124,8 @@ const valueFlagUpdaters: { readonly [K in ValueKey]: (raw: RawOptions, value: st
118124
codexHome: (raw, value) => ({ ...raw, codexHome: value }),
119125
cpuLimit: (raw, value) => ({ ...raw, cpuLimit: value }),
120126
ramLimit: (raw, value) => ({ ...raw, ramLimit: value }),
127+
playwrightCpuLimit: (raw, value) => ({ ...raw, playwrightCpuLimit: value }),
128+
playwrightRamLimit: (raw, value) => ({ ...raw, playwrightRamLimit: value }),
121129
dockerNetworkMode: (raw, value) => ({ ...raw, dockerNetworkMode: value }),
122130
dockerSharedNetworkName: (raw, value) => ({ ...raw, dockerSharedNetworkName: value }),
123131
archivePath: (raw, value) => ({ ...raw, archivePath: value }),

packages/app/src/docker-git/cli/usage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ Options:
5454
--codex-home <path> Container path for Codex auth (default: /home/dev/.codex)
5555
--cpu <value> CPU limit: percent or cores (examples: 30%, 1.5; default: 30%)
5656
--ram <value> RAM limit: percent or size (examples: 30%, 512m, 4g; default: 30%)
57+
--playwright-cpu <value> CPU limit for the MCP Playwright browser sidecar (default: 30% or --cpu when set)
58+
--playwright-ram <value> RAM limit for the MCP Playwright browser sidecar (default: 30% or --ram when set)
5759
--network-mode <mode> Compose network mode: shared|project (default: shared)
5860
--shared-network <name> Shared Docker network name when network-mode=shared (default: docker-git-shared)
5961
--out-dir <path> Output directory (default: <projectsRoot>/<org>/<repo>[/issue-<id>|/pr-<id>])

packages/app/src/docker-git/frontend-lib/core/command-builders.ts

Lines changed: 65 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@ import { type RawOptions } from "./command-options.js"
88
import {
99
type AgentMode,
1010
type CreateCommand,
11-
defaultCpuLimit,
12-
defaultRamLimit,
1311
defaultTemplateConfig,
1412
deriveRepoPathParts,
1513
deriveRepoSlug,
1614
type ParseError,
1715
resolveRepoInput
1816
} from "./domain.js"
19-
import { normalizeCpuLimit, normalizeRamLimit } from "./resource-limits.js"
17+
import { resolveResourceLimitsIntent } from "./resource-limits.js"
2018
import { trimRightChar } from "./strings.js"
2119
import { normalizeAuthLabel, normalizeGitTokenLabel } from "./token-labels.js"
2220

@@ -193,6 +191,8 @@ type BuildTemplateConfigInput = {
193191
readonly paths: PathConfig
194192
readonly cpuLimit: string | undefined
195193
readonly ramLimit: string | undefined
194+
readonly playwrightCpuLimit: string | undefined
195+
readonly playwrightRamLimit: string | undefined
196196
readonly dockerNetworkMode: CreateCommand["config"]["dockerNetworkMode"]
197197
readonly dockerSharedNetworkName: string
198198
readonly gitTokenLabel: string | undefined
@@ -205,53 +205,64 @@ type BuildTemplateConfigInput = {
205205
readonly clonedOnHostname?: string | undefined
206206
}
207207

208-
const buildTemplateConfig = ({
209-
agentAuto,
210-
agentMode,
211-
claudeAuthLabel,
212-
clonedOnHostname,
213-
codexAuthLabel,
214-
cpuLimit,
215-
dockerNetworkMode,
216-
dockerSharedNetworkName,
217-
enableMcpPlaywright,
218-
gitTokenLabel,
219-
names,
220-
paths,
221-
ramLimit,
222-
repo,
223-
skipGithubAuth
224-
}: BuildTemplateConfigInput): CreateCommand["config"] => ({
225-
containerName: names.containerName,
226-
serviceName: names.serviceName,
227-
sshUser: repo.sshUser,
228-
sshPort: repo.sshPort,
229-
repoUrl: repo.repoUrl,
230-
repoRef: repo.repoRef,
231-
gitTokenLabel,
232-
skipGithubAuth,
233-
codexAuthLabel,
234-
claudeAuthLabel,
235-
targetDir: repo.targetDir,
236-
volumeName: names.volumeName,
237-
dockerGitPath: paths.dockerGitPath,
238-
authorizedKeysPath: paths.authorizedKeysPath,
239-
envGlobalPath: paths.envGlobalPath,
240-
envProjectPath: paths.envProjectPath,
241-
codexAuthPath: paths.codexAuthPath,
242-
codexSharedAuthPath: paths.codexSharedAuthPath,
243-
codexHome: paths.codexHome,
244-
geminiAuthPath: paths.geminiAuthPath,
245-
geminiHome: paths.geminiHome,
246-
cpuLimit,
247-
ramLimit,
248-
dockerNetworkMode,
249-
dockerSharedNetworkName,
250-
enableMcpPlaywright,
208+
const buildTemplateConfigBase = (
209+
input: Pick<BuildTemplateConfigInput, "repo" | "names" | "paths">
210+
): Pick<
211+
CreateCommand["config"],
212+
| "containerName"
213+
| "serviceName"
214+
| "sshUser"
215+
| "sshPort"
216+
| "repoUrl"
217+
| "repoRef"
218+
| "targetDir"
219+
| "volumeName"
220+
| "dockerGitPath"
221+
| "authorizedKeysPath"
222+
| "envGlobalPath"
223+
| "envProjectPath"
224+
| "codexAuthPath"
225+
| "codexSharedAuthPath"
226+
| "codexHome"
227+
| "geminiAuthPath"
228+
| "geminiHome"
229+
> => ({
230+
containerName: input.names.containerName,
231+
serviceName: input.names.serviceName,
232+
sshUser: input.repo.sshUser,
233+
sshPort: input.repo.sshPort,
234+
repoUrl: input.repo.repoUrl,
235+
repoRef: input.repo.repoRef,
236+
targetDir: input.repo.targetDir,
237+
volumeName: input.names.volumeName,
238+
dockerGitPath: input.paths.dockerGitPath,
239+
authorizedKeysPath: input.paths.authorizedKeysPath,
240+
envGlobalPath: input.paths.envGlobalPath,
241+
envProjectPath: input.paths.envProjectPath,
242+
codexAuthPath: input.paths.codexAuthPath,
243+
codexSharedAuthPath: input.paths.codexSharedAuthPath,
244+
codexHome: input.paths.codexHome,
245+
geminiAuthPath: input.paths.geminiAuthPath,
246+
geminiHome: input.paths.geminiHome
247+
})
248+
249+
const buildTemplateConfig = (input: BuildTemplateConfigInput): CreateCommand["config"] => ({
250+
...buildTemplateConfigBase(input),
251+
gitTokenLabel: input.gitTokenLabel,
252+
skipGithubAuth: input.skipGithubAuth,
253+
codexAuthLabel: input.codexAuthLabel,
254+
claudeAuthLabel: input.claudeAuthLabel,
255+
cpuLimit: input.cpuLimit,
256+
ramLimit: input.ramLimit,
257+
playwrightCpuLimit: input.playwrightCpuLimit,
258+
playwrightRamLimit: input.playwrightRamLimit,
259+
dockerNetworkMode: input.dockerNetworkMode,
260+
dockerSharedNetworkName: input.dockerSharedNetworkName,
261+
enableMcpPlaywright: input.enableMcpPlaywright,
251262
bunVersion: defaultTemplateConfig.bunVersion,
252-
agentMode,
253-
agentAuto,
254-
clonedOnHostname
263+
agentMode: input.agentMode,
264+
agentAuto: input.agentAuto,
265+
clonedOnHostname: input.clonedOnHostname
255266
})
256267

257268
// CHANGE: build a typed create command from raw options (CLI or API)
@@ -275,8 +286,7 @@ export const buildCreateCommand = (
275286
const gitTokenLabel = normalizeGitTokenLabel(raw.gitTokenLabel)
276287
const codexAuthLabel = normalizeAuthLabel(raw.codexTokenLabel)
277288
const claudeAuthLabel = normalizeAuthLabel(raw.claudeTokenLabel)
278-
const cpuLimit = yield* _(normalizeCpuLimit(raw.cpuLimit ?? defaultCpuLimit, "--cpu"))
279-
const ramLimit = yield* _(normalizeRamLimit(raw.ramLimit ?? defaultRamLimit, "--ram"))
289+
const limits = yield* _(resolveResourceLimitsIntent(raw))
280290
const dockerNetworkMode = yield* _(parseDockerNetworkMode(raw.dockerNetworkMode))
281291
const dockerSharedNetworkName = yield* _(
282292
nonEmpty("--shared-network", raw.dockerSharedNetworkName, defaultTemplateConfig.dockerSharedNetworkName)
@@ -295,8 +305,10 @@ export const buildCreateCommand = (
295305
repo,
296306
names,
297307
paths,
298-
cpuLimit,
299-
ramLimit,
308+
cpuLimit: limits.cpuLimit,
309+
ramLimit: limits.ramLimit,
310+
playwrightCpuLimit: limits.playwrightCpuLimit,
311+
playwrightRamLimit: limits.playwrightRamLimit,
300312
dockerNetworkMode,
301313
dockerSharedNetworkName,
302314
gitTokenLabel,

packages/app/src/docker-git/frontend-lib/core/command-options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface RawOptions {
2828
readonly codexHome?: string
2929
readonly cpuLimit?: string
3030
readonly ramLimit?: string
31+
readonly playwrightCpuLimit?: string
32+
readonly playwrightRamLimit?: string
3133
readonly dockerNetworkMode?: string
3234
readonly dockerSharedNetworkName?: string
3335
readonly enableMcpPlaywright?: boolean

packages/app/src/docker-git/frontend-lib/core/domain.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export {
4545
defaultCpuLimit,
4646
defaultDockerNetworkMode,
4747
defaultDockerSharedNetworkName,
48+
defaultPlaywrightCpuLimit,
49+
defaultPlaywrightRamLimit,
4850
defaultRamLimit,
4951
defaultTemplateConfig,
5052
dockerGitSharedCacheVolumeName,
@@ -81,6 +83,8 @@ export interface TemplateConfig {
8183
readonly geminiHome: string
8284
readonly cpuLimit?: string | undefined
8385
readonly ramLimit?: string | undefined
86+
readonly playwrightCpuLimit?: string | undefined
87+
readonly playwrightRamLimit?: string | undefined
8488
readonly dockerNetworkMode: DockerNetworkMode
8589
readonly dockerSharedNetworkName: string
8690
readonly enableMcpPlaywright: boolean
@@ -173,6 +177,8 @@ export interface ApplyCommand {
173177
readonly geminiTokenLabel?: string | undefined
174178
readonly cpuLimit?: string | undefined
175179
readonly ramLimit?: string | undefined
180+
readonly playwrightCpuLimit?: string | undefined
181+
readonly playwrightRamLimit?: string | undefined
176182
readonly enableMcpPlaywright?: boolean | undefined
177183
}
178184

packages/app/src/docker-git/frontend-lib/core/resource-limits.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
/* jscpd:ignore-start */
22
import { Either } from "effect"
33

4-
import { defaultCpuLimit, defaultRamLimit, type ParseError, type TemplateConfig } from "./domain.js"
4+
import { type RawOptions } from "./command-options.js"
5+
import {
6+
defaultCpuLimit,
7+
defaultPlaywrightCpuLimit,
8+
defaultPlaywrightRamLimit,
9+
defaultRamLimit,
10+
type ParseError,
11+
type TemplateConfig
12+
} from "./domain.js"
513

614
const mebibyte = 1024 ** 2
715
const minimumResolvedCpuLimit = 0.25
@@ -109,7 +117,9 @@ export const withDefaultResourceLimitIntent = (
109117
): TemplateConfig => ({
110118
...template,
111119
cpuLimit: template.cpuLimit ?? defaultCpuLimit,
112-
ramLimit: template.ramLimit ?? defaultRamLimit
120+
ramLimit: template.ramLimit ?? defaultRamLimit,
121+
playwrightCpuLimit: template.playwrightCpuLimit ?? defaultPlaywrightCpuLimit,
122+
playwrightRamLimit: template.playwrightRamLimit ?? defaultPlaywrightRamLimit
113123
})
114124

115125
const resolvePercentCpuLimit = (percent: number, cpuCount: number): number =>
@@ -142,4 +152,38 @@ export const resolveComposeResourceLimits = (
142152
: resolvePercentRamLimit(ramPercent, hostResources.totalMemoryBytes)
143153
}
144154
}
155+
156+
export const resolvePlaywrightComposeResourceLimits = (
157+
template: Pick<TemplateConfig, "playwrightCpuLimit" | "playwrightRamLimit" | "cpuLimit" | "ramLimit">,
158+
hostResources: HostResources
159+
): ResolvedComposeResourceLimits =>
160+
resolveComposeResourceLimits(
161+
{
162+
cpuLimit: template.playwrightCpuLimit ?? template.cpuLimit ?? defaultPlaywrightCpuLimit,
163+
ramLimit: template.playwrightRamLimit ?? template.ramLimit ?? defaultPlaywrightRamLimit
164+
},
165+
hostResources
166+
)
167+
168+
export type ResolvedResourceLimitsIntent = {
169+
readonly cpuLimit: string | undefined
170+
readonly ramLimit: string | undefined
171+
readonly playwrightCpuLimit: string | undefined
172+
readonly playwrightRamLimit: string | undefined
173+
}
174+
175+
export const resolveResourceLimitsIntent = (
176+
raw: RawOptions
177+
): Either.Either<ResolvedResourceLimitsIntent, ParseError> =>
178+
Either.gen(function*(_) {
179+
const cpuLimit = yield* _(normalizeCpuLimit(raw.cpuLimit ?? defaultCpuLimit, "--cpu"))
180+
const ramLimit = yield* _(normalizeRamLimit(raw.ramLimit ?? defaultRamLimit, "--ram"))
181+
const playwrightCpuLimit = yield* _(
182+
normalizeCpuLimit(raw.playwrightCpuLimit ?? cpuLimit, "--playwright-cpu")
183+
)
184+
const playwrightRamLimit = yield* _(
185+
normalizeRamLimit(raw.playwrightRamLimit ?? ramLimit, "--playwright-ram")
186+
)
187+
return { cpuLimit, ramLimit, playwrightCpuLimit, playwrightRamLimit }
188+
})
145189
/* jscpd:ignore-end */

packages/app/src/docker-git/frontend-lib/core/template-defaults.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type DefaultTemplateConfig = Pick<
2222
| "geminiHome"
2323
| "cpuLimit"
2424
| "ramLimit"
25+
| "playwrightCpuLimit"
26+
| "playwrightRamLimit"
2527
| "dockerNetworkMode"
2628
| "dockerSharedNetworkName"
2729
| "enableMcpPlaywright"
@@ -38,6 +40,10 @@ export const defaultCpuLimit = "30%"
3840

3941
export const defaultRamLimit = "30%"
4042

43+
export const defaultPlaywrightCpuLimit = "30%"
44+
45+
export const defaultPlaywrightRamLimit = "30%"
46+
4147
export const defaultTemplateConfig = {
4248
containerName: "dev-ssh",
4349
serviceName: "dev",
@@ -58,6 +64,8 @@ export const defaultTemplateConfig = {
5864
geminiHome: "/home/dev/.gemini",
5965
cpuLimit: defaultCpuLimit,
6066
ramLimit: defaultRamLimit,
67+
playwrightCpuLimit: defaultPlaywrightCpuLimit,
68+
playwrightRamLimit: defaultPlaywrightRamLimit,
6169
dockerNetworkMode: defaultDockerNetworkMode,
6270
dockerSharedNetworkName: defaultDockerSharedNetworkName,
6371
enableMcpPlaywright: false,

0 commit comments

Comments
 (0)