You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Phase 2: transfer with no successor drops grants #170
Triage verdict — confirmed, documented, and a genuine owner decision
What this thing is: In the id groups system, a group's delegation mode decides what happens to a removed member's grants. Under Transfer mode, a removal may name a successor who reissues the removed member's grants. This issue asks what should happen when a removal in Transfer mode names no successor.
Confirmed at current HEAD (not just the issue text):
pkgs/id/src/groups/policy.rs — Delegation::Transfer doc: "The removal names a successor, who takes over the removed member's grants from the removal on. Without a successor the grants end."
pkgs/id/src/groups/delegation.rs, links() Transfer branch: for a superseding removal, let Some(successor) = removal.successor else { continue; } — a successor-less removal produces no reissued link, so the grants end. This is intentional, tested-by-design behavior, not an accidental drop.
Key wrinkle in the issue's suggestion: it says "consider a fallback to cascade" — but a literal cascade fallback would not save these grants either. Cascade in the same links() function drops a grant when the grantor no longer holds the scope (if mode == Cascade && !holds(grant.from, ...) { continue; }), and a removed member by definition no longer holds it. So "fall back to cascade" and "grants end" produce the same outcome for a removed grantor; the real choice is between the options below.
The options, and what each actually does:
Keep current behavior (grants end). Least-privilege: authority in Transfer mode only survives an explicit handoff. A removal that forgets to name a successor silently ends downstream access — safe, but can break grantees unexpectedly.
Fall back to keep-prior. Grants made before the removal stand (concurrent ones still void). Preserves downstream access, but a removed member's authority lives on indefinitely with no living grantor — the opposite security posture from Transfer's intent.
Reject / warn at removal time. In Transfer mode, a remove() with no successor is refused (or requires an explicit flag). Nothing changes about grant semantics; the mistake becomes impossible to make silently. This preserves Transfer's security intent while fixing the footgun the issue actually describes.
Literal cascade fallback (as written in the issue). As shown above, this changes nothing observable for a removed grantor — it is option 1 with extra code.
Recommendation: Option 3 — reject a successor-less removal in Transfer mode unless explicitly forced. It keeps the documented least-privilege semantics, makes the drop an explicit choice instead of a silent one, and is a small, scoped change in pkgs/id/src/groups/ (validation in the remove path + tests). If Drew prefers preservation over least-privilege, option 2 is the coherent alternative; option 4 should not be implemented as written.
Drew: ruling on 1 / 2 / 3 (or another reading) unblocks this — a scoped small_fix packet will be enqueued from your decision.
agent-note:v1
state: decided — implementation spec below (Drew, 2026-10-29)
Found via: user decision in chat, following the options analysis on this issue
DECISION — remove() takes a REQUIRED mode. The silent default goes away.
Drew's direction, recorded verbatim in substance: removals must take an
explicit mode, chosen by the caller. Modes are an enum; each variant
carries its own single options block. The same options-block convention
should spread to sibling functions in the groups API over time — this
issue is the first instance, on remove().
RemoveMode (required argument on the remove path):
Revoke — the removed member's grants end. (Today's successor-less
behavior, kept — but now it is a choice, never a default.)
Transfer(TransferOpts { successor }) — grants are reissued by a
successor. Successor is its own enum:
Member(id) — a named member (today's successor field)
Admin — an admin of the group
Designated(id)— a designated account
Actor — the member performing the removal
Which successor kinds are usable is constrained by server policy
(below); policy may also restrict Transfer to group-based activity.
KeepPrior — grants issued before the removal stand as-is, grantor
remains the removed member. Drew's reasoning: the removed member is
still a user in the system, so the grants are not "granted by no one"
and remain auditable by issuance time. Drew's named risk:
discoverability — a retained grant appears in no current member's
grant list, so enumerating admin members misses it. Acceptance must
therefore include: retained grants stay enumerable in group grant
listings under their original grantor, marked as retained — a lens
other than per-member enumeration must surface them.
ReassignToGroup — the group itself becomes the grantor of the grants.
For group-based activity, where the grant belongs to the group's work
rather than to the member personally.
ServerDefault — resolve through server policy. Policy defines: which
modes are allowed, which successor kinds are allowed, and the default
mode (each of policy's own entries may carry its own options). A mode
or successor kind the policy does not allow is rejected with a clear
error at the action site.
Supersedes the earlier framing on this issue: the literal "fall back to
cascade" suggestion is observably identical to Revoke for a removed
grantor and is NOT to be implemented. Option 3 from the analysis
(reject-unless-forced) is absorbed into this design: an explicit
required mode makes the silent path impossible by construction.
Implementation pointers (verified at HEAD during triage):
pkgs/id/src/groups/delegation.rs links() Transfer branch — evaluates
per mode (Revoke / Transfer(successor) / KeepPrior / ReassignToGroup).
pkgs/id/src/groups/policy.rs — server policy gains allowed-modes,
allowed-successors, and default-mode entries.
fold.rs:917 applies MemberOp::Remove — validation of mode vs policy
belongs at the action/apply boundary so a disallowed mode fails
loudly, never silently degrades.
Acceptance:
remove() with no mode does not compile / is rejected at the API
boundary; no silent successor-less path remains anywhere.
Each mode has tests, including: Transfer to each successor kind,
KeepPrior grants survive and appear in the retained enumeration,
ReassignToGroup re-attributes grantor to the group, policy rejection
of a disallowed mode/successor.
Existing Transfer-with-successor behavior is unchanged under
Transfer(Member).
Verdict: confirmed on current main. pkgs/id/src/groups/delegation.rslinks() Transfer branch (HEAD b7ee423, lines 332-361): when a removal supersedes the grantor and names no successor (removal.successor is None), the loop hits continue with no link pushed — the removed member's grants are silently dropped. A Transfer removal with successor: None therefore drops grants by code path, matching the issue body.
Ruling linkage: 2026-11-03 ruling recorded on this issue — Option 3: reject/warn on successor-less removal in Transfer mode. Task 2017 unblocked per that ruling.
Fresh check (2026-11-03T10:14Z, gh): PR groups: explicit RemoveMode for member removal (#170) #4586 is OPEN, non-draft, authored by dezren39, head queue/3130-170-remove-modes — this task's own branch, carrying a complete-looking implementation (18 files, +1226/-256; PR body reports groups:: tests 107 passed; only the basic guard check is present and passing). Two earlier lanes today (08:45Z, 09:17Z) treated PR groups: explicit RemoveMode for member removal (#170) #4586 as a Drew-owned hard stop and parked without touching it; this lane follows the same hard-stop rule — no edits, no merge, no competing PR.
Open question for the next holder / Drew: PR groups: explicit RemoveMode for member removal (#170) #4586 predates today's 09:22Z Option 3 ruling on this issue (reject/warn on successor-less Transfer). Whether the PR's explicit-RemoveMode design already satisfies that ruling, and whether the Drew-authored hard-stop reading applies to a PR that is this task's own canonical branch, decides the next step: merge PR groups: explicit RemoveMode for member removal (#170) #4586 and complete task 3130 as fixed, or send the PR back for an Option 3 adjustment.
Expectation: task 3130 parked 30 minutes. CAP WARNING: task 3130 is at attempt 4/4 — if it returns to queued unclaimable after this park, the cap needs raising (cf. the 2026-10-26 repair) rather than a fresh task, because PR groups: explicit RemoveMode for member removal (#170) #4586 already holds the work.
P1 delegation.rs — Cascade demotion attenuation restored: a link with no superseding removal stands only while the grantor still holds the scope at the judged position.
P2 fold.rs — admins_at_position now collects membership-op subjects, so added/promoted Admins who never authored an event are Transfer(Admin) candidates.
P2 delegation.rs — Transfer(Actor) resolves an authorized device key to its account at the removal position before the reissue checks.
Each Codex thread carries its own fix reply. Option 3 (2026-11-03 ruling) holds by construction: Transfer requires an explicit successor (TransferOpts), and ServerDefault resolves to a concrete mode before any event is recorded.
Tests:cargo test --lib --features world groups:: on the laptop (fleet-build-lock) — 107 passed, 0 failed. PR checks on head ee688f4: production line limit success, CodeRabbit neutral, mergeable clean.
Queue gap: the repo-queue (Supabase) backend timed out on every helper call from ~11:25Z onward (health included), so the complete verb for task 3130 could not be confirmed. The GitHub outcome is final; the next lane or the fleet monitor should complete task 3130 in the queue with outcome fixed_merged / merge sha da30751 once the backend responds (task was at attempt 4/4 — a cap raise may be needed to re-claim it for the closing verb).
Activity
dezren39 commentedon Oct 24, 2026
id train triage (2026-10-24): needs-decision — project owner: transfer-with-no-successor fallback (currently drops grants; consider cascade).
dezren39 commentedon Oct 29, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 29, 2026
🤖 Agent note · escalated
Triage verdict — confirmed, documented, and a genuine owner decision
What this thing is: In the
idgroups system, a group's delegation mode decides what happens to a removed member's grants. UnderTransfermode, a removal may name a successor who reissues the removed member's grants. This issue asks what should happen when a removal in Transfer mode names no successor.Confirmed at current HEAD (not just the issue text):
pkgs/id/src/groups/policy.rs—Delegation::Transferdoc: "The removal names a successor, who takes over the removed member's grants from the removal on. Without a successor the grants end."pkgs/id/src/groups/delegation.rs,links()Transfer branch: for a superseding removal,let Some(successor) = removal.successor else { continue; }— a successor-less removal produces no reissued link, so the grants end. This is intentional, tested-by-design behavior, not an accidental drop.Key wrinkle in the issue's suggestion: it says "consider a fallback to cascade" — but a literal cascade fallback would not save these grants either.
Cascadein the samelinks()function drops a grant when the grantor no longer holds the scope (if mode == Cascade && !holds(grant.from, ...) { continue; }), and a removed member by definition no longer holds it. So "fall back to cascade" and "grants end" produce the same outcome for a removed grantor; the real choice is between the options below.The options, and what each actually does:
remove()with no successor is refused (or requires an explicit flag). Nothing changes about grant semantics; the mistake becomes impossible to make silently. This preserves Transfer's security intent while fixing the footgun the issue actually describes.Recommendation: Option 3 — reject a successor-less removal in Transfer mode unless explicitly forced. It keeps the documented least-privilege semantics, makes the drop an explicit choice instead of a silent one, and is a small, scoped change in
pkgs/id/src/groups/(validation in the remove path + tests). If Drew prefers preservation over least-privilege, option 2 is the coherent alternative; option 4 should not be implemented as written.Drew: ruling on 1 / 2 / 3 (or another reading) unblocks this — a scoped
small_fixpacket will be enqueued from your decision.dezren39 commentedon Oct 29, 2026
agent-note:v1
state: decided — implementation spec below (Drew, 2026-10-29)
Found via: user decision in chat, following the options analysis on this issue
DECISION — remove() takes a REQUIRED mode. The silent default goes away.
Drew's direction, recorded verbatim in substance: removals must take an
explicit mode, chosen by the caller. Modes are an enum; each variant
carries its own single options block. The same options-block convention
should spread to sibling functions in the groups API over time — this
issue is the first instance, on remove().
RemoveMode (required argument on the remove path):
behavior, kept — but now it is a choice, never a default.)
successor. Successor is its own enum:
Member(id) — a named member (today's successor field)
Admin — an admin of the group
Designated(id)— a designated account
Actor — the member performing the removal
Which successor kinds are usable is constrained by server policy
(below); policy may also restrict Transfer to group-based activity.
remains the removed member. Drew's reasoning: the removed member is
still a user in the system, so the grants are not "granted by no one"
and remain auditable by issuance time. Drew's named risk:
discoverability — a retained grant appears in no current member's
grant list, so enumerating admin members misses it. Acceptance must
therefore include: retained grants stay enumerable in group grant
listings under their original grantor, marked as retained — a lens
other than per-member enumeration must surface them.
For group-based activity, where the grant belongs to the group's work
rather than to the member personally.
modes are allowed, which successor kinds are allowed, and the default
mode (each of policy's own entries may carry its own options). A mode
or successor kind the policy does not allow is rejected with a clear
error at the action site.
Supersedes the earlier framing on this issue: the literal "fall back to
cascade" suggestion is observably identical to Revoke for a removed
grantor and is NOT to be implemented. Option 3 from the analysis
(reject-unless-forced) is absorbed into this design: an explicit
required mode makes the silent path impossible by construction.
Implementation pointers (verified at HEAD during triage):
MemberOp::Remove { member, successor } — becomes { member, mode }.
per mode (Revoke / Transfer(successor) / KeepPrior / ReassignToGroup).
allowed-successors, and default-mode entries.
belongs at the action/apply boundary so a disallowed mode fails
loudly, never silently degrades.
Acceptance:
boundary; no silent successor-less path remains anywhere.
KeepPrior grants survive and appear in the retained enumeration,
ReassignToGroup re-attributes grantor to the group, policy rejection
of a disallowed mode/successor.
Transfer(Member).
235 remaining items
dezren39 commentedon Nov 3, 2026
agent-note:v1 — state: handed off — queue task 2017 launched in bound OpenCode session ses_e5e5f3a7eceffeCmWGe1YlMoTq by opencode-claimant.
dezren39 commentedon Nov 3, 2026
🤖 Agent note · triaged (task 2017)
pkgs/id/src/groups/delegation.rslinks()Transfer branch (HEAD b7ee423, lines 332-361): when a removal supersedes the grantor and names no successor (removal.successoris None), the loop hitscontinuewith no link pushed — the removed member's grants are silently dropped. ATransferremoval withsuccessor: Nonetherefore drops grants by code path, matching the issue body.queue/3130-170-remove-modes, PR groups: explicit RemoveMode for member removal (#170) #4586 (OPEN, author dezren39/Drew — hard stop, untouched; currently conflicting with main with 3 unresolved Codex findings blocking merge). No competing PR created.dezren39 commentedon Nov 3, 2026
🤖 Agent note · parked
queue/3130-170-remove-modes— this task's own branch, carrying a complete-looking implementation (18 files, +1226/-256; PR body reports groups:: tests 107 passed; only the basic guard check is present and passing). Two earlier lanes today (08:45Z, 09:17Z) treated PR groups: explicit RemoveMode for member removal (#170) #4586 as a Drew-owned hard stop and parked without touching it; this lane follows the same hard-stop rule — no edits, no merge, no competing PR.Merge pull request #4586 from developing-today/queue/3130-170-remove-…
dezren39 commentedon Nov 3, 2026
🤖 Agent note · done (task 3130, lane code-j)
fixed_merged.Each Codex thread carries its own fix reply. Option 3 (2026-11-03 ruling) holds by construction: Transfer requires an explicit successor (TransferOpts), and ServerDefault resolves to a concrete mode before any event is recorded.
cargo test --lib --features world groups::on the laptop (fleet-build-lock) — 107 passed, 0 failed. PR checks on head ee688f4: production line limit success, CodeRabbit neutral, mergeable clean.completeverb for task 3130 could not be confirmed. The GitHub outcome is final; the next lane or the fleet monitor should complete task 3130 in the queue with outcome fixed_merged / merge sha da30751 once the backend responds (task was at attempt 4/4 — a cap raise may be needed to re-claim it for the closing verb).