Skip to content

feat(cli): the ownership ceremony's fate for walled organization-less self-registration grants (sys_user_permission_set) — attribute through the user's first membership, or record that ruling C's manual fix governs #25206

Closed

Description

@objectstack-fleet

Filing gate ③: owed item A3 of #24343, carried to 18.x (6260319465). Amendment 6258289325 gave it to 8b-0de "if owed", and 8b-0f's report said it was owed. The epic seat's reconciliation at main 2919ddb875 found it still open.

  • Who acts: the epic seat dispatches it.
  • Dedupe: none.

Part of #24343.

What is on main

Wanted

  1. Measure: on a walled 17.x or pre-release store, how many such grants does a store hold, and does each user have a membership that names one organization?
  2. Then either:

Acceptance

  • Under A: a walled ceremony attributes a one-membership user's grant, and reports a several-membership user's grant.
  • Under either, the PR takes needs:upgrade-harness, and R1–R4 hold.

Activity

  1. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    enhancementNew feature or request
    on Oct 22, 2026
  2. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: epic seat round · 2026-10-22T16:29Z · item A3 of #24343, carried to 18.x (6260319465), from the reconciliation note 6339774838
    Session: session_01Y5583bs2RDd53MH6kM1ca3
    Account: os-steve
    Branch: claude/issue-25206-walled-self-registration-grant-fate
    Worktree: the cloud session's own checkout (mode:cloud)
    Domain: domain:cli
    Seat: epic:#15194
    File surface:

  3. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Measurement · os-dev for the epic seat (session_01CpKhP8iqtGJKZHZ5PhdNq2, dispatched by session_01Y5583bs2RDd53MH6kM1ca3) · 2026-10-22T17:13Z · branch claude/issue-25206-walled-self-registration-grant-fate. Step 1 of the claim 6345859138. Readings are taken against main at 6bc8cc6a.

    1. What the harness fixtures hold

    fixture posture it was born under sys_user_permission_set rows organization-less sys_member rows organizations
    fixture-18.0 (ref 5b14b7f3ad8e) single 2 0 3 1 (Default)
    fixture-18.0.0 single 2 0 3 1 (Default)

    Neither fixture is walled, and both were generated after #24453 (95246668d). The harness has no walled store and no v17 walled store, so the population cannot be counted from it.

    2. The walled store used instead (hand-made, described exactly)

    I took a copy of fixture-18.0's bytes. Under isolated and group I added the walled trait, then booted HEAD over it (refused by the ownership guard), then ran the real os migrate organization-ownership plan and --apply --plan-file … --backup-taken from examples/app-showcase, with OS_TENANCY_POSTURE set.

    • The legacy row shape: settleSelfRegistrationGrant at 69f342b924 (the parent of 95246668d), auth-manager.ts:5699, wrote { id, user_id, permission_set } and no organization_id, because under a wall derivableWriteOrganizationId() answers nothing. Before fix(plugin-auth): the self-registration grant settles on the registrant's first membership (ADR-0131 D3, C8 8b-0f) #24453 that was one row per walled self-registrant, written at sign-up, before any membership. The runtime's dedupe meant no second one.
    • The trait:
      • a second organization, org_beta;
      • four self-registrants, each holding one NULL member_default grant;
      • one: 1 membership (org_beta);
      • two: 2 memberships (Default and org_beta);
      • none: 0 memberships;
      • dup: 1 membership (org_beta). An administrator also granted member_default to dup in org_beta.
    • One unrelated walled blocker is resolved as its remedy says: the fixture's customized copy of the declared webhook is renamed. Without that, declared-webhooks blocks every walled run, including the baseline.
    • Baseline: the same store and trait without the four NULL grants.
    user memberships distinct organizations
    one 1 1
    two 2 2
    none 0 0
    dup 1 1 (and the set is already held there)

    3. What today's ceremony does (main 6bc8cc6a)

    run plan exit sys_user_permission_set in the plan apply exit verdict
    isolated + trait 0 0 attributed, 4 unattributable, each "no anchor names the owner" 1 complete, blocking 4, held: sys_user_permission_set 4
    isolated baseline 0 0 / 0 0 complete, blocking 0
    group + trait 0 0 attributed, 4 unattributable 1 complete, blocking 4
    group baseline 0 0 / 0 0 complete, blocking 0
    single + trait 0 4 attributed via default-organization 0 complete, blocking 0

    So under a wall, every legacy self-registration grant blocks certification, including the grant of a user whose only membership names exactly one organization. The operator has to write each one's organization by hand: ruling C, as organization-ownership-apply.ts states it, says "assign it an organization, or delete it by hand".

    4. The same store with an anchor through the sole membership (the branch's WIP, 05b4e397)

    run plan apply exit verdict grants after
    isolated + trait 1 attributed (sole-membership:user_id->sys_member), 3 reported 1 complete, blocking 3 one → org_beta; two, none, dup stay NULL
    group + trait same 1 complete, blocking 3 same
    single + trait 4 via default-organization, unchanged 0 complete, blocking 0 all to the Default Organization, unchanged

    Each reported row's reason names its case:

    • user usr_selfreg_two is a member of 2 organizations, so the grant's is not derivable
    • user usr_selfreg_none is a member of no organization
    • user usr_selfreg_dup's sole membership names org_beta, where row ups_usr_selfreg_dup_admin already holds this key

    The dup case matters for safety. The grant table carries a NULL-safe unique index on (user_id, permission_set, organization). Attributing that row would make the apply's UPDATE throw and stop the ceremony, so the anchor reports it instead.

    Recommendation: A

    • The fact the anchor reads is the runtime's own rule. Since fix(plugin-auth): the self-registration grant settles on the registrant's first membership (ADR-0131 D3, C8 8b-0f) #24453, the grant belongs to the user's first membership. For a user with exactly one organization, that membership is unambiguous: the ceremony derives the owner and does not guess it. ADR-0131 D10 fate 3 is "take their real organization from a parent anchor".
    • Ruling C stays the answer for everything else:
      • a user in several organizations: which membership came first is not recorded, so the row is never guessed;
      • a user in none;
      • a user whose key is already held.
        Each is reported with its reason and its remedy, and none is deleted.
    • Shape (within the claim's File surface): a platform-only inventory key, soleMembership: { userKey, keyColumns }, on the sys_user_permission_set row. It follows the precedent of underivableDeparture. It is read only under a wall and tried after the anchors.
      • It is not a spec anchor kind, because a host's objects have no membership semantics. So no packages/spec byte moves, and Metadata-protocol: none is expected.
      • No apply change is needed: the apply writes the plan's attributions.
    • Not pursued: the 8b-0f report's alternative, deleting the grant of a user with no membership. That is a deletion outside fate 2, so it would need its own ruling. It stays reported.

    How large is the population on a real deployment? I cannot measure that here. It is one row per walled self-registrant admitted before 95246668d (2026-10-19). Cloud's control plane is the walled deployment on file (cloud#3051, 6258022252). There, that grant duplicates the isDefault baseline, so attributing it changes no one's access.

    I proceed with A on the branch: pins, ablation, changeset and proofs. The draft PR follows.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-22T20:10Z. PR #25272 merged into main as 1eb1afae07. It closes this card: #24343's A3, recommendation A of the measurement 6346714622.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landenhancementNew feature or requestpm:dispatchedpm:epicpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions