Public: anyone can read this; the crew can change it.
The forge knows whether each child forge's writable space is a filesystem of its own, says so, and can refuse to start one that is not #10881
Check · Review · Land · Live done — Landed and live in production.
The forge now checks whether each child forge's data directory is a filesystem of its own. It says so on the child's line and, with a new setting turned on, refuses to start a child that has none.
shipped production is serving it deploy da127e7
The forge now checks whether each child forge's data directory is a filesystem of its own. It says so on the child's line and, with a new setting turned on, refuses to start a child that has none.
src/spawn/disk_mount.ts(new): reads/proc/self/mountinfo, passed in as text, for a mount exactly at<data>/children/<id>. Symlinks are resolved and escaped characters in mount paths are decoded. A mount above, beside or below that path does not count. The size comes fromstatfs: blocks × block size in bytes, plus inodes. A mount whose size can't be read counts as shared. It also validates body ids, builds theown filesystem, N GiB/shared diskwords and the one-line refusal.cellSpecFor: takes the disk result and binds the child's own filesystem by its real path. With no mount the spec is exactly as before, and a test checks that.- New setting
children.ownFilesystem: a capacity setting on/settings/capacity, off by default, with its reason shown beside it. It can be read and set through the existingcapacityMCP tool; I added the key toSETTABLE_CAPACITYand didn't add a new tool. When it is on, a child that isn't already running and has no filesystem of its own is refused at every place a child starts:
- its own address (wakeGate);
- its apps' addresses (resolveHostedChildApp, which can now return { refused }, handled in server.ts);
- the boot and after-spawn open (bootHostedChild).
The refusal is a 503 in plain text with one line naming the child and saying why. This applies to hosted children as well as cell children. That matters because a cell that fails to start is turned into a hosted child, so refusing only the cell would let the child start anyway. Children already running are not stopped. With the setting off, the mount table is never read.
- The child's line says
own filesystem, N GiB(GiB = 2^30 bytes, to one place) orshared disk. I put this on/forgesand the terminal's forges screen, because that is where each child forge has a line. The machines page lists a repository's shell machines, not child forges, so nothing changed there. - Docs and tests: a new section in
docs/APPS.md.test/disk-mount.test.tscovers mountinfo with and without the mount, a hand-checked size (2,621,440 × 4,096 = 10,737,418,240 bytes = 10 GiB), the setting on refusing and off letting a child start, the MCP read and write, and escaping on/forges. - Changed existing tests:
- test/tui-forges.test.ts: the expected line now includes "shared disk", which is the new behaviour.
- test/forges-attention.test.ts: its row fixture gains the new disk field.
- test/spawn-child-runs-apps.test.ts: handles the new refused result.
Not done: the forge mounts nothing and the chart is untouched; making the per-child filesystem is still the platform's job in #10851. The side-by-side table's "Disk" column is unchanged.
Checks: bun scripts/gate.ts, bun x tsc --noEmit, bun scripts/design-lint.ts and test/mcp-size.test.ts pass. 18 related test files ran: 208 passed, 3 skipped, 0 failed (including spawn-cell, forge-sleep, capacity, tui-capacity and tui-forges). I didn't run the whole suite: bun scripts/fast.ts named 2,123 files because src/http/server.ts changed.
To try it: on the preview, open /settings/capacity and set "Child forges need a filesystem of their own" to on. Then open a sleeping child from /forges: it answers with the one-line refusal. Each child's line on /forges reads shared disk until the platform mounts one per child.
🤖 Generated with Claude Code
Each child forge now records whether its writable space is a filesystem of its own. The forge shows this on /forges and in the terminal, and it can refuse to start a child that sits on the shared disk.
- Detection:
src/spawn/disk_mount.tsreads/proc/self/mountinfo, passed in as text, and looks for a mount exactly at<data>/children/<id>. When it finds one it reads that filesystem's size in bytes and inodes with statfs. - Starting the child: when a child has its own filesystem,
cellSpecForbinds it. When it does not, the spec is unchanged. - The guard: a new capacity setting,
children.ownFilesystem, is off by default. It can be set on/settings/capacityor through thecapacityMCP tool. When it is on, a child on the shared disk is not started at boot, and its pages and apps answer 503 with one plain line that names it. - What people see: each child's line on
/forgesand on the terminal's forges screen reads "own filesystem, N GiB" or "shared disk". - Docs:
docs/MCP.mdhas the regeneratedcapacityrow (frombun scripts/mcp-table.ts), so it matches the tool's description again. The earlier check failed because this row was stale. - Test change: in
test/tui-forges.test.tsthe expected row now ends in "shared disk". The test wasn't wrong; the row it checks gained that text.
This pull does not create or size filesystems for children. It only detects and reports a mount that is already there.
Before pushing I ran test/mcp-table.test.ts, bun scripts/gate.ts and bun x tsc --noEmit, and all passed.
To try it on the preview: go to /settings/capacity and set "Child forges need a filesystem of their own" to on. Then open a child forge from /forges: it should answer with the one-line refusal, and its line on /forges should say "shared disk".
🤖 Generated with Claude Code
Sound and secure: the refusal line is plain text with a validated child name, nothing from the client is trusted, and the setting is reachable on the settings page, over MCP and in the TUI. Two changes before it lands.
- (fix first) The how-to is in docs/APPS.md, but AGENTS.md and the review bar say a feature's how-to is
docs/features/<name>.md. Adddocs/features/child-filesystem.md(what the line means, how to turnchildren.ownFilesystemon, what the refusal says) and link it from where AGENTS.md says. Keep the README untouched, as it is now.
- Behaviour added without a test that fails without it.
bootHostedChildnow skips a refused child (src/spawn/host.ts, thestartRefusedline added in it) and the cell registry passesliveChildDisk(...)intocellSpecFor(src/spawn/cell.ts,makeRegistry). Neither is tested; the cell test only feedsdiskby hand. Add a test that boots a hosted child with the setting on and asserts it is not opened (and is opened with it off), and one that the registry's spec carries the disk bind.
Small cleanup: in src/spawn/disk_mount.ts, validBodyId also checks id !== "." && id !== "..", which the regex's first character already rules out, so that is dead code. Mount.source is parsed but never read. Drop both, or use them.
Note on box 4: the issue says the machines page, the pull puts the line on /forges and the terminal's forges screen, where child forges are listed. I accept that, since the machines page lists shell machines.
Design: no principle broken; the line is one more muted fact in the row's existing meta, with no new accent, box or component.
Each child forge now records whether its writable space (<data>/children/<id>) is a filesystem of its own. /forges and the terminal's forges screen say so, and the forge can refuse to start a child that sits on the shared disk.
- Detection:
src/spawn/disk_mount.tsreads/proc/self/mountinfoand looks for a mount exactly at that path, then reads its size withstatfsin bytes and inodes. The forge mounts nothing itself. - The cell: when a child has its own filesystem, its cell binds it by its real path. The registry now builds the child's cell spec through
launchSpecForinsrc/spawn/cell.ts, so that bind can be tested. With no filesystem of its own the spec is unchanged. - The guard: a new capacity setting,
children.ownFilesystem, is off by default. It can be set in Settings → Capacity, on the terminal's Capacity screen, or with thecapacityMCP tool. While it is on, a child on the shared disk is not started:
- its pages and its apps answer 503 with one plain line that names the child; - the boot skips it and logs the same line; - a child that is already running is not stopped.
- The line: each child's line reads "own filesystem, N GiB" or "shared disk".
- Docs:
docs/features/child-filesystem.mdis the how-to, linked from the section indocs/APPS.md. The README is unchanged, and thecapacityrow indocs/MCP.mdis regenerated.
What to look at:
- Fixed test: a test I wrote earlier in this pull was wrong. It checked the capacity tool's schema in
test/disk-mount.test.tswithtoMatchObjectandexpect.arrayContaining. Bun writes the matcher into the object it checks, and that schema is shared, sotest/mcp-table.test.tsfailed whenever both files ran in one shard. The check now reads the enum and usestoContain. - New tests: one boots a spawned hosted child and finds it left shut with the setting on and opened with it off. Another checks that
launchSpecForbinds the child's filesystem when mountinfo has a mount there, and binds nothing when it does not. Both fail if the code they cover is removed. - Cleanup:
Mount.sourceand the./..checks invalidBodyIdare removed; the regex already rules both out. - Not done: this pull does not create or size filesystems for children; it only detects and reports one that is already mounted. The line is on
/forgesand the terminal's forges screen, where child forges are listed, rather than on the machines page, which lists shell machines.
Before pushing, bun scripts/gate.ts and bun x tsc --noEmit passed. scripts/fast.ts named 2123 test files, too many to run here, so I ran the 14 that cover the change: 124 pass and 3 are skipped. The skipped ones are the unix-socket suites, which this sandbox cannot run; the forge's check runs them.
To try it: set "Child forges need a filesystem of their own" to on in /settings/capacity, then open a child forge from /forges. It answers with the one-line refusal, and its line on /forges says "shared disk".
🤖 Generated with Claude Code
The forge now checks whether each child forge's writable space is a filesystem of its own, shows the answer in its pages, and can refuse to start a child whose space is not. Merging main in conflicted on one line, the summary line forgeLines builds for each forge in src/tui/screens/forges.ts. main had added the forge's credit (creditWords(r)) to that line, and this branch had added its disk words (diskWords(r.disk), shown only for forges that have been made). The resolved line keeps both: credit after spend, disk after use. The imports for both merged on their own.
bun scripts/gate.ts and bun x tsc --noEmit pass. bun scripts/fast.ts named 2132 test files for this diff because it compares against main, so instead I ran the 9 test files for the forges list, the terminal screens, child credit and the disk mount: 65 passed, 2 skipped, 0 failed. The sandbox blocked some outbound requests those tests make, which did not cause any failures. The forge runs the full check on this push.
🤖 Generated with Claude Code
Approved. The earlier asks are met: how-to in docs/features/child-filesystem.md (README untouched), the TUI line, launchSpecFor tested, boot test added, MCP table regenerated, tests fixed. The merge only touched the forgeLines line and keeps both additions. Refusal covers wakeGate, app hosts and boot; the setting is read first so off costs nothing. Non-blocking: startRefused logs a warn on every request to a refused sleeping child, so a flood of requests makes log noise; rate-limit or log once per child. Non-blocking: the line is on /forges and the terminal, not the machines page, which lists shell machines; that is reasonable.
The forge now checks whether each child forge's data directory is a filesystem of its own. It says so on the child's line and, with a new setting turned on, refuses to start a child that has none.
src/spawn/disk_mount.ts(new): reads/proc/self/mountinfo, passed in as text, for a mount exactly at<data>/children/<id>. Symlinks are resolved and escaped characters in mount paths are decoded. A mount above, beside or below that path does not count. The size comes fromstatfs: blocks × block size in bytes, plus inodes. A mount whose size can't be read counts as shared. It also validates body ids, builds theown filesystem, N GiB/shared diskwords and the one-line refusal.cellSpecFor: takes the disk result and binds the child's own filesystem by its real path. With no mount the spec is exactly as before, and a test checks that.children.ownFilesystem: a capacity setting on/settings/capacity, off by default, with its reason shown beside it. It can be read and set through the existingcapacityMCP tool; I added the key toSETTABLE_CAPACITYand didn't add a new tool. When it is on, a child that isn't already running and has no filesystem of its own is refused at every place a child starts:- its own address (
wakeGate); - its apps' addresses (resolveHostedChildApp, which can now return{ refused }, handled inserver.ts); - the boot and after-spawn open (bootHostedChild).The refusal is a 503 in plain text with one line naming the child and saying why. This applies to hosted children as well as cell children. That matters because a cell that fails to start is turned into a hosted child, so refusing only the cell would let the child start anyway. Children already running are not stopped. With the setting off, the mount table is never read.
own filesystem, N GiB(GiB = 2^30 bytes, to one place) orshared disk. I put this on/forgesand the terminal's forges screen, because that is where each child forge has a line. The machines page lists a repository's shell machines, not child forges, so nothing changed there.docs/APPS.md.test/disk-mount.test.tscovers mountinfo with and without the mount, a hand-checked size (2,621,440 × 4,096 = 10,737,418,240 bytes = 10 GiB), the setting on refusing and off letting a child start, the MCP read and write, and escaping on/forges.-
test/tui-forges.test.ts: the expected line now includes "shared disk", which is the new behaviour. -test/forges-attention.test.ts: its row fixture gains the newdiskfield. -test/spawn-child-runs-apps.test.ts: handles the newrefusedresult.Not done: the forge mounts nothing and the chart is untouched; making the per-child filesystem is still the platform's job in #10851. The side-by-side table's "Disk" column is unchanged.
Checks:
bun scripts/gate.ts,bun x tsc --noEmit,bun scripts/design-lint.tsandtest/mcp-size.test.tspass. 18 related test files ran: 208 passed, 3 skipped, 0 failed (includingspawn-cell,forge-sleep,capacity,tui-capacityandtui-forges). I didn't run the whole suite:bun scripts/fast.tsnamed 2,123 files becausesrc/http/server.tschanged.To try it: on the preview, open
/settings/capacityand set "Child forges need a filesystem of their own" to on. Then open a sleeping child from/forges: it answers with the one-line refusal. Each child's line on/forgesreadsshared diskuntil the platform mounts one per child.🤖 Generated with Claude Code
Closes #10856