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

Merged opened this in answer to

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

Merged

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

opened this

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.ts gives 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> and check:<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.ts and spawn/usage.ts also read cellCounters, but only for the parent (main.ts starts one sampler), so I left them alone.

Tests

  • New test/hosted-child-counters.test.ts sets 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 /metrics test and the listCapacity test 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 --noEmit and the design lint pass, and so do 138 tests across the capacity, egress, metrics, hosted-child and terminal egress/capacity suites. scripts/fast.ts named 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

Closes #10517

commented

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.ts gives 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> and check:<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.ts and spawn/usage.ts also read cellCounters, but only for the parent (main.ts starts one sampler), so I left them alone.

Tests

  • New test/hosted-child-counters.test.ts sets 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 /metrics test and the listCapacity test 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 --noEmit and the design lint pass, and so do 138 tests across the capacity, egress, metrics, hosted-child and terminal egress/capacity suites. scripts/fast.ts named 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

Changes requested — before the latest push

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.

  1. 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 [] and egressRowsFor(parent) returns the recorded rows, and assert npmBytesFor / npmTopRepoFor / egressTotalsFor the 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.
  1. test/hosted-child-counters.test.ts polls for up to 4 s on a real sleep 30 process, which can flake. The direction asks for fast tests that never flake. Cover the cell case through the pure cellCountersFor, 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.

commented

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.ts takes an optional ProcessCounters source, live by default. So does egressHour, and so does connectionsScreen in src/tui/screens/egress.ts. Tests give it rows directly, with no proxy socket and no watched process.
  • Gate tests: egressRowsFor, egressTotalsFor, npmBytesFor, npmTopRepoFor and cellCountersFor each return the parent's rows and nothing on the child.
  • Terminal screen test: connectionsScreen is 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 30 polling test.
  • Kept: the /metrics and 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:

  • recordEgress does not feed the open-connections table. That table is filled only by the proxy (tally, a private function in src/apps/egress.ts), so egressRowsFor can'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

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs