Repository navigation
fix(server): hand a new pairing code once the previous one was accepted - #7599
Merged
Merged
Conversation
otavio
added this pull request to stack #7601
October 17, 2026 23:42
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/pairing-pending-code-only
branch
from
October 18, 2026 20:03
79c675f to
00aa912
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/pairing-pending-code-only
branch
from
October 18, 2026 23:11
00aa912 to
bbe178d
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/pairing-pending-code-only
branch
from
October 19, 2026 00:36
bbe178d to
0d47c63
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/pairing-pending-code-only
branch
from
October 19, 2026 00:41
0d47c63 to
3c2aa31
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/pairing-pending-code-only
branch
from
October 19, 2026 00:48
3c2aa31 to
cefc124
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/pairing-pending-code-only
branch
from
October 19, 2026 01:01
cefc124 to
c4c4af5
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/pairing-pending-code-only
branch
from
October 19, 2026 01:19
c4c4af5 to
c1a4858
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
CreateDevicePairing reused the code cached for a public key for the whole code lifetime, including after the code was accepted, and then answered with the accepted status and tenant. An agent whose paired device is removed forgets its tenant and asks for a pairing again. If that happened within ten minutes of the pairing, it got the stale acceptance back, kept the tenant, authenticated, got 401 for the removed device, and started over: about 30 pairing and auth requests a second until the code expired. Only a pending code is reused now. AcceptDevicePairing now refuses a code that is no longer pending, the same not-found answer as an expired one. Without that, an accept page left open on the old code could put the removed device back in the namespace while the agent waits on its new code, which nobody accepts. The accepted code is also dropped from the cache when the new one is minted, but that alone does not close the hole: an accept already in flight on the old code writes it back after the drop. Refusing at accept time holds whichever side wins. Re-submitting an accepted code, a double click or a retry, now answers not found instead of succeeding again. The console's accept page resolves the code first, and the resolve used to answer for a pairing code whatever its status, so a reloaded or second-tab accept page offered the namespace picker and then failed with a generic error. The resolve now refuses a pairing code that is no longer pending too, and the page shows its usual "invalid or has expired" message. Dropping the accepted code made the accepted-device lookup load-bearing. Any lookup error used to read as "no accepted device", which was harmless while the cached code still answered accepted. Now it would discard a pairing that went through: an agent restarted before saving its tenant would be handed a fresh code nobody will accept, and it never asks again. Only a not-found result moves on to the cache; any other store error is returned, so the agent exits and retries. For the same reason, an accepted cached code triggers a second lookup before it is dropped: an accept can land between the first lookup and the cache read, as when `shellhub-agent login` runs while the daemon waits on the same code, and dropping it then would leave neither process with the tenant.
otavio
force-pushed
the
fix/pairing-pending-code-only
branch
from
October 19, 2026 01:27
c1a4858 to
bb1b31d
Compare
Member
Author
|
/review |
|
Claude finished @otavio's task in 3m 8s —— View job Code Review CompleteReviewed 4 files for Standards (repo rules, language conventions, tests, code smells) and Correctness (security and runtime defects). The Spec axis did not run: the PR closes no issue and its commit has no Notes
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An agent whose paired device is removed gets a 401, forgets its tenant and asks for a pairing again
(66eaaa8). For the ten minutes the first code stays cached, the server handed back that code's
acceptedstatus and tenant. The agent persisted the tenant, authenticated, got 401 for the removeddevice, and started over. It never showed a new code and kept the server busy with about 30
pairing and auth requests a second until the cached code expired.
Accept now refuses a code that is no longer pending. Otherwise an accept page left open on the old
code could put the removed device back while the agent waits on its new code. Dropping the old code
alone doesn't close that, because an accept already in flight writes it back.
A store error during the accepted-device lookup is now returned instead of read as "not accepted",
so it can't discard a pairing that went through. An accepted cached code is checked against the store
again before it is dropped, in case its accept landed after the first lookup.
Evidence
TestCreateDevicePairing/mints_a_new_code_when_the_public_key's_code_was_already_acceptedfails with
expected: "pending" actual: "accepted". In the e2e stacked on this PR, the stack logshows 607
POST /api/devices/pairingin 20 s, each followed by a 401 on/api/devices/auth.After: the unit test passes, and the agent prints a second pairing code, is accepted and
connects again (
TestRemovedPairedDeviceAgent/an_agent_that_learned_its_tenant_from_pairing_pairs_again).Merge Danger
Door: two-way
Blast Radius: pairing
An agent that restarts after its code was accepted no longer gets the cached acceptance. It still
gets
accepted+ tenant from the device lookup that runs first, as long as the device is accepted.Re-submitting an accepted code (a double click, a retry) now answers 404 instead of succeeding
again. In the console, reloading the accept page after a pairing was accepted, or opening it in a
second tab, now shows "This code is invalid or has expired" instead of the namespace picker.