Public: anyone can read this; the crew can change it.
Cells on the forge pod have no kernel memory or process cap #10584
Check · Review · Land · Live done — Landed and live in production.
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
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 thepidscontroller for the forge's cgroup and forcells, the same optional way it already doescpuandio. It never fails without it.openCellCgrouptakes an optionalprocesseslimit, writes it topids.max, and reportspidsCapped. 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, orFORGE_CELL_CGROUPpointing at a subtree withmemoryandpids.
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()answeringkernelon 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
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.
- 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,
pidsCappedin src/apps/cgroup.ts is set and read only by the new test, so it is dead in production.
- The wiring has no test that fails without it. The only new test calls openCellCgroup with PROCESSES_MAX itself. Delete the
PROCESSES_MAXargument 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.
- 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.
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.openCellCgroupwritespids.maxonly 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:
openCellnow gets its cgroup from a newcellCgroup(spec, host)insrc/apps/contain.ts, which passesPROCESSES_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-pidswithdata-by="kernel|watch"and the reason when it is the watch. Both holders are timed together in the existingcapsphase.
Review points
pidsCappeddeleted instead of given a reader. The cap is a fact about the host, decided bypidsDelegated, and/systemreports 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: nopids.maxfile, the cell still opens, and the holder answerswatch.- 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.tsasserts#cell-pidsis on the page.
Checks run
bun scripts/gate.ts,bun x tsc --noEmitandbun scripts/design-lint.tsare clean.scripts/fast.tsnamed 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/cgroupis 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. 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:
- 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.
- 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.
Cells now get a kernel process cap (
pids.max) wherever the forge holds a delegated cgroup. Before, a delegated cgroup set onlymemory.max, and nothing anywhere wrotepids.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/appis the per-app chart), so that can't be done from a pull.What changed
src/apps/cgroup.ts:prepare()turns on thepidscontroller for the forge's cgroup and forcells, the same optional way it already doescpuandio. It never fails without it.openCellCgrouptakes an optionalprocesseslimit, writes it topids.max, and reportspidsCapped. 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.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, orFORGE_CELL_CGROUPpointing at a subtree withmemoryandpids.Test changed: in
test/cgroup.test.ts, the layout test's fake cgroup already listspids, so the last controller write it reads back is now+pidsinstead of+memory. The assertion was updated for that new behaviour, not because it was wrong. A new test checks thatpids.maxis 512 for a cell, absent for a crew run, and that the forge's own cgroup gets no pids weight.Not done
memoryCapHolder()answeringkernelon the pod, can only pass once the cgroup is delegated there.keep_plan, because that tool isn't available in this run.Checks:
bun scripts/gate.tsandbun x tsc --noEmitpass.scripts/fast.tsnamed 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-cellsand thesystem*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/cgroupis 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. Thencat <R>/cells/c*/pids.maxduring a check should print512.Part of #10518.
🤖 Generated with Claude Code
Closes #10518