Skip to content

acq ports reports a create-time mapping as live when it only echoes declared config (stale forwarder holds the port) #520

Open

Description

@mogul

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:

  1. 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).
  2. Separate declared from observed. Keep acq ports as the config view and add acq ports --verify (or a line in acq doctor) that asserts reachability.
  3. 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-identity compares the host-published port's daemon id against acq exec's, and flags a listener older than the sandbox or from a foreign msb build.

Environment

  • acq with msb backend; host microsandbox-acq 0.6.18, leaked forwarder from microsandbox 0.7.2
  • macOS (Apple silicon), Homebrew

Activity

  1. mogul commented on Sep 30, 2026

    @mogul
    ContributorAuthor

    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:03 local = 2026-09-23T23:42:03Z. The first commit on fix/msb-version-block-0-7-0-0-7-2 is 2026-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 msb processes involved were from different formulas:

    leaked   PID 75399  /opt/homebrew/Cellar/microsandbox/0.7.2/libexec/msb machine --nam...
    healthy  PID 19966  /opt/homebrew/Cellar/microsandbox-acq/0.6.18/libexec/msb sandbo...
    

    The leaked forwarder is 0.7.2 from upstream's microsandbox formula — precisely the version range #510 exists to block — while the working one is the pinned microsandbox-acq 0.6.18. Note also the different subcommands (machine vs sandbox), i.e. different forwarder implementations across that version boundary.

    Why this makes the leak reachable in normal use. Replacing the active msb does not stop forwarders already running from the previous build. The new binary has no knowledge of them, and because upstream microsandbox and microsandbox-acq both own bin/msb (they conflicts_with each other in the tap), swapping versions is exactly the operation that leaves a forwarder no surviving binary will reap. acq stop/acq rm then 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 ports output documented above while trying to work out why their sandbox's UI is wrong.

    Two implications worth considering for the fix:

    1. The liveness cross-check proposed above should compare the listener's binary path against the active 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= vs command -v msb, comparing installation prefixes).
    2. The version-switch paths in install.sh (and the recovery paths in acq.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-identity in agentic-coding-patterns) already implements the binary-path comparison and is verified against both real observed paths.

  2. added theissue type on Oct 1, 2026
  3. removed
    bugSomething isn't working
    on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions