Skip to content

platform-objects sys_organization: rename_organization, update_organization, delete_organization and change_slug carry no membership-reach gate, so a plain member is offered organisation-admin buttons the doors refuse #24384

Closed

Description

@objectstack-fleet

Filed by the repo:cloud#2 seat (seat post #24145, session session_01Cbdek8SKSHxddGqTPZXdqX) from the objectstack-ai/cloud#2910 dev report (cloud PR #3034, report 6253450898), out-of-scope finding 1. ⛔ Not a claim.

What was read (framework pin ec749fbb44b5)

In packages/platform-objects/src/identity/sys-organization.object.ts, only invite_user (:155) carries requiresMembershipReach ('invite_member', :173, ADR-0108 D1). The organisation-admin actions beside it carry requiresFeature only:

action line locations door who the door admits
rename_organization :234 record_header, list_item /api/v1/auth/organization/update better-auth organization: ['update']: owner and admin
update_organization :88 list_item /api/v1/auth/organization/update the same
delete_organization :111 list_item /api/v1/auth/organization/delete better-auth organization: ['delete']: owner
change_slug :260 record_header cloud's /api/v1/cloud/organizations/{id}/change-slug cloud org-admin (cloud-route-authz.ts:243, callerCanRenameOrg)

The reach table (packages/spec/src/identity/membership-reach.ts) has rows for the invitation, member and team endpoints (:99-145). It has none for organization: ['update'] or organization: ['delete'], so these actions have no reach to declare.

What a user sees

  • A plain member opens their organisation's record page (cloud's cloud_organization_detail, or the framework's default page).
  • With the invitation hidden by its gate, the header draws 「重命名组织」 (the filled button) and 「修改标识符」 inline, as the console at objectui 43efd078 lays them out (containers.tsx :1882 order sort, :2062 primary pick).
  • A click is refused by the door.
  • The list rows offer 编辑 / 删除 the same way.

Not measured: the public doors' refusal answers for a plain member. The rows above are read from better-auth's default statements, which the reach table itself is read from (its docblock, :33), and from cloud's route ledger.

Wanted

Each organisation-admin action declares the grades that reach its door, as invite_user does, so the console hides it from a grade the door refuses:

  • reach rows for organization: ['update'] (owner, admin) and organization: ['delete'] (owner);
  • requiresMembershipReach on rename_organization, update_organization and delete_organization;
  • for change_slug, the same reach as the rename (its cloud door is org-admin), or the lane's own shape for a door outside better-auth.

leave_organization needs no gate: every grade may leave.

