Public: anyone can read this; the crew can change it.

A machine's memory and CPU ceilings are declared in forge.json and enforced on the crew run #9479

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A machine in `forge.json` can now set `memory` (whole MiB, 256–65536) and `cpus` (cores, 0.25–64), and the crew run is held to them by the kernel. Example: `"machines": { "audit": { "memory": 2048, "cpus": 2 } }`.

shipped production is serving it deploy f21b126

opened this

A machine in forge.json can now set memory (whole MiB, 256–65536) and cpus (cores, 0.25–64), and the crew run is held to them by the kernel. Example: "machines": { "audit": { "memory": 2048, "cpus": 2 } }.

  • Parsing (src/crew/machines.ts): a value out of bounds or of the wrong type rejects the declaration. As with any invalid declaration, the crew then runs in standard. machineLimits(machine) returns the ceilings; standard and security return none.
  • Enforcement (new src/crew/run-cell.ts): crew runs already got a cgroup cell from runClaude, with no memory cap and FORGE_CREW_CPUS cores. openRunCell now opens that cell with the machine's memory cap and the lower of the machine's cpus and FORGE_CREW_CPUS. A machine can only narrow the cell, never widen it. With no ceilings declared, the cell is exactly today's: no memory cap and best-effort entry.
  • Strict entry with a memory cap: the run uses enterArgv, so it refuses to start (exit 125) rather than run uncapped in a cell made for it. Look at this choice: a declared machine whose cgroup can be made but not entered will fail its run instead of running loose.
  • Run log: the log says machine: held to memory 2048 MiB and 2 CPUs when the host holds the ceilings. Otherwise it says one line such as machine: memory 512 MiB not applied: <reason>, covering no delegated cgroup, a cgroup that could not be made, or no cpu controller (detected by reading cpu.max back). The runner passes the limits in from src/crew/runner.ts.
  • Tests (test/crew-machines.test.ts): bounds and types; standard has no ceilings; cores never go above the forge's own; on a fake cgroup root, memory.max and cpu.max are written and the spawn argv enters cgroup.procs strictly; the "not applied" line is logged; nothing is logged for standard.
  • Docs: docs/features/machines.md says how to set the ceilings and what the log lines mean.

The issue named src/crew/sandbox.ts, but that file only builds Claude Code's sandbox settings. The process is started in src/crew/claude.ts, so the limits go into the cgroup cell that already exists there rather than a new bubblewrap layer. Disk ceilings are not done.

To try it: the gate, bun x tsc --noEmit, and the 17 test files that touch runClaude, machines or the run's cgroup all pass, plus test/cgroup.test.ts (15 skipped: the unix-socket and real-cgroup suites this sandbox can't run). scripts/fast.ts named 429 files (about 38 minutes), so the full suite was not run here.

The keep_plan tool the issue asks for was not available in this run, so the issue has no plan posted.

🤖 Generated with Claude Code

Closes #9469

commented

A machine in forge.json can now set memory (whole MiB, 256–65536) and cpus (cores, 0.25–64), and the crew run is held to them by the kernel. Example: "machines": { "audit": { "memory": 2048, "cpus": 2 } }.

  • Parsing (src/crew/machines.ts): a value out of bounds or of the wrong type rejects the declaration. As with any invalid declaration, the crew then runs in standard. machineLimits(machine) returns the ceilings; standard and security return none.
  • Enforcement (new src/crew/run-cell.ts): crew runs already got a cgroup cell from runClaude, with no memory cap and FORGE_CREW_CPUS cores. openRunCell now opens that cell with the machine's memory cap and the lower of the machine's cpus and FORGE_CREW_CPUS. A machine can only narrow the cell, never widen it. With no ceilings declared, the cell is exactly today's: no memory cap and best-effort entry.
  • Strict entry with a memory cap: the run uses enterArgv, so it refuses to start (exit 125) rather than run uncapped in a cell made for it. Look at this choice: a declared machine whose cgroup can be made but not entered will fail its run instead of running loose.
  • Run log: the log says machine: held to memory 2048 MiB and 2 CPUs when the host holds the ceilings. Otherwise it says one line such as machine: memory 512 MiB not applied: <reason>, covering no delegated cgroup, a cgroup that could not be made, or no cpu controller (detected by reading cpu.max back). The runner passes the limits in from src/crew/runner.ts.
  • Tests (test/crew-machines.test.ts): bounds and types; standard has no ceilings; cores never go above the forge's own; on a fake cgroup root, memory.max and cpu.max are written and the spawn argv enters cgroup.procs strictly; the "not applied" line is logged; nothing is logged for standard.
  • Docs: docs/features/machines.md says how to set the ceilings and what the log lines mean.

The issue named src/crew/sandbox.ts, but that file only builds Claude Code's sandbox settings. The process is started in src/crew/claude.ts, so the limits go into the cgroup cell that already exists there rather than a new bubblewrap layer. Disk ceilings are not done.

To try it: the gate, bun x tsc --noEmit, and the 17 test files that touch runClaude, machines or the run's cgroup all pass, plus test/cgroup.test.ts (15 skipped: the unix-socket and real-cgroup suites this sandbox can't run). scripts/fast.ts named 429 files (about 38 minutes), so the full suite was not run here.

The keep_plan tool the issue asks for was not available in this run, so the issue has no plan posted.

🤖 Generated with Claude Code

Approved

Memory and cpus parse with bounds, fall back to standard when invalid, and are written into the run cell's cgroup. A host that cannot hold them logs one line. Tests cover parsing, the cell files, the strict-entry argv and the log lines. The how-to is in docs/features/machines.md, not README. No new dependency, and no web or terminal surface was added. Not run here: the real-cgroup suites, which need a delegated cgroup v2. Two things I would do next: (1) the held line is logged before the run enters the cell, so a strict-entry failure (exit 125) says held and then fails; log the entry outcome, or say in the run failure that the cell could not be entered. (2) For memory, read memory.max back the way cpu.max is, so a missing memory controller logs not applied instead of held. Also add one test that runClaude receives limits from the runner, since only openRunCell is exercised now.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs