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

Merged opened this

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

Merged

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

opened this

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 from statfs: blocks × block size in bytes, plus inodes. A mount whose size can't be read counts as shared. It also validates body ids, builds the own filesystem, N GiB / shared disk words 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 existing capacity MCP tool; I added the key to SETTABLE_CAPACITY and 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) or shared disk. I put this on /forges and 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.ts covers 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

Closes #10856

commented

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 from statfs: blocks × block size in bytes, plus inodes. A mount whose size can't be read counts as shared. It also validates body ids, builds the own filesystem, N GiB / shared disk words 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 existing capacity MCP tool; I added the key to SETTABLE_CAPACITY and 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) or shared disk. I put this on /forges and 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.ts covers 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

commented

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.ts reads /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, cellSpecFor binds 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/capacity or through the capacity MCP 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 /forges and on the terminal's forges screen reads "own filesystem, N GiB" or "shared disk".
  • Docs: docs/MCP.md has the regenerated capacity row (from bun 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.ts the 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

Changes requested — before the latest push

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.

  1. (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. Add docs/features/child-filesystem.md (what the line means, how to turn children.ownFilesystem on, what the refusal says) and link it from where AGENTS.md says. Keep the README untouched, as it is now.
  1. Behaviour added without a test that fails without it. bootHostedChild now skips a refused child (src/spawn/host.ts, the startRefused line added in it) and the cell registry passes liveChildDisk(...) into cellSpecFor (src/spawn/cell.ts, makeRegistry). Neither is tested; the cell test only feeds disk by 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.

commented

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.ts reads /proc/self/mountinfo and looks for a mount exactly at that path, then reads its size with statfs in 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 launchSpecFor in src/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 the capacity MCP 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.md is the how-to, linked from the section in docs/APPS.md. The README is unchanged, and the capacity row in docs/MCP.md is 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.ts with toMatchObject and expect.arrayContaining. Bun writes the matcher into the object it checks, and that schema is shared, so test/mcp-table.test.ts failed whenever both files ran in one shard. The check now reads the enum and uses toContain.
  • 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 launchSpecFor binds 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.source and the ./.. checks in validBodyId are 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 /forges and 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

commented

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

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs