Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/playwright-resource-limits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@prover-coder-ai/docker-git": minor
"@effect-template/lib": minor
---

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.
4 changes: 4 additions & 0 deletions packages/app/src/docker-git/cli/parser-apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const parseApply = (
const { projectDir, raw } = yield* _(parseProjectDirWithOptions(args))
const cpuLimit = yield* _(normalizeCpuLimit(raw.cpuLimit, "--cpu"))
const ramLimit = yield* _(normalizeRamLimit(raw.ramLimit, "--ram"))
const playwrightCpuLimit = yield* _(normalizeCpuLimit(raw.playwrightCpuLimit, "--playwright-cpu"))
const playwrightRamLimit = yield* _(normalizeRamLimit(raw.playwrightRamLimit, "--playwright-ram"))
return {
_tag: "Apply",
projectDir,
Expand All @@ -31,6 +33,8 @@ export const parseApply = (
claudeTokenLabel: raw.claudeTokenLabel,
cpuLimit,
ramLimit,
playwrightCpuLimit,
playwrightRamLimit,
enableMcpPlaywright: raw.enableMcpPlaywright
}
})
8 changes: 8 additions & 0 deletions packages/app/src/docker-git/cli/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ interface ValueOptionSpec {
| "codexHome"
| "cpuLimit"
| "ramLimit"
| "playwrightCpuLimit"
| "playwrightRamLimit"
| "dockerNetworkMode"
| "dockerSharedNetworkName"
| "archivePath"
Expand Down Expand Up @@ -60,6 +62,10 @@ const valueOptionSpecs: ReadonlyArray<ValueOptionSpec> = [
{ flag: "--cpus", key: "cpuLimit" },
{ flag: "--ram", key: "ramLimit" },
{ flag: "--memory", key: "ramLimit" },
{ flag: "--playwright-cpu", key: "playwrightCpuLimit" },
{ flag: "--playwright-cpus", key: "playwrightCpuLimit" },
{ flag: "--playwright-ram", key: "playwrightRamLimit" },
{ flag: "--playwright-memory", key: "playwrightRamLimit" },
{ flag: "--network-mode", key: "dockerNetworkMode" },
{ flag: "--shared-network", key: "dockerSharedNetworkName" },
{ flag: "--archive", key: "archivePath" },
Expand Down Expand Up @@ -118,6 +124,8 @@ const valueFlagUpdaters: { readonly [K in ValueKey]: (raw: RawOptions, value: st
codexHome: (raw, value) => ({ ...raw, codexHome: value }),
cpuLimit: (raw, value) => ({ ...raw, cpuLimit: value }),
ramLimit: (raw, value) => ({ ...raw, ramLimit: value }),
playwrightCpuLimit: (raw, value) => ({ ...raw, playwrightCpuLimit: value }),
playwrightRamLimit: (raw, value) => ({ ...raw, playwrightRamLimit: value }),
dockerNetworkMode: (raw, value) => ({ ...raw, dockerNetworkMode: value }),
dockerSharedNetworkName: (raw, value) => ({ ...raw, dockerSharedNetworkName: value }),
archivePath: (raw, value) => ({ ...raw, archivePath: value }),
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/docker-git/cli/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Options:
--codex-home <path> Container path for Codex auth (default: /home/dev/.codex)
--cpu <value> CPU limit: percent or cores (examples: 30%, 1.5; default: 30%)
--ram <value> RAM limit: percent or size (examples: 30%, 512m, 4g; default: 30%)
--playwright-cpu <value> CPU limit for the MCP Playwright browser sidecar (default: 30% or --cpu when set)
--playwright-ram <value> RAM limit for the MCP Playwright browser sidecar (default: 30% or --ram when set)
--network-mode <mode> Compose network mode: shared|project (default: shared)
--shared-network <name> Shared Docker network name when network-mode=shared (default: docker-git-shared)
--out-dir <path> Output directory (default: <projectsRoot>/<org>/<repo>[/issue-<id>|/pr-<id>])
Expand Down
118 changes: 65 additions & 53 deletions packages/app/src/docker-git/frontend-lib/core/command-builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ import { type RawOptions } from "./command-options.js"
import {
type AgentMode,
type CreateCommand,
defaultCpuLimit,
defaultRamLimit,
defaultTemplateConfig,
deriveRepoPathParts,
deriveRepoSlug,
type ParseError,
resolveRepoInput
} from "./domain.js"
import { normalizeCpuLimit, normalizeRamLimit } from "./resource-limits.js"
import { resolveResourceLimitsIntent } from "./resource-limits.js"
import { trimRightChar } from "./strings.js"
import { normalizeAuthLabel, normalizeGitTokenLabel } from "./token-labels.js"

Expand Down Expand Up @@ -193,6 +191,8 @@ type BuildTemplateConfigInput = {
readonly paths: PathConfig
readonly cpuLimit: string | undefined
readonly ramLimit: string | undefined
readonly playwrightCpuLimit: string | undefined
readonly playwrightRamLimit: string | undefined
readonly dockerNetworkMode: CreateCommand["config"]["dockerNetworkMode"]
readonly dockerSharedNetworkName: string
readonly gitTokenLabel: string | undefined
Expand All @@ -205,53 +205,64 @@ type BuildTemplateConfigInput = {
readonly clonedOnHostname?: string | undefined
}

const buildTemplateConfig = ({
agentAuto,
agentMode,
claudeAuthLabel,
clonedOnHostname,
codexAuthLabel,
cpuLimit,
dockerNetworkMode,
dockerSharedNetworkName,
enableMcpPlaywright,
gitTokenLabel,
names,
paths,
ramLimit,
repo,
skipGithubAuth
}: BuildTemplateConfigInput): CreateCommand["config"] => ({
containerName: names.containerName,
serviceName: names.serviceName,
sshUser: repo.sshUser,
sshPort: repo.sshPort,
repoUrl: repo.repoUrl,
repoRef: repo.repoRef,
gitTokenLabel,
skipGithubAuth,
codexAuthLabel,
claudeAuthLabel,
targetDir: repo.targetDir,
volumeName: names.volumeName,
dockerGitPath: paths.dockerGitPath,
authorizedKeysPath: paths.authorizedKeysPath,
envGlobalPath: paths.envGlobalPath,
envProjectPath: paths.envProjectPath,
codexAuthPath: paths.codexAuthPath,
codexSharedAuthPath: paths.codexSharedAuthPath,
codexHome: paths.codexHome,
geminiAuthPath: paths.geminiAuthPath,
geminiHome: paths.geminiHome,
cpuLimit,
ramLimit,
dockerNetworkMode,
dockerSharedNetworkName,
enableMcpPlaywright,
const buildTemplateConfigBase = (
input: Pick<BuildTemplateConfigInput, "repo" | "names" | "paths">
): Pick<
CreateCommand["config"],
| "containerName"
| "serviceName"
| "sshUser"
| "sshPort"
| "repoUrl"
| "repoRef"
| "targetDir"
| "volumeName"
| "dockerGitPath"
| "authorizedKeysPath"
| "envGlobalPath"
| "envProjectPath"
| "codexAuthPath"
| "codexSharedAuthPath"
| "codexHome"
| "geminiAuthPath"
| "geminiHome"
> => ({
containerName: input.names.containerName,
serviceName: input.names.serviceName,
sshUser: input.repo.sshUser,
sshPort: input.repo.sshPort,
repoUrl: input.repo.repoUrl,
repoRef: input.repo.repoRef,
targetDir: input.repo.targetDir,
volumeName: input.names.volumeName,
dockerGitPath: input.paths.dockerGitPath,
authorizedKeysPath: input.paths.authorizedKeysPath,
envGlobalPath: input.paths.envGlobalPath,
envProjectPath: input.paths.envProjectPath,
codexAuthPath: input.paths.codexAuthPath,
codexSharedAuthPath: input.paths.codexSharedAuthPath,
codexHome: input.paths.codexHome,
geminiAuthPath: input.paths.geminiAuthPath,
geminiHome: input.paths.geminiHome
})

const buildTemplateConfig = (input: BuildTemplateConfigInput): CreateCommand["config"] => ({
...buildTemplateConfigBase(input),
gitTokenLabel: input.gitTokenLabel,
skipGithubAuth: input.skipGithubAuth,
codexAuthLabel: input.codexAuthLabel,
claudeAuthLabel: input.claudeAuthLabel,
cpuLimit: input.cpuLimit,
ramLimit: input.ramLimit,
playwrightCpuLimit: input.playwrightCpuLimit,
playwrightRamLimit: input.playwrightRamLimit,
dockerNetworkMode: input.dockerNetworkMode,
dockerSharedNetworkName: input.dockerSharedNetworkName,
enableMcpPlaywright: input.enableMcpPlaywright,
bunVersion: defaultTemplateConfig.bunVersion,
agentMode,
agentAuto,
clonedOnHostname
agentMode: input.agentMode,
agentAuto: input.agentAuto,
clonedOnHostname: input.clonedOnHostname
})

// CHANGE: build a typed create command from raw options (CLI or API)
Expand All @@ -275,8 +286,7 @@ export const buildCreateCommand = (
const gitTokenLabel = normalizeGitTokenLabel(raw.gitTokenLabel)
const codexAuthLabel = normalizeAuthLabel(raw.codexTokenLabel)
const claudeAuthLabel = normalizeAuthLabel(raw.claudeTokenLabel)
const cpuLimit = yield* _(normalizeCpuLimit(raw.cpuLimit ?? defaultCpuLimit, "--cpu"))
const ramLimit = yield* _(normalizeRamLimit(raw.ramLimit ?? defaultRamLimit, "--ram"))
const limits = yield* _(resolveResourceLimitsIntent(raw))
const dockerNetworkMode = yield* _(parseDockerNetworkMode(raw.dockerNetworkMode))
const dockerSharedNetworkName = yield* _(
nonEmpty("--shared-network", raw.dockerSharedNetworkName, defaultTemplateConfig.dockerSharedNetworkName)
Expand All @@ -295,8 +305,10 @@ export const buildCreateCommand = (
repo,
names,
paths,
cpuLimit,
ramLimit,
cpuLimit: limits.cpuLimit,
ramLimit: limits.ramLimit,
playwrightCpuLimit: limits.playwrightCpuLimit,
playwrightRamLimit: limits.playwrightRamLimit,
dockerNetworkMode,
dockerSharedNetworkName,
gitTokenLabel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface RawOptions {
readonly codexHome?: string
readonly cpuLimit?: string
readonly ramLimit?: string
readonly playwrightCpuLimit?: string
readonly playwrightRamLimit?: string
readonly dockerNetworkMode?: string
readonly dockerSharedNetworkName?: string
readonly enableMcpPlaywright?: boolean
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/docker-git/frontend-lib/core/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export {
defaultCpuLimit,
defaultDockerNetworkMode,
defaultDockerSharedNetworkName,
defaultPlaywrightCpuLimit,
defaultPlaywrightRamLimit,
defaultRamLimit,
defaultTemplateConfig,
dockerGitSharedCacheVolumeName,
Expand Down Expand Up @@ -78,6 +80,8 @@ export interface TemplateConfig {
readonly geminiHome: string
readonly cpuLimit?: string | undefined
readonly ramLimit?: string | undefined
readonly playwrightCpuLimit?: string | undefined
readonly playwrightRamLimit?: string | undefined
readonly dockerNetworkMode: DockerNetworkMode
readonly dockerSharedNetworkName: string
readonly enableMcpPlaywright: boolean
Expand Down Expand Up @@ -170,6 +174,8 @@ export interface ApplyCommand {
readonly geminiTokenLabel?: string | undefined
readonly cpuLimit?: string | undefined
readonly ramLimit?: string | undefined
readonly playwrightCpuLimit?: string | undefined
readonly playwrightRamLimit?: string | undefined
readonly enableMcpPlaywright?: boolean | undefined
}

Expand Down
48 changes: 46 additions & 2 deletions packages/app/src/docker-git/frontend-lib/core/resource-limits.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
/* jscpd:ignore-start */
import { Either } from "effect"

import { defaultCpuLimit, defaultRamLimit, type ParseError, type TemplateConfig } from "./domain.js"
import { type RawOptions } from "./command-options.js"
import {
defaultCpuLimit,
defaultPlaywrightCpuLimit,
defaultPlaywrightRamLimit,
defaultRamLimit,
type ParseError,
type TemplateConfig
} from "./domain.js"

const mebibyte = 1024 ** 2
const minimumResolvedCpuLimit = 0.25
Expand Down Expand Up @@ -109,7 +117,9 @@ export const withDefaultResourceLimitIntent = (
): TemplateConfig => ({
...template,
cpuLimit: template.cpuLimit ?? defaultCpuLimit,
ramLimit: template.ramLimit ?? defaultRamLimit
ramLimit: template.ramLimit ?? defaultRamLimit,
playwrightCpuLimit: template.playwrightCpuLimit ?? defaultPlaywrightCpuLimit,
playwrightRamLimit: template.playwrightRamLimit ?? defaultPlaywrightRamLimit
})

const resolvePercentCpuLimit = (percent: number, cpuCount: number): number =>
Expand Down Expand Up @@ -142,4 +152,38 @@ export const resolveComposeResourceLimits = (
: resolvePercentRamLimit(ramPercent, hostResources.totalMemoryBytes)
}
}

export const resolvePlaywrightComposeResourceLimits = (
template: Pick<TemplateConfig, "playwrightCpuLimit" | "playwrightRamLimit" | "cpuLimit" | "ramLimit">,
hostResources: HostResources
): ResolvedComposeResourceLimits =>
resolveComposeResourceLimits(
{
cpuLimit: template.playwrightCpuLimit ?? template.cpuLimit ?? defaultPlaywrightCpuLimit,
ramLimit: template.playwrightRamLimit ?? template.ramLimit ?? defaultPlaywrightRamLimit
},
hostResources
)

export type ResolvedResourceLimitsIntent = {
readonly cpuLimit: string | undefined
readonly ramLimit: string | undefined
readonly playwrightCpuLimit: string | undefined
readonly playwrightRamLimit: string | undefined
}

export const resolveResourceLimitsIntent = (
raw: RawOptions
): Either.Either<ResolvedResourceLimitsIntent, ParseError> =>
Either.gen(function*(_) {
const cpuLimit = yield* _(normalizeCpuLimit(raw.cpuLimit ?? defaultCpuLimit, "--cpu"))
const ramLimit = yield* _(normalizeRamLimit(raw.ramLimit ?? defaultRamLimit, "--ram"))
const playwrightCpuLimit = yield* _(
normalizeCpuLimit(raw.playwrightCpuLimit ?? cpuLimit, "--playwright-cpu")
)
const playwrightRamLimit = yield* _(
normalizeRamLimit(raw.playwrightRamLimit ?? ramLimit, "--playwright-ram")
)
return { cpuLimit, ramLimit, playwrightCpuLimit, playwrightRamLimit }
})
/* jscpd:ignore-end */
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type DefaultTemplateConfig = Pick<
| "geminiHome"
| "cpuLimit"
| "ramLimit"
| "playwrightCpuLimit"
| "playwrightRamLimit"
| "dockerNetworkMode"
| "dockerSharedNetworkName"
| "enableMcpPlaywright"
Expand All @@ -38,6 +40,10 @@ export const defaultCpuLimit = "30%"

export const defaultRamLimit = "30%"

export const defaultPlaywrightCpuLimit = "30%"

export const defaultPlaywrightRamLimit = "30%"

export const defaultTemplateConfig = {
containerName: "dev-ssh",
serviceName: "dev",
Expand All @@ -58,6 +64,8 @@ export const defaultTemplateConfig = {
geminiHome: "/home/dev/.gemini",
cpuLimit: defaultCpuLimit,
ramLimit: defaultRamLimit,
playwrightCpuLimit: defaultPlaywrightCpuLimit,
playwrightRamLimit: defaultPlaywrightRamLimit,
dockerNetworkMode: defaultDockerNetworkMode,
dockerSharedNetworkName: defaultDockerSharedNetworkName,
enableMcpPlaywright: false,
Expand Down
Loading