Public: anyone can read this; the crew can change it.
A hosted child's admin sees process-wide counters that belong to its parent and siblings #10588
Check · Review · Land · Live done — Landed and live in production.
A hosted child's admin no longer sees counters that this process keeps once for every App in it. On a hosted child, `/metrics`, the capacity page, the capacity listing that the MCP `capacity` tool reads, and the terminal's egress screen now show none of the parent's or the other children's rows. The parent sees them all, as before.
shipped production is serving it deploy 9759e67
A hosted child's admin no longer sees counters that this process keeps once for every App in it. On a hosted child, /metrics, the capacity page, the capacity listing that the MCP capacity tool reads, and the terminal's egress screen now show none of the parent's or the other children's rows. The parent sees them all, as before.
What changed
- New
src/spawn/process_counters.tsgives one App's view of each process-wide counter:
- egressRowsFor, egressTotalsFor, npmBytesFor, npmTopRepoFor and cellCountersFor each answer empty ([], 0 or undefined) when isHostedChild(app) is true.
- Otherwise they return the counter from apps/egress.ts or apps/contain.ts unchanged.
- Every reader the issue names now goes through that module:
- the /metrics route in src/http/api.ts;
- appSources in src/metrics/app_sources.ts;
- egressHour and listCapacity in src/actions/capacity.ts. egressHour now takes the App instead of its database, so the hosted-child check can't be skipped;
- the open-connections list in src/web/pages/capacity.ts;
- connectionsScreen in src/tui/screens/egress.ts.
What I chose not to do
- A hosted child sees none of these counters, not its own share of them. The tables are keyed by labels like
app:<slug>/<env>andcheck:<run>, which don't say which App a cell belongs to, and a child's slugs can match its parent's. Showing each child its own rows would need the owning App recorded where cells and egress meters are opened. That's a separate change. boards/sampler.tsandspawn/usage.tsalso readcellCounters, but only for the parent (main.tsstarts one sampler), so I left them alone.
Tests
- New
test/hosted-child-counters.test.tssets up a parent and a hosted child in one process. It records egress to a parent-only destination, attributes npm bytes to a private parent repository, and watches a cell labelled with a parent app. It then checks:
- the child's /metrics names neither the destination nor the registry, and the parent's does;
- the child's /settings/capacity doesn't name the parent's repository;
- listCapacity gives the child an empty egress hour, while the parent's names alice/parent-private-repo;
- cellCountersFor gives the parent the cell and the child nothing.
- Without the fix, the
/metricstest and thelistCapacitytest fail. The capacity-page test passes either way: that page doesn't render destinations, and the parent's repository id doesn't resolve in the child's database. It's kept as a guard against regressions. bun scripts/gate.ts,bun x tsc --noEmitand the design lint pass, and so do 138 tests across the capacity, egress, metrics, hosted-child and terminal egress/capacity suites.scripts/fast.tsnamed 1639 files for this diff, which is too broad, so I picked those suites by hand.- No test covers the open-connections list, which is the part of the capacity page that would actually have shown the parent's rows. Populating it needs a real egress proxy on a unix socket, and this sandbox can't bind one.
The keep_plan tool wasn't available in this session, so there is no plan on the issue. The pre-push hook asked for a "Try:" commit, so there is an empty one on the branch.
To try it: on a hosted child, signed in as its owner, open /metrics and /settings/capacity. No parent destination, repository or cell label appears. The parent's own pages still list them.
🤖 Generated with Claude Code
The fix is the right shape: one module, every reader routed through it, and egressHour takes the App so the check can't be skipped. Two things to fix before it lands.
- Two of the changed readers have no test that fails without the change. The capacity-page test passes with or without the fix, as the description admits. The open-connections list in src/web/pages/capacity.ts and the TUI connections screen in src/tui/screens/egress.ts are not exercised at all. They were the rows that leaked most directly, and the checklist asks for a test that fails without each behaviour. Unit-test the pure pieces: assert
egressRowsFor(child)is[]andegressRowsFor(parent)returns the recorded rows, and assertnpmBytesFor/npmTopRepoFor/egressTotalsForthe same way. These need no proxy socket, because recordEgress feeds the same table. For the screen, render connectionsScreen with an admin Session on the child and assert the parent's destination is absent. Make the capacity-page test assert something that fails without the fix, or drop it.
- test/hosted-child-counters.test.ts polls for up to 4 s on a real
sleep 30process, which can flake. The direction asks for fast tests that never flake. Cover the cell case through the purecellCountersFor, or inject the counter, instead of spawning a process.
Also drop the empty "Try:" commit from the branch if it carries no change.
This is a security fix rather than one of the listed pipeline items. I'm not blocking on that, since it closes a cross-tenant leak.
On a hosted child, /metrics, the capacity page, the capacity listing that MCP reads (MCP is the agent tool interface) and the terminal's egress screens no longer show the process-wide egress and cell counters. Those counters belong to the parent and its sibling apps. Every reader goes through src/spawn/process_counters.ts, which returns nothing on a hosted child and the full counters on the parent.
This push answers the review:
- Counters can be injected: each reader in
process_counters.tstakes an optionalProcessCounterssource, live by default. So doesegressHour, and so doesconnectionsScreeninsrc/tui/screens/egress.ts. Tests give it rows directly, with no proxy socket and no watched process. - Gate tests:
egressRowsFor,egressTotalsFor,npmBytesFor,npmTopRepoForandcellCountersForeach return the parent's rows and nothing on the child. - Terminal screen test:
connectionsScreenis rendered with an admin session on each app. The parent shows the parent's connection and destination; the child shows neither and says no app holds a connection open. - Dropped: the capacity-page test that passed without the fix, and the
sleep 30polling test. - Kept: the
/metricsand capacity-listing tests, which do fail without the fix.
With the hosted-child check removed, all 5 tests in test/hosted-child-counters.test.ts fail; with it, all pass.
Where I differ from the review:
recordEgressdoes not feed the open-connections table. That table is filled only by the proxy (tally, a private function insrc/apps/egress.ts), soegressRowsForcan't be tested by recording rows. That is why the counters are injected instead.- The empty "Try:" commit (113cdc25d) is already on the remote. Removing it would take a force push, which is refused, so it stays.
bun x tsc --noEmit and bun scripts/gate.ts pass. bun scripts/fast.ts found the change too broad to pick tests from (1639 files), so I ran the capacity, egress, metrics and admin test files myself: 265 tests, none failing. The web code didn't change in this push, so design-lint didn't need to run again.
To try it, open /metrics and /settings/capacity on a hosted child as its owner. No parent destination, repository or cell is named. The parent's admin still sees all of them.
🤖 Generated with Claude Code
Earlier asks are met. Counters are injectable through ProcessCounters, with gate tests for each reader. The terminal connections screen is tested on both apps. The test that passed without the fix and the sleep-based test are gone. The remaining tests fail without the hosted-child check. Every reader named in the issue goes through process_counters.ts, and egressHour takes the App so the check cannot be skipped. The terminal screen is gated the same way as the page. No new dependency, no README change, no secrets. The empty Try commit is harmless. Non-blocking: the open-connections list on the capacity page has no test of its own, because it needs a proxy socket.
A hosted child's admin no longer sees counters that this process keeps once for every App in it. On a hosted child,
/metrics, the capacity page, the capacity listing that the MCPcapacitytool reads, and the terminal's egress screen now show none of the parent's or the other children's rows. The parent sees them all, as before.What changed
src/spawn/process_counters.tsgives one App's view of each process-wide counter:-
egressRowsFor,egressTotalsFor,npmBytesFor,npmTopRepoForandcellCountersForeach answer empty ([],0orundefined) whenisHostedChild(app)is true. - Otherwise they return the counter fromapps/egress.tsorapps/contain.tsunchanged.- the
/metricsroute insrc/http/api.ts; -appSourcesinsrc/metrics/app_sources.ts; -egressHourandlistCapacityinsrc/actions/capacity.ts.egressHournow takes theAppinstead of its database, so the hosted-child check can't be skipped; - the open-connections list insrc/web/pages/capacity.ts; -connectionsScreeninsrc/tui/screens/egress.ts.What I chose not to do
app:<slug>/<env>andcheck:<run>, which don't say which App a cell belongs to, and a child's slugs can match its parent's. Showing each child its own rows would need the owning App recorded where cells and egress meters are opened. That's a separate change.boards/sampler.tsandspawn/usage.tsalso readcellCounters, but only for the parent (main.tsstarts one sampler), so I left them alone.Tests
test/hosted-child-counters.test.tssets up a parent and a hosted child in one process. It records egress to a parent-only destination, attributes npm bytes to a private parent repository, and watches a cell labelled with a parent app. It then checks:- the child's
/metricsnames neither the destination nor the registry, and the parent's does; - the child's/settings/capacitydoesn't name the parent's repository; -listCapacitygives the child an empty egress hour, while the parent's namesalice/parent-private-repo; -cellCountersForgives the parent the cell and the child nothing./metricstest and thelistCapacitytest fail. The capacity-page test passes either way: that page doesn't render destinations, and the parent's repository id doesn't resolve in the child's database. It's kept as a guard against regressions.bun scripts/gate.ts,bun x tsc --noEmitand the design lint pass, and so do 138 tests across the capacity, egress, metrics, hosted-child and terminal egress/capacity suites.scripts/fast.tsnamed 1639 files for this diff, which is too broad, so I picked those suites by hand.The
keep_plantool wasn't available in this session, so there is no plan on the issue. The pre-push hook asked for a "Try:" commit, so there is an empty one on the branch.To try it: on a hosted child, signed in as its owner, open
/metricsand/settings/capacity. No parent destination, repository or cell label appears. The parent's own pages still list them.🤖 Generated with Claude Code
Closes #10517