Skip to content

fix(shell): the jail test asks the sandbox's verdict on the hub, not the whole connect's - #294

Merged
ymote merged 4 commits into
mainfrom
fix/flaky-sandbox-jail-hub
Oct 2, 2026
Merged

ymote merged 4 commits into
mainfrom
fix/flaky-sandbox-jail-hub

Conversation

@ymote

@ymote ymote commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Fixes the flaky sandbox::tests::a_jail_only_app_reads_its_jail_and_is_refused_everything_else (crates/shell/src/sandbox/tests.rs:116). It failed twice on GitHub's macOS 14 runners (phone.yml / home): run 36980365594, job 110753267785, on PR #281; and run 36918328960 on main. Both times it failed with:

the hub is reachable: 

Cause

  • The probe was nc -z -w 2 127.0.0.1 <hub port> under the jail's Seatbelt profile. Apple's nc (remote_connectx in apple-oss-distributions/netcat) says why a connect failed only with -v. Its -w never bounds the blocking connectx (a blackholed connect takes 75 s here). The whole suite finished in 5.88 s, so this connect failed at once: it was refused.
  • Something refused it, either the sandbox (EPERM) or something past the sandbox (no listener on the port at that moment, or the network stack). The test read only nc's exit status, and that looks the same either way. Only the sandbox's verdict is what this test is about.
  • Closing the hub's listener before the probe reproduces the CI message exactly (mutation A below).

Ruled out on macOS 26, running only the sandbox tests:

  • The profile with every one of the 16,374 bindable ephemeral ports (all reachable).
  • 360 replays of the test's probe sequence, six in parallel.
  • 50 runs of this test alone, 50 runs of sandbox::, and 100 more with four processes in parallel. None failed.
  • A signal: one sent to the suite's process group would have stopped cargo too.
  • Pid reuse: macOS hands out pids in sequence.

What refused the connect on the runner was not recorded. With this change, nc will say it. GitHub's macOS queue was saturated, so I did not dispatch a diagnostic run on the macos-14 image.

Fix

  • The probe runs nc -v -n, so nc always says what became of each connect.
  • The hub fails only on the sandbox's own refusal: EPERM from Seatbelt and seccomp, EACCES from Landlock. It also fails if nc reports no connect at all (for example, sandbox-exec refused to start it). A refusal past the sandbox is printed and passes.
  • Any other port must now be refused by the sandbox. The old !ok passed on any failure.

Evidence

Mutations, built and run on macOS and not committed:

Mutation Result
A: main's test, hub listener dropped before the probe fails with the hub is reachable: and an empty detail, exactly as on CI
B: this test, the same mutation passes; prints the hub's connect left the sandbox and failed past it: Connection refused
C: this test, profile allows another port (a real sandbox refusal) fails with the sandbox lets the hub through: nc: connectx to 127.0.0.1 port … (tcp) failed: Operation not permitted

Repeated runs:

main this branch
macOS, the test alone, 50× 0 failed 0 failed
macOS, --lib sandbox::, 50× 0 failed 0 failed
Linux build host (Landlock, seccomp), the test alone, 50× — 0 failed
Linux build host, sandbox::, 50× — 0 failed

If the next CI failure says Operation not permitted, Seatbelt refused the hub itself. That would be a real problem for every network: none app, and this test will keep failing on it.

🤖 Generated with Claude Code

ymote and others added 4 commits October 2, 2026 13:57
The supervisor saw the kernel end (its stdout closing, or its exit
status) and read the stderr tail at once, but stderr is copied into the
tail by a task of its own. The line the kernel wrote just before it
exited could still be in the pipe, so the reason lost it: CI's
`a_kernel_that_dies_closes_its_connections_with_its_last_words` failed
with "the kernel closed its output: fake kernel up", without the
"asked to exit" the fake kernel wrote last.

The tail is now read after the kernel is stopped and the stderr task has
reached the end of the pipe (bounded at 2 s, for a process the kernel
started that still holds the pipe). Failed reasons get the same complete
tail. The test asks for the last words themselves, not just "exit".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… probes

The glance tile tests register probe system apps (os.glancetile,
os.glanceinput, os.glancequiet, os.glancesweep) in App Hub's
system-app registry. That registry is process-wide, has no removal, and
is the one `system_card_apps` lists, so a catalog test running after a
glance test in the same process saw the probes as launcher rows:
`bundled_apps_open_without_catalog_files_or_child_processes` failed its
catalog comparison 7 of 10 times beside `glance_card::` on main, and
`the_system_apps_ship_as_card_apps` compares the same list.

Tests now register their system apps through
`apps::test_system_apps::register`, which notes the id before it
registers it, and the tests of the build's own catalog leave noted ids
out. Production code is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`approvals::tests::the_relay_gets_decisions_made_before_it_was_installed`
began with `init_memory()`, which replaces the shell's process-wide
approvals with empty ones. Every consent a test running beside it had
given was gone. `app_chat::tests::the_panel_opens_a_sharing_context_and_sends_person_turns_there`
allows its app, then reads `agents::line()`, which reports a peer slug
only while the app is allowed. Beside `approvals::` it failed 50 of 50
runs with `peer_slug: null`. The same test also installed its recording
relay into the shared approvals, where it would take other tests'
decisions.

The relay seam (`approval_requested`, `set_relay`, `bus_requested`) is
now methods on `Approvals`. The free functions call them on the shell's
approvals, as before. The test builds `Approvals::memory()` and never
touches the shared one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the whole connect's

`a_jail_only_app_reads_its_jail_and_is_refused_everything_else` failed
on GitHub's macOS 14 runners with "the hub is reachable: " and nothing
after it: the sandboxed `nc -z` exited 1 without a word, because nc
prints why a connect failed only with `-v`. Its exit status says only
that the connect did not complete. A sandbox refusal and a refusal past
the sandbox (no listener on the port then, the network stack) look the
same, though only the first is what this test is about. Closing the
hub's listener before the probe reproduces the CI message exactly.

The probe now runs `nc -v -n` and the test reads what nc says. The hub
fails only on the sandbox's own refusal (EPERM from Seatbelt and
seccomp, EACCES from Landlock), or when nc never reported a connect at
all. A refusal past the sandbox is printed and passes. Any other port
must now be refused by the sandbox itself, not just fail, which the old
check could not tell apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote force-pushed the fix/flaky-sandbox-jail-hub branch from 70f4996 to 102b90b Compare October 2, 2026 20:57
@ymote
ymote merged commit e03717c into main Oct 2, 2026
3 of 5 checks passed
@ymote

ymote commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Merged in a merge train; local CI (Mac with rom toolchain + --linux-host) passed on the train head 72e5d55, which contains this PR.

  • tools/ci-local.sh --only all --linux-host, started 2026-10-02T14:00:45: passed. Desktop, phone and apps passed on the Mac; apps services, kernel-security and linux-host / sandbox passed on the Linux build host. Its rom.yml steps were skipped on the Linux host, and the train doesn't trigger rom.yml.
  • tools/ci-local.sh --only all on the Mac with the rom toolchain (JDK 17, Android SDK, node 24), started 2026-10-02T14:51:21: 42/42 steps passed, none skipped.

The train was #291, #293, #297, #294, #298, #295, #296, #299, #300 and #292, on main f6678b5. After the last merge, main 301be0c has the tested head's tree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant