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

Cells on the forge pod have no kernel memory or process cap #10584

Merged opened this in answer to

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

Merged

Cells now get a kernel process cap (`pids.max`) wherever the forge holds a delegated cgroup. Before, a delegated cgroup set only `memory.max`, and nothing anywhere wrote `pids.max`. This pull doesn't close #10518: the pod still needs the owner to delegate a cgroup. The forge pod's manifest isn't in this repository (`charts/app` is the per-app chart), so that can't be done from a pull.

shipped production is serving it deploy cca14ac

opened this

Cells now get a kernel process cap (pids.max) wherever the forge holds a delegated cgroup. Before, a delegated cgroup set only memory.max, and nothing anywhere wrote pids.max. This pull doesn't close #10518: the pod still needs the owner to delegate a cgroup. The forge pod's manifest isn't in this repository (charts/app is the per-app chart), so that can't be done from a pull.

What changed

  • src/apps/cgroup.ts: prepare() turns on the pids controller for the forge's cgroup and for cells, the same optional way it already does cpu and io. It never fails without it.
  • openCellCgroup takes an optional processes limit, writes it to pids.max, and reports pidsCapped. If the controller isn't there, the write fails quietly and the 1 s watch keeps counting, as it does today.
  • src/apps/contain.ts: PROCESSES_MAX (512, the watch's own limit) is now exported and passed for every app and check cell. A fork bomb then fails inside its own cell before the watch's next check, and the forge's process table stays intact.
  • Crew runs get no process cap; they never had one, so that is unchanged.
  • docs/APPS.md: describes the process cap and how an operator delegates a cgroup on Kubernetes: its own cgroup namespace with a writable cgroup2 mount owned by the forge's uid, or FORGE_CELL_CGROUP pointing at a subtree with memory and pids.

Test changed: in test/cgroup.test.ts, the layout test's fake cgroup already lists pids, so the last controller write it reads back is now +pids instead of +memory. The assertion was updated for that new behaviour, not because it was wrong. A new test checks that pids.max is 512 for a cell, absent for a crew run, and that the forge's own cgroup gets no pids weight.

Not done

  • The deployment change itself.
  • Adding the process cap to the /system wording, which still talks only about memory.
  • The issue's test, memoryCapHolder() answering kernel on the pod, can only pass once the cgroup is delegated there.
  • I couldn't update the issue's plan with keep_plan, because that tool isn't available in this run.

Checks: bun scripts/gate.ts and bun x tsc --noEmit pass. scripts/fast.ts named 1,841 test files for this diff, too many to run here, so I ran the suites that cover it: cgroup, contain, spawn-cell, crew-sandbox, hosted-child-cells, metrics-cells and the system* page tests. 165 passed, 36 skipped, 0 failed. The skipped ones are the real kernel-kill and unix-socket suites, which can't run here because /sys/fs/cgroup is read-only in this sandbox; the forge's check runs them on push.

To try it: where a cgroup is delegated with pids, open /system; it should say the kernel holds the cap. Then cat <R>/cells/c*/pids.max during a check should print 512.

Part of #10518.

🤖 Generated with Claude Code

Closes #10518

commented

Cells now get a kernel process cap (pids.max) wherever the forge holds a delegated cgroup. Before, a delegated cgroup set only memory.max, and nothing anywhere wrote pids.max. This pull doesn't close #10518: the pod still needs the owner to delegate a cgroup. The forge pod's manifest isn't in this repository (charts/app is the per-app chart), so that can't be done from a pull.

What changed

  • src/apps/cgroup.ts: prepare() turns on the pids controller for the forge's cgroup and for cells, the same optional way it already does cpu and io. It never fails without it.
  • openCellCgroup takes an optional processes limit, writes it to pids.max, and reports pidsCapped. If the controller isn't there, the write fails quietly and the 1 s watch keeps counting, as it does today.
  • src/apps/contain.ts: PROCESSES_MAX (512, the watch's own limit) is now exported and passed for every app and check cell. A fork bomb then fails inside its own cell before the watch's next check, and the forge's process table stays intact.
  • Crew runs get no process cap; they never had one, so that is unchanged.
  • docs/APPS.md: describes the process cap and how an operator delegates a cgroup on Kubernetes: its own cgroup namespace with a writable cgroup2 mount owned by the forge's uid, or FORGE_CELL_CGROUP pointing at a subtree with memory and pids.

Test changed: in test/cgroup.test.ts, the layout test's fake cgroup already lists pids, so the last controller write it reads back is now +pids instead of +memory. The assertion was updated for that new behaviour, not because it was wrong. A new test checks that pids.max is 512 for a cell, absent for a crew run, and that the forge's own cgroup gets no pids weight.

Not done

  • The deployment change itself.
  • Adding the process cap to the /system wording, which still talks only about memory.
  • The issue's test, memoryCapHolder() answering kernel on the pod, can only pass once the cgroup is delegated there.
  • I couldn't update the issue's plan with keep_plan, because that tool isn't available in this run.

Checks: bun scripts/gate.ts and bun x tsc --noEmit pass. scripts/fast.ts named 1,841 test files for this diff, too many to run here, so I ran the suites that cover it: cgroup, contain, spawn-cell, crew-sandbox, hosted-child-cells, metrics-cells and the system* page tests. 165 passed, 36 skipped, 0 failed. The skipped ones are the real kernel-kill and unix-socket suites, which can't run here because /sys/fs/cgroup is read-only in this sandbox; the forge's check runs them on push.

To try it: where a cgroup is delegated with pids, open /system; it should say the kernel holds the cap. Then cat <R>/cells/c*/pids.max during a check should print 512.

Part of #10518.

🤖 Generated with Claude Code

Changes requested — before the latest push

The pids.max wiring in cgroup.ts is sound and optional in the right way, and the docs sit in docs/APPS.md, not README. But three things stop it landing.

  1. The issue (#10518) is that cells on the pod have no kernel memory cap, and /system must say so. This pull does not change that: it adds a pids cap that only applies where a cgroup is already delegated, and leaves the issue's own test (memoryCapHolder answering kernel) and the /system wording undone. Say plainly in the description that it is a partial step, and do the part that is in reach: make /system report the process cap (pidsCapped) beside the memory cap. That also gives pidsCapped a reader. As written, pidsCapped in src/apps/cgroup.ts is set and read only by the new test, so it is dead in production.
  1. The wiring has no test that fails without it. The only new test calls openCellCgroup with PROCESSES_MAX itself. Delete the PROCESSES_MAX argument in contain.ts openCell and every test stays green. Add a test that openCell passes the limit to the cgroup, for example through an injected host, or assert the cell's pids.max.
  1. The failure path is untested. The comment says a failed pids.max write is swallowed and the watch keeps counting. Test that: a host with no pids controller returns pidsCapped false and the cell still opens. Also test that an invalid limit (0, 1.5) writes nothing.

The subtree_control assertions now read back +pids only because the fake keeps the last write. That is acceptable, but add one assertion that +memory is still written, so dropping it would fail a test.

This is also not on the pipeline-first list. If it goes ahead, keep it to this one concern.

commented

Cells get pids.max from the kernel where the pids controller is delegated, and /system now says who holds a cell's process count, next to the line about who holds its memory cap. This is only part of #10518. On the forge pod no cgroup is delegated, so cells there are still capped only by the once-a-second watch, for memory and processes alike. The issue's own test (memoryCapHolder() answering kernel on the pod) needs the owner to delegate a cgroup to the container. The code can't do that, so the issue should stay open. docs/APPS.md says how the operator delegates one.

What changed

  • The cap itself: prepare() turns on the pids controller for cells when the host offers it; it is never required. openCellCgroup writes pids.max only when two things hold:

- the cells' cgroup.subtree_control reads back pids (new pidsDelegated(host) in src/apps/cgroup.ts); - the limit is a positive whole number.

The write runs only when the pids controller is enabled for the cells; if the kernel still refuses it, the error is ignored and the watch keeps counting. Crew runs (src/crew/run-cell.ts) still get no process cap.

  • Where the cell's cgroup is opened: openCell now gets its cgroup from a new cellCgroup(spec, host) in src/apps/contain.ts, which passes PROCESSES_MAX (512). Being its own function, a test can call it on a fake host.
  • /system: processCapHolder(host) answers from the same check that decides the write. The page renders it as #cell-pids with data-by="kernel|watch" and the reason when it is the watch. Both holders are timed together in the existing caps phase.

Review points

  • pidsCapped deleted instead of given a reader. The cap is a fact about the host, decided by pidsDelegated, and /system reports it once for the page, as it does memory. The flag was per cell, and showing it per cell would mean changing the live process view, which is beyond this one concern. The failure path is tested through what the flag stood for: no pids.max file, the cell still opens, and the holder answers watch.
  • Tests in test/cgroup.test.ts:

- cellCgroup writes pids.max = 512. I removed the PROCESSES_MAX argument as a check, and this test went red. - A host with only the memory controller: the cell opens with its memory.max, no pids.max is written, and the holder answers watch with its reason. - A missing limit or 0, −1, 1.5 and NaN write nothing. - +memory is still what the cells' subtree_control gets.

  • test/system-telemetry.test.ts asserts #cell-pids is on the page.

Checks run

  • bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/design-lint.ts are clean.
  • scripts/fast.ts named 1841 files for this diff, so I picked the suites myself: cgroup, contain, metrics-cells, crew-watch, system-telemetry, and the system timing, parts, btop, crew and memory suites. 75 pass, 0 fail, 33 skipped. Most of the skips are the suites that need real cells, which this sandbox can't run (here, cgroup.test.ts's real-kernel memory-kill suite, since /sys/fs/cgroup is read-only); the forge's check runs them.

How to try it: open /system as the owner. Under Machine, next to the memory-cap line, #cell-pids says whether the kernel (pids.max 512) or the watch holds each cell's process count, and why.

🤖 Generated with Claude Code

Approved

Approved. The earlier ask (the /system wording and a reader for the process cap) is met: #cell-pids shows who holds the process count and why, and the pidsCapped flag was dropped instead of left unread. Tests cover pids.max being written, the missing-controller path, bad limits, and the page element. The preview answers 200. The new line follows the existing memory-cap paragraph, which uses the same markup, so there is no new component or accent and it works with JavaScript off.

Two things I would do next:

  1. Non-blocking: /system has no screen in src/tui/, so neither cap line is in the terminal. Add a system screen that carries both holders.
  2. Non-blocking: #10518 stays open until the owner delegates a cgroup to the pod. Say so in the issue so it is not closed by this pull.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs