Repository navigation
Description
Implement GitHub issue #4887 in developing-today/code: id(hosted_binding): a pending delayed rebind does not reserve its target key
Summary
The delayed-rebind protocol added in #4885 (slice 2 of #3809, pkgs/id/src/hosted_binding.rs at 9d1464c05) never reserves the target key of a pending delayed rebind, and effect_due_rebind never re-checks it. During the 30-day wait, another account can bind that exact key by an ordinary ceremony; when the wait elapses, the delayed rebind still effects and apply_rebind silently overwrites the other binding's binding_by_iroh entry. Two active binding records then claim the same iroh_id, and the by-key index points at only one of them — breaking the §1.2 rule the ceremony itself enforces ("one sub per iroh_id at a time", submit_proof conflict check).
Evidence (verified this run, laptop probe at origin/main 9d1464c05)
Probe test appended in a throwaway laptop worktree (probe-2026-11-03-laneB-scan20, removed after the run; log /tmp/probe-scan20.log on the laptop):
- Account A binds key21, then
request_delayed_rebindto key22 → requestPending, bindingrebind_pending. - A third account completes an ordinary
complete_browser_ceremonywith key22 → succeeds. Nothing consulted the pending request:request_delayed_rebindchecks onlybinding_by_irohat request time (hosted_binding.rs:1269), the pending target key is inserted into no index, andsubmit_proof's conflict check (hosted_binding.rs:~997) checks onlybinding_by_iroh. - Time advances past
effective_at;effect_due_rebindwith a valid new-key countersignature → succeeds. Unlike both other paths, it has noNewKeyBoundElsewhererecheck:rebind_with_old_key_authorizationchecksbinding_by_iroh.contains_key(&new_iroh_id)at effect time (hosted_binding.rs:1414) andrequest_delayed_rebindchecks it at request time (:1269);effect_due_rebind(hosted_binding.rs:1462) checks neither that nor pending requests. apply_rebind(hosted_binding.rs:1538-1540) unconditionally doesbinding_by_iroh.remove(old)+binding_by_iroh.insert(new, this binding), overwriting the third account's index entry.
Probe outcome (asserted in the probe, all true on current main): both A's and the third account's records are Active with iroh_id = key22; binding_by_iroh[key22] names A's binding only; the third account's record is silently unindexed for its own key.
The symmetric hole also exists at request time: two different accounts can both open pending delayed rebinds to the same target key, and whichever effects second overwrites the first's index entry the same way.
Why it matters
The delayed path is the account-recovery path for a lost root key — exactly the path whose target-key ownership must be unambiguous. The 30-day wait is the security property (§4.2), but it also creates a 30-day window in which the target key is claimable by anyone: a squatter who binds the target key during the wait is not stopped at effect time, is not notified, and loses their by-key index entry without any error, while their binding record still says active. Any consumer resolving a binding by iroh_id (device authorization, grant references — §1) then reaches the wrong account's binding.
Acceptance criteria
- A pending delayed rebind reserves its target key: an ordinary ceremony (
submit_proof) naming a key that is the target of a pending rebind fails with a conflict (e.g.IrohIdBoundElsewhereor a dedicated pending-rebind conflict), and a secondrequest_delayed_rebindfrom another account naming the same target key fails withNewKeyBoundElsewhere(or equivalent). -
effect_due_rebindre-checks at effect time, before any mutation: if the target key is now bound to a different binding (or is the target of another pending request that was created earlier), it fails withNewKeyBoundElsewhereand leaves the binding, the request, and both indexes unchanged (theRebindErrorno-mutation contract). -
apply_rebindcannot silently overwrite abinding_by_irohentry owned by a different binding: it refuses (or its callers guarantee the check) when the existing entry names another binding. - Regression tests cover: ceremony-bind of a pending target key during the wait; two pending rebinds to one target key; effect after the target key was bound elsewhere (state unchanged on refusal); the existing 21 hosted_binding tests still pass (
cargo test -p id --lib hosted_binding, laptop or CI).
Distinct from: #4868 / #4871 (the §3 ceremony's consumed-challenge and pairing-code findings in the same file — different functions and defects), #3809 (the delayed-rebind implementation epic this landed under; this is a defect in that new code, found by reading it against the §1.2 conflict rules).
Context: a Jules session began this issue on 2026-11-03 and was stopped mid-flight when the owner retired the Jules service (decommission, #5098). Before starting, check for any pull request or branch that attempt may have produced (search PRs referencing #4887) and treat the issue text above as the source of truth.
House rules from the repo owner: (1) Deliver the finished work as a pull request in this run; put "Fixes #4887" in the PR description so the issue closes on merge. (2) Cover the whole set in the one PR: every instance of the same problem, and any directly related dependency updates batched in — never one PR per item or per dependency. (3) Run the relevant tests and checks before opening the PR. (4) Merging is permitted only when you are reasonably certain the PR is correct and complete (checks green, tests run, low-risk); you never have to merge — when in doubt, leave the PR open and state your confidence and what a human should check. (5) If you hit a genuine blocker (missing credentials, a decision only the owner can make), stop and say exactly what is needed in your final summary.
Launched by pm-dispatch for developing-today/code. Queue task and OpenCode session ids follow in a comment below.
Activity
developing-today commentedon Nov 3, 2026
Dispatch launch FAILED at enqueue before a queue task was created, so this dispatch never became real. Closing this tracking issue as not planned so it does not linger as an orphan (#4917).
Error: getaddrinfo ENOTFOUND nltgxorqwxhgfzseiirt.supabase.co