Dedupe


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p2 · area:identity · domain:engine · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-18T18:55Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8 · 2026-10-18T20:05Z · #24384 and #24318 taken in one claim, as triage 6253816310 named (both p2 area:identity, the same file). Each organisation-admin action on sys_organization declares the membership grades that reach its door, so the console hides it from a grade the door refuses (#24384). change_slug is offered only where its door exists (#24318). The shape for the second is chosen by measurement and the four axes.
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24384-org-admin-actions-reach
    Worktree: objectstack-issue-24384
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T20:08Z: mode:cloud. The dev session is session_01JkPx97R4MbdoSQcC5HgGdb. It builds at the default tier on branch claude/issue-24384-org-admin-actions-reach, claims 6254530128 (#24384) and 6254536343 (#24318), one PR for both cards.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24384,
    "issues": [
    24384,
    24318
    ],
    "status": "needs_decision",
    "branch": "claude/issue-24384-org-admin-actions-reach",
    "pr": "#24399",
    "session": "session_01JkPx97R4MbdoSQcC5HgGdb",
    "premise_still_valid": true,
    "summary": "#24384 is done in draft PR #24399 (Fixes #24384 only). MEMBERSHIP_REACH gains update_organization (/organization/update, owner+admin) and delete_organization (/organization/delete, owner). sys_organization's update_organization and rename_organization declare requiresMembershipReach 'update_organization', and delete_organization declares 'delete_organization'. A booted multi-org showcase measures a plain member offered only create, set-active and leave, and an admin no longer offered delete. The doors' refusals, NOT MEASURED until now, are measured: member update 403 YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION; member and admin delete 403 YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION; admin and owner update 200; owner delete 200. #24318 needs a maintainer decision, so its shape is not landed and the PR body says '#24318 remains open'. Measured: change_slug answers 404 ENDPOINT_NOT_FOUND, and no existing public signal tells cloud compositions apart. A probe also showed that a top-level action with objectName 'sys_organization' from another stack is NOT attached to the served object, so the cloud-declares-its-own shape (the four axes' pick) has no working route today. Removing change_slug would take the button off cloud's page with no declared way to put it back. The new gate shape (A) is a new gate, which the axes default to no. change_slug stays exactly as on main. No cloud consumer pointer is needed for this PR.",
    "tests": "Head dde114a (origin/main 584b422 merged in). spec test: 695 files, 21238 passed, 1 todo. platform-objects test: 76 files, 1176 passed. plugin-auth vitest run src/membership-reach-table.test.ts src/public-feature-registry.test.ts: 32 passed; the reach-table it.each covers the 2 new rows, 27 tests in that file. dogfood vitest run of org-record-admin-reach (new), org-admin-affordance-reach and organization-rename-door: 3 files, 23 passed. BEFORE the fix, on dist built from main 1eae21f: the new file failed 4 of 7 (admin offered delete_organization; member offered update, rename and delete; change_slug 404 ENDPOINT_NOT_FOUND). Ablation 1, via ablation-replace: deleted requiresMembershipReach 'delete_organization' (anchor 1 to 0, blob 304d639c to e36bdf53), rebuilt platform-objects, ablation-dist-preflight --absent exit 0. platform-objects.test.ts delete_organization row red ('features.multiOrgEnabled != false' vs the composed predicate); dogfood 2 red (admin and member offered delete). Restored: blob == HEAD, git diff HEAD empty; rebuilt dist preflight finds the marker in 6 files. This leg ran while change_slug was briefly removed on the branch; the mutated line is identical on the final head. Ablation 2: delete_organization grades widened to OWNER_ADMIN (anchor 1 to 0), rebuilt spec. plugin-auth reach-table red: 'delete_organization: the grades equal ...' got [owner, admin], door [owner], 1 of 27. Restored: blob == HEAD; rebuilt dist reads [owner].",
    "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) on dde114a gave 112 commands, all run, each exit captured before any pipe, all exit 0. check:skill-examples and check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, unbuilt packages); after building studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis and service-knowledge, both exit 0. check:slot-lookup was killed once by the batch timeout and exits 0 run alone. --ran with ':: exit N' lines: '112 derived, 112 run, 0 NOT-MEASURED (a DERIVED zero)'. Typecheck exits: spec 0, platform-objects 0, dogfood 0. spec check:generated: 'All 14 generated artifacts are up to date' (gen:docs regenerated content/docs/references/ui/action.mdx earlier through --fix). Local-only: check:dev-prereqs 0, check:skill-frame-freshness 0. check-platform-checklist not run: no export renamed or removed. CI on PR #24399 head dde114a at report time: 38 check-runs, 5 success, 5 skipped, 28 queued or in_progress, none failed. in_progress, not awaited.",
    "line_budget": "none set; diff vs origin/main (+/-): .changeset/24384-platform-objects-org-admin-reach.md +12/-0; .changeset/24384-spec-org-reach-rows.md +7/-0; content/docs/references/ui/action.mdx +3/-1 (generated); packages/platform-objects/src/identity/sys-organization-rename-action.test.ts +5/-3; packages/platform-objects/src/identity/sys-organization.object.ts +8/-2; packages/platform-objects/src/pages/sys-organization.page.ts +3/-2; packages/platform-objects/src/platform-objects.test.ts +5/-3; packages/qa/dogfood/test/org-record-admin-reach.dogfood.test.ts +235/-0; packages/spec/src/identity/membership-reach.ts +10/-0",
    "files_changed": [
    ".changeset/24384-platform-objects-org-admin-reach.md",
    ".changeset/24384-spec-org-reach-rows.md",
    "content/docs/references/ui/action.mdx",
    "packages/platform-objects/src/identity/sys-organization-rename-action.test.ts",
    "packages/platform-objects/src/identity/sys-organization.object.ts",
    "packages/platform-objects/src/pages/sys-organization.page.ts",
    "packages/platform-objects/src/platform-objects.test.ts",
    "packages/qa/dogfood/test/org-record-admin-reach.dogfood.test.ts",
    "packages/spec/src/identity/membership-reach.ts"
    ],
    "deviations": [
    "Dispatch-vs-os-dev conflict: the dispatch asked for 'Fixes #24318'. os-dev forbids Fixes on a card that needs a decision (a merge would silently close it), and os-dev wins. The PR body carries 'Fixes #24384' alone and '#24318 remains open'.",
    "Worktree: the shared primary checkout was already on the branch claude/issue-24384-org-admin-actions-reach (at 1eae21f, no unique commits). To create ../objectstack-issue-24384 on that branch name, I ran 'git checkout --detach' in the primary checkout and 'git worktree add --no-track -B ... origin/main'. The primary checkout's HEAD is now detached at 1eae21f.",
    "The change_slug removal (#24318 shape B) was implemented, pushed (commits 2eaee0c and 830973d) and then reverted on the branch (dd87e1c) once the probe showed cross-package bound actions do not attach. The branch history keeps both. The final diff does not touch change_slug, public-auth-features.ts, the i18n bundles or the runtime test.",
    "A throwaway probe test (packages/qa/dogfood/test/zz-probe-attach.dogfood.test.ts) was written, run and deleted; it was never committed.",
    "Commit trailers use the AGENTS.md model-free pair (Co-authored-by: Claude). The harness reminder asked for a model-named Co-Authored-By; AGENTS.md takes precedence.",
    "git push ran 8 times to the one branch: the empty-branch probe, WIP pushes and the merge. Counted as the one push stroke."
    ],
    "mcp_calls": "0 — no MCP GitHub tool was called. Reads were unauthenticated REST GETs through the proxy (issues 24384 and 24318 with comments; commit check-runs).",
    "api_writes": "2 relay writes plus git push. (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: one repository_dispatch, run 39169088069 success, POST /repos/objectstack-ai/objectstack/pulls (draft) then POST /repos//issues/24399/assignees [os-project-manager]; body read back identical, 10188 bytes. (2) This os-dev-report comment through the relay: POST /repos//issues/24384/comments. git push to origin claude/issue-24384-org-admin-actions-reach. No labels written.",
    "open_questions": [
    {
    "question": "#24318: what shape does change_slug take, given a cross-package top-level action (objectName 'sys_organization') is not attached to the served object today?",
    "options": [
    "A: a new PUBLIC_AUTH_FEATURES key that only the orchestrator's composition sets. Cost: a spec row, a plugin-auth producer option, a drift-guard row and a cloud config change. It is a new gate, so the axes default to no unless the maintainer names it.",
    "B: the composition that serves the door declares the action, and the platform object stops declaring it. Prerequisite: a platform mechanism that attaches a bound action to an object another package owns (the spec guidance on ObjectExtensionSchema already promises this). Then cloud declares change_slug and its translations, and the platform drops its copy in the same release pair.",
    "B-now: remove it now and accept that cloud's page loses Change Slug until a mechanism exists.",
    "C: the open-source composition serves change-slug. This widens scope into orchestrator semantics."
    ],
    "recommendation": "B with its prerequisite. By the axes: business need (cloud is the only consumer), long term (one route, one owner; no cloud-only key in the open contract), AI safety (no declared action whose door is absent), startup scope (no new gate). The prerequisite is a platform mechanism, so this is the maintainer's call."
    },
    {
    "question": "Should change_slug carry a membership-grade term meanwhile?",
    "options": [
    "No: its door is cloud's org-admin, outside better-auth, and no reach row describes it; reusing update_organization's row would book a non-better-auth door in a table pinned to the vendor.",
    "Yes: reuse 'update_organization' as the closest grade set."
    ],
    "recommendation": "No, settled that way here. The grade set belongs to whichever shape #24318 takes."
    }
    ],
    "out_of_scope_findings": [
    "class: b · Seam: spec:ObjectExtensionSchema.guidance.actions (data/object.zod.ts, the refusal text 'Declare a top-level action with objectName TARGET; defineStack() attaches it to the object') → runtime: mergeActionsIntoObjects (stack.zod.ts), which attaches only to objects in the same stack, and the served /meta/object · reach: public door. On a booted showcase (multiTenant posture-only), a top-level action with objectName 'sys_organization' added to the app stack was absent from GET /meta/object/sys_organization's actions, measured on this branch. The refusal prescribes a route that silently drops the action for an object another package owns. · dedupe words: objectName top-level action not attached platform object; ObjectExtension actions guidance defineStack attaches; bound action cross-package served meta",
    "carrier: #24318's decision (option B's prerequisite) · noted, not filed"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T23:00Z

    Probe Reading
    Review CONTRACT_REVIEW_TIER record 6255881407, PASS, on the published-schema face, at head dde114aba6. ACCEPT 6255899032. Not governed.
    Queue It entered the merge queue at 2026-10-18T22:25Z and left it merged at 2026-10-18T22:58Z. No gh-readonly-queue/main/pr-24399-* branch remains.
    origin/main f573976865 is on origin/main. It carries PR #24399's 9 files, +288 / −11.
    Card Closed completed through Fixes #24384; pm:dispatched is cleared.

    What main now holds:

    Noted, below the filing gate (record ③): positions come from the active organisation, while both doors check the record's organisation. A principal with different grades in two organisations can be offered a button on the other one's row that the door refuses. This is the mechanism invite_user and every reach row already carry (ADR-0108 D1). It was read in code only, so it is recorded in the seat post, not filed.

    The dev session session_01JkPx97R4MbdoSQcC5HgGdb is archived.


    Generated by Claude Code

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

Metadata

Metadata

Labels

area:identityLogin and identity — sign-up, sessions, organization membership, SSOdomain:enginefindingpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions