Repository navigation
Description
Problem
acq ports <sandbox> reports a create-time published mapping as if it were live, when in fact it only echoes the sandbox's declared configuration. When the host-side forwarder failed to bind — because a stale forwarder from an earlier sandbox still holds the port — acq ports still prints the mapping, so a broken route looks healthy.
Observed:
$ acq ports opencode-agentic-coding
sandbox 6767 -> host 127.0.0.1:6767 (create-time -p)
$ lsof -nP -iTCP:6767 -sTCP:LISTEN
COMMAND PID USER FD TYPE ... NAME
msb 75399 bretamogilefsky 45u IPv4 ... TCP 127.0.0.1:6767 (LISTEN)
PID 75399 was a forwarder from a different, earlier sandbox — 7 days old, PPID 1, and from a different msb installation (Cellar/microsandbox/0.7.2) than the one acq drives (Cellar/microsandbox-acq/0.6.18). It survived acq stop and acq rm, so the recreated sandbox's own forwarder never bound. acq ports reported success throughout.
Why this is costly
The mapping is the thing an operator checks first to decide whether the route is good. Because it reported success, every restart appeared to work and the investigation moved away from the port layer. Under a TLS-intercepting sandbox the downstream symptom is a provider 401 "Authentication failed" (a placeholder credential that never got substituted, because the request reached a daemon outside the interception path) — so the visible failure is an auth error two layers from its cause, while the one command that could have localised it said the mapping was fine.
Where it comes from
_acq_msb_ports_from_inspect (acq.backends/msb.sh) parses msb inspect --format json → .active_config.network.ports[] and prints (create-time -p). That is the declared config; nothing cross-checks that a host listener exists, or that it belongs to this sandbox. The function is correct for what it does — the gap is that its output reads as a liveness claim.
Worth noting the codebase already anticipated this failure mode on the other publish path. A design note in acq.backends/msb.sh explains that wrapping the backgrounded msb ssh serve would make teardown "kill the wrapper and orphan the listener with its loopback port still bound." The create-time path has now hit the same class of leak for real.
Suggested direction
Not a rewrite — a liveness cross-check or an honest label. Options, roughly in order of preference:
- Cross-check and annotate. For each create-time mapping, probe the host port and compare the listening PID's provenance to this sandbox. Emit e.g.
(create-time -p, LISTENER NOT THIS SANDBOX)or(create-time -p, declared; no host listener). - Separate declared from observed. Keep
acq portsas the config view and addacq ports --verify(or a line inacq doctor) that asserts reachability. - Minimum viable. Change the label so it does not read as a liveness guarantee, and document the discrepancy in
docs/KNOWN_FAILURE_MODES.md.
A KNOWN_FAILURE_MODES.md entry seems warranted regardless. It already documents two adjacent modes — §33 "--kit Services Dead After a Resume/Reboot on msb — Ports Mapped, Nothing Listening" and §36 "msb Create-Time Published Port Returns Empty Response for a Loopback-Only Guest Service". This is a third sibling: ports mapped, something listening, wrong owner.
Upstream boundary
The forwarder leak itself is a microsandbox defect (a host msb process surviving msb rm of the sandbox that created it) and is being reported separately. The acq-side gap tracked here is narrower and independently fixable: acq reports a mapping as established that it has not verified.
Related
- acq: allow choosing the HOST port for a published guest port at run/create time #334 — host-port selection at run/create time. Adjacent, and its "Related downstream limitation" section already describes the paseo kit's verify SKIPping when a developer's own daemon owns host
6767. Same neighbourhood (who actually owns the host port), different defect. - Detection and operator-facing docs now live in the paseo kit in
agentic-coding-patterns:scripts/paseo-verify-identitycompares the host-published port's daemon id againstacq exec's, and flags a listener older than the sandbox or from a foreignmsbbuild.
Environment
- acq with
msbbackend; hostmicrosandbox-acq 0.6.18, leaked forwarder frommicrosandbox 0.7.2 - macOS (Apple silicon), Homebrew
Activity
mogul commentedon Sep 30, 2026
Provenance of the leaked forwarder — msb version switching
The maintainer identified the likely origin: host-side testing for #510, which repeatedly upgrades and downgrades the installed
msb. The timeline and the binary paths both support it, and they sharpen this issue considerably.Timeline. The orphaned forwarder started
Wed Sep 23 16:42:03local =2026-09-23T23:42:03Z. The first commit onfix/msb-version-block-0-7-0-0-7-2is2026-09-24T01:24:56Z— so the forwarder began 1h43m before the first commit, consistent with the exploratory version-switching that preceded the branch rather than with anything after it.Binary paths are the decisive part. The two
msbprocesses involved were from different formulas:The leaked forwarder is 0.7.2 from upstream's
microsandboxformula — precisely the version range #510 exists to block — while the working one is the pinnedmicrosandbox-acq 0.6.18. Note also the different subcommands (machinevssandbox), i.e. different forwarder implementations across that version boundary.Why this makes the leak reachable in normal use. Replacing the active
msbdoes not stop forwarders already running from the previous build. The new binary has no knowledge of them, and because upstreammicrosandboxandmicrosandbox-acqboth ownbin/msb(theyconflicts_witheach other in the tap), swapping versions is exactly the operation that leaves a forwarder no surviving binary will reap.acq stop/acq rmthen cannot release the port no matter how many times they are run — which is what was observed.So this is not a freak occurrence: anyone who follows #510's own reviewer instructions (install the pinned msb, test a blocked version, roll forward or back) can end up with a stale forwarder holding a published port, then hit the misleading
acq portsoutput documented above while trying to work out why their sandbox's UI is wrong.Two implications worth considering for the fix:
msb, not just check that something is listening. A foreign-build forwarder is the specific case the lifecycle commands cannot reach, and it is cheap to detect (ps -ww -o command=vscommand -v msb, comparing installation prefixes).install.sh(and the recovery paths inacq.backends/msb.sh) are a natural place to warn about, or clean up, forwarders from the outgoing build before replacing it — the moment the leak is created is the moment it is cheapest to catch.The detector in the paseo kit (
scripts/paseo-verify-identityinagentic-coding-patterns) already implements the binary-path comparison and is verified against both real observed paths.feat(patterns): add paseo kit identity gate for a stale host forwarder (