Skip to content

Phase 2: transfer with no successor drops grants #170

Closed

Description

@dezren39

transfer with no named successor drops the removed member's grants. Documented. Consider a fallback to cascade.

Activity

  1. dezren39 commented on Oct 24, 2026

    @dezren39
    MemberAuthor

    id train triage (2026-10-24): needs-decision — project owner: transfer-with-no-successor fallback (currently drops grants; consider cascade).

  2. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · escalated

    • Agent: queue worker (repo-worker-code), Lane B shift
    • Session / ID: queue task 2017
    • Found via: queue claim
    • Task: Triage verdict for Phase 2: transfer with no successor drops grants #170 — confirmed at HEAD, escalating the semantics decision to Drew (project owner)
    • Expectation: Decision needed from Drew; no fix packet enqueued until he rules, because every option changes grant-survival semantics
    • Links: pkgs/id/src/groups/policy.rs, pkgs/id/src/groups/delegation.rs (citations below)

    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:

    1. 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.
    2. 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.
    3. 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.
    4. 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.

  4. added
    queue/blockedQueue or user escalation state
    status/blockedQueue or user escalation state
    user/escalatedQueue or user escalation state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Oct 29, 2026
  5. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    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/actions.rs:136-152 builds
      MemberOp::Remove { member, successor } — becomes { member, mode }.
    • 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).
  6. 235 remaining items

  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — state: handed off — queue task 2017 launched in bound OpenCode session ses_e5e5f3a7eceffeCmWGe1YlMoTq by opencode-claimant.

  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triaged (task 2017)

    • Verdict: confirmed on current main. pkgs/id/src/groups/delegation.rs links() 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.
    • Follow-up linkage: implementation follow-up already exists — task 3130 (RemoveMode for Phase 2: transfer with no successor drops grants #170), branch 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.
    • Outcome: triaged; issue stays OPEN pending PR groups: explicit RemoveMode for member removal (#170) #4586 resolution. No code changes in task 2017 (triage-only packet scope).
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  10. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
  11. added a commit that references this issue on Nov 3, 2026
  12. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
  13. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (task 3130, lane code-j)

    • Outcome: PR groups: explicit RemoveMode for member removal (#170) #4586 MERGED (merge sha da30751); issue Phase 2: transfer with no successor drops grants #170 auto-closed by the merge. Task 3130's outcome is fixed_merged.
    • What this shift did: resolved the PR's merge conflicts with current main (pkgs/id/src/cli.rs — main's total-memory test lines kept; pkgs/id/src/commands/serve.rs — main's id(chat-presence): receipt never scopes heartbeats to sender membership — update_scoped has no production caller, serve stores channels unscoped #4363 receive_presence_heartbeat kept; merge commit e516c0d), then fixed all three accepted Codex findings in ee688f4:
      1. 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.
      2. P2 fold.rs — admins_at_position now collects membership-op subjects, so added/promoted Admins who never authored an event are Transfer(Admin) candidates.
      3. 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions