Repository navigation
feat(cli): add pair --reset to discard a stuck OpenTunnel tunnel - #55502
IgTriguero wants to merge 1 commit into
Conversation
|
The following comment was made by an LLM, it may be inaccurate: |
There was a problem hiding this comment.
This works for a stuck pending tunnel. I set one up in an isolated home with the network blocked. On the base, pair --remote --yes keeps trying to resume it and leaves the files behind. With this PR, pair --reset --remote --yes deletes them and goes straight to creating a new tunnel. The new tests pass and packages/cli type-checks.
Public API: adds the --reset flag to opencode pair. It only works together with --remote, and it's backwards compatible.
The main concern is the order of steps in pair.ts (inline comment). The reset runs before the first-time consent prompt and while the background service is still running. I reproduced the first part: when consent is refused, the command still deletes the device-wide tunnel data. The second part, from reading the code, is that the running service can write tunnel files at the same time as the reset.
Also, CONTRIBUTING asks for a maintainer's design approval before feature PRs, and #54624 doesn't have one yet.
Related: #54596
| if (input.remote && config.remote === undefined) { | ||
| if (!(yield* remoteConsent(config, input))) return | ||
| if (input.reset && !input.remote) return yield* Effect.fail(new Error("--reset requires --remote")) | ||
| if (input.reset) yield* RemoteTunnel.reset() |
There was a problem hiding this comment.
Two ordering problems here:
-
The reset runs before
remoteConsent. I ranpair --reset --remotewith no TTY and no--yes: it printed the warning and exited 1, but~/.local/share/opentunnel/defaultwas already deleted. That's the device-wide profile the opentunnel CLI and other SDK apps also use. Declining at the interactive prompt has the same effect. Running the reset after consent succeeds would fix it. -
When remote access is already on, the background service is still running here. It's only stopped later, inside
ServiceConfig.set("remote", "true"), afterRemoteTunnel.ensure()has already created the new tunnel. In the outage case this flag is for, the service keeps retryingclient.tunnel.connect. That callsensure()/create(), which either resumes the pending identity or creates a new tunnel, writing into the same profile. So the service can recreate the old state right after the deletion, or run a create at the same time as the foregroundensure(). Both writetoken/private-key.pem/pending.json, so the saved files could come from two different tunnels. Stopping the service (Service.stop(yield* ServiceConfig.options())) before resetting would avoid this.
There was a problem hiding this comment.
Both fixed in the latest push (bf1acbbfd7). The reset now runs only after first-time consent succeeds, so a refused or non-interactive run leaves the stored profile untouched — added a regression test for the case you reproduced. The handler also stops the service with Service.stop(ServiceConfig.options()) before RemoteTunnel.reset(), so it cannot recreate the pending state or race the new tunnel. Thanks for the detailed repro.
c9b7b5a to
bf1acbb
Compare
There was a problem hiding this comment.
Both concerns from my last review are fixed. The reset now runs only after first-time consent succeeds. Rerunning my earlier repro (pair --reset --remote with no TTY and no --yes) now leaves the stored profile in place. The background service is also stopped before the reset, so it can't write to the profile during it. With the network blocked, pair --reset --remote --yes still deletes a stuck pending profile and goes straight to creating a new tunnel. The 4 reset tests pass and packages/cli type-checks.
Public API: adds the --reset flag to opencode pair. It only works together with --remote, and it's backwards compatible.
No further concerns. As noted before, CONTRIBUTING asks for a maintainer's design approval before feature PRs, and #54624 doesn't have one yet.
Related: #54596
Issue for this PR
Closes #54624
Type of change
What does this PR do?
Adds
opencode pair --reset(requires--remote). It discards the stored OpenTunnel state and then runs the normal pairing flow, so a tunnel whose certificate issuance got stuck (for example during the opentunnel.xyz outage) can be replaced without deleting files under~/.local/share/opentunnel/<profile>/by hand.RemoteTunnel.reset()calls the SDK's publictunnel.remove()(server-side removal for a completed tunnel) and then removes the profile directory. The directory removal is what clears a stuck pending issuance, becauseremove()only acts on completed tunnels. The handler stops the background service first, because it keeps retrying the wedged tunnel and can write the same profile while the reset runs. The service route is kept, so the pairing subdomain survives; only the tunnel hostname changes, because a new tunnel is created.CLI-only: the flag, the handler wiring, and the small
RemoteTunnel.reset()helper inpackages/cli.How did you verify your code works?
bun test test/pair-remote-reset.test.tsinpackages/cli(4 pass): reset removes a planted stuck pending profile under an isolatedXDG_DATA_HOME; reset succeeds with no stored tunnel;pair --resetwithout--remoteexits 1; a refused first-time consent leaves the stored profile untouched.bun test --timeout 30000 test/pair-remote-consent.test.ts(5 pass) andbun typecheckinpackages/cli.bun run dev pair --helpshows the new flag:The live end-to-end path needs a real tunnel, so it is written out as a reviewer script below.
Screenshots / recordings
No visual change; CLI output is covered above.
Checklist
Reviewer test plan: reproduce the stuck pending tunnel without an outage
Setup. Tunnel data lives at
~/.local/share/opentunnel/default($XDG_DATA_HOME/opentunnel/default; Windows%USERPROFILE%\.local\share\opentunnel\default). A completed tunnel hastunnel.json,token,private-key.pem,certificate.pem,chain.pem; a pending issuance haspending.json,token,private-key.pem.ensure()resumes a pending identity instead of creating a fresh tunnel, which is the reported wedge.1. Plant an unresumable pending state
bun run dev pair --remote --yesas soon aspending.jsonappears, or write a minimalpending.json({"id":"...","hostname":"...","csr":"..."}) next to the existingtoken/private-key.pem.token(append a character) or blockopentunnel.xyz. A corrupted token fails fast withFailed to start certificate issuance; a blackholed network can take the 5-minuteensuretimeout.2. Confirm the bug (same build, no flag)
It keeps resuming the dead pending identity, fails again on every run, and leaves the files behind — the only recovery is deleting them by hand.
3. Confirm the fix
The profile is discarded first, then
Setting up remote access; this can take a minute...appears and a new tunnel is created with a fresh pairing link/QR whose hostname differs from the oldtunnel.json.4. Completed tunnel, network on
pair --reset --remote --yesremoves the old tunnel server-side, creates a new one, and restarts the service; the old remote URL stops serving.pair --remote --yeswithout the flag still reuses the existing tunnel and does not create a new one.5. Background service recovery (the original outage shape)
remote access tunnel unavailable; retryingandpair --remoteeventually times out withTimed out waiting for the remote tunnel.pair --reset --remote --yes, the service restarts with the new tunnel and the warnings stop.Edge cases
tunnel.remove()needs the API, so reset fails and keeps the local state (no server-side orphan). Offline + pending-only works, since that removal is local.Added by the opencode bot (AI-generated).
Related: #54596
Public API: adds the --reset flag to
opencode pair(requires --remote; backwards compatible)