Repository navigation
[DGX Spark][Policy&Network] recover reports the dashboard forward restored and exits 0 while an unrelated process owns the port #11149
Description
Description
When an unrelated local process already listens on a sandbox's dashboard forward port,
nemoclaw {sandbox} recover exits 0 and reports that the host forward was restored and that
launch readiness is healthy. The port is never taken over: the foreign process keeps the
listener for the whole operation, and an HTTP request to the dashboard URL is answered by the
foreign process, not by the sandbox.
The command is therefore telling the user that a forward it did not establish is ready. The
dashboard URL that NemoClaw prints carries an access token in its fragment, so a user who
follows the reported-healthy URL sends that token to whatever local process squatted the port.
Platform scope: Platform-specific not established; reproduced on DGX Spark aarch64 only, other
platforms not tested for this path.
Regression: Unknown - earlier releases were not tested for this path.
OpenShell issue: No. The failing command is NemoClaw's own recover command.
Environment
Device: NVIDIA DGX Spark
OS: Ubuntu 24.04.4 LTS
Architecture: aarch64
Kernel: 6.17.0-1014-nvidia
Node.js: v22.23.1
npm: 10.9.8
Docker: Docker version 29.2.1, build a5c7197
OpenShell CLI: openshell 0.0.106
NemoClaw: nemoclaw v0.0.120
OpenClaw: 2026.7.1
Steps to Reproduce
1. Onboard an OpenClaw sandbox with a dashboard enabled and record its forward port:
nemoclaw {sandbox} dashboard-url --quiet
2. Stop the sandbox so it releases the forward:
nemoclaw {sandbox} stop
3. Start an unrelated listener on the freed port and confirm it is serving:
python3 -m http.server {port} --bind 127.0.0.1 &
ss -ltnp | grep :{port}
curl -fsS "http://127.0.0.1:{port}/" -o /tmp/pre.html
4. Ask NemoClaw to recover the sandbox and record the exit code:
nemoclaw {sandbox} recover; echo "RECOVER_EXIT=$?"
5. Re-check who owns the port and what answers on it:
ss -ltnp | grep :{port}
curl -fsS "http://127.0.0.1:{port}/" -o /tmp/post.htmlExpected Result
Recovery exits non-zero, names the occupied or unverified-owner port, and withholds launch
readiness. The foreign listener is left running and untouched.
Actual Result
The foreign listener is correctly left untouched, but recovery reports success:
RECOVER_EXIT=0
Probe complete: OpenClaw gateway is running in {sandbox}.
... forwardAction=restored result=ready
Port ownership before and after recovery is identical and belongs to the foreign process:
LISTEN 0 5 127.0.0.1:{port} 0.0.0.0:* users:(("python3",pid={foreign pid},fd=3))
The dashboard request before and after recovery both return the same 9433-byte directory
listing produced by the foreign process, so nothing of the sandbox is reachable on the port
that recovery just declared restored.
Reproduced twice on the same host in independent runs.
Logs
Foreign listener log, showing it served both requests and was never signalled:
127.0.0.1 - - [07/Sep/2026 06:35:58] "GET / HTTP/1.1" 200 -
Process state after recovery:
PID STAT CMD
{foreign pid} S python3 -m http.server {port} --bind 127.0.0.1
Related Bugs / not duplicate of
Same area, different symptom, not a duplicate:
- [DGX Station][Policy&Network] recover reports a port-forward failure when the forward is already active #7085 — recover reported a port-forward failure when the forward was already active. That is the inverse symptom of this report and is already fixed. Worth checking whether that fix removed the occupied-port rejection path this report needs.
- [DGX Spark][CLI&UX] stop leaves the dashboard port-forward alive; status flags sandbox_dashboard_port_conflict and start/recover intermittently fail #7227 — stop left NemoClaw's own dashboard forward alive and status flagged a port conflict. That concerns a forward NemoClaw itself owns; this report concerns a listener owned by an unrelated process.
Activity
fix(recover): refuse a dashboard port held by a listener the sandbox …
Dongni-Yang commentedon Sep 8, 2026
Taking this. PR #11196 makes
recovername the occupied port, leave the foreign listener untouched, skip the relaunch, and exit 1 with the port in the probe-failure line.Context for anyone re-verifying: #11128 (merged today, not linked here) already changed the healthy check to prove listener ownership from the PID, executable and argv, so on
mainrecovery exits 1 in this scenario. It still reported the forward as "missing or dead" and tried to relaunch onto the occupied port, surfacing that launch's "already occupied" second-hand. #11196 completes the expected result stated above and adds a CLI-level test that runs the real ownership proof against a genuine foreign listener.test(recovery): fold the foreign-listener cases into the declared-por…
fix(recover): require a tracked legacy row to name the process on the…
test(recovery): keep the listed forward PID live in the relaunch fixt…
refactor(recover): classify a legacy forward row from one PID lookup (#…
docs(recover): state the nonzero exit for an unattributed dashboard l…
test(recover): refuse a stale legacy row over a real foreign listener…
docs(recover): split the unattributed-listener sentence (#11149)
fix(recover): migrate a legacy row only while OpenShell reports it ru…
fix(recover): require the OpenShell executable behind a legacy forwar…
test(recovery): stub the OpenShell resolver in the legacy-forward fix…
Merge upstream/main into fix/11149-recover-foreign-listener
refactor(recover): classify the dashboard listener once (#11149)
style(forward-service): narrow the listener PID without non-null asse…
style(test): apply the pinned formatter to the legacy forward row test (
fix(recover): refuse a dashboard port held by a listener the sandbox …