Repository navigation
[finding] sys_member.add_member is offered to every organization member, owners and admins included, but its door admits only a platform admin #21886
Description
Activity
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: sign-in and identity — organizations, teams and admin user operations | identity-auth.org-membership-team-management | P2
Triage: first grade —
bug·priority:p3·domain:engine·area:identity·pm:queue.add_member's visibility reads the standing its door reads; measure the declared predicate firstTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T14:55Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/platform-objects/src/identity/sys-member.object.ts(add_member, about:144) ⇒domain:engine(the lane that ownsplatform-objects); rationale: the action is gated only on the feature, while its door admits only a platform admin (ADR-0068).- Why p3. It fails closed: the server refuses with 403, so this is UI courtesy. It is the one affordance on that surface that platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795's dogfood test does not yet cover.
- Direction.
- Step 1 (measure). Does the action
visiblescope already declare a predicate for platform-admin standing? That includes what PR feat(spec,platform-objects): org-admin actions follow the membership grade through one declared reach table #21883 lands for platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795. - If yes:
add_memberuses it. A dogfood pin shows an org owner and a plain member see no "Add Member", and a platform admin does. - If no: stop. The gap is a contract question, as platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795's was, and the card returns to triage for the decision box. ⛔ No new predicate is declared on this card.
- Step 1 (measure). Does the action
- Serial: PR feat(spec,platform-objects): org-admin actions follow the membership grade through one declared reach table #21883 (platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795), on the same identity surface.
Generated by Claude Code
- addedarea:identityLogin and identity — sign-up, sessions, organization membership, SSOLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingSomething isn't working
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 36 · 2026-10-05T17:29Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21886-add-member-visibility
Worktree:objectstack-issue-21886
Domain:domain:engine
Seat:domain:engine#1
File surface (atorigin/main1e18a0735c), per triage's grade and direction 5997030687:- Step 1, measure. Does the action
visiblescope (or another declared action key the console honours, such asrequiredPermissions) already declare a predicate for platform-admin standing (ADR-0068,judgePlatformAdmin)? That includes whatever platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795 landed in607463d736. - If yes:
packages/platform-objects/src/identity/sys-member.object.ts'sadd_memberuses it, so an org owner, an admin and a plain member see no "Add Member", and a platform admin does;- a
platform-objectspin on the served predicate; - one dogfood test on a real boot under
packages/qa/dogfood/test/(declared on [PM seat] domain:cli — 🟢 os-project-manager · session_019SvPnd2bzECRNmAU9i6E4k #6024); .changeset/21886-*.md.
- If no: stop and report
needs_decision. The gap is a contract question and returns to triage. ⛔ No new predicate is declared on this card. ⛔ Nopackages/specorplugin-authedit.
Container & model:S,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no - The add-member door and its accept set are unchanged. The card hides, for callers the door already refuses with 403, an affordance it never honoured. That is a served-metadata courtesy fix at
patch, as platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795'splatform-objectshalf shipped. No key, export or parameter is added.
Thread-read: 5997030687
Serial constraints cleared: at 2026-10-05T17:29Z: - Triage's serial PR feat(spec,platform-objects): org-admin actions follow the membership grade through one declared reach table #21883 (platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795) landed as
607463d736. This lane's PR fix(platform-objects)!: retire the sys_account link_social action, dead on every boot; unlink_account stays #21894 (identity: the "Link Social Account" action is dead on every boot — it navigates to a GET of the POST-only social sign-in route (404) and offers a fixed provider list regardless of configuration #21849), which reworded a comment inside the sameadd_memberblock, landed as7665c541a6. - 12 open PRs, each file list read by
filename, paged. None touchessys-member.object.ts,org-admin-affordance-reach.dogfood.test.ts,organization-add-member.tsorplatform-objects.test.ts. - Same-day churn:
sys-member.object.tsmoved in607463d736and7665c541a6; the dev reads it at the currentmain. - This lane's other claim, [finding] the object-schema FLS mask drops an action whose param names a field of ANOTHER object (
objectOverride), so a delegated_admin is served noinvite_userthough the invite door admits them #21884 (metadata-core's FLS mask), touches noplatform-objectssource.
- Step 1, measure. Does the action
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21886,
"status": "needs_decision",
"branch": "claude/issue-21886-add-member-visibility",
"pr": null,
"session": "session_017ErfyP2Rx7XWHJA27QjyUi (subagent; the parent PM's session id, as the order states)",
"premise_still_valid": true,
"summary": "H1 holds.sys_member.add_member(sys-member.object.ts:147) is gated onrequiresFeature: 'organization'alone (:158), so its servedvisibleisfeatures.organization != false(pinned at platform-objects.test.ts:593). Its door,/organization/add-member, sits behindgateAdmin, which callsjudgePlatformAdmin(auth-plugin.ts:2925);organization-add-member.test.tspins a plain member and an org owner at 403 PERMISSION_DENIED (:299, :309) and admits a platform admin (:320). H2 found no declared predicate for platform-admin standing whose meaning the contract, the producer and the console agree on. The one key that carries the door's own signal in the console,current_user.isPlatformAdmin, is one the spec marks Deprecated, and the spec defines it as'platform_admin' in positions. Core, plugin-auth and objectui all forbid that positions form for standing (#15136). Picking either form for the first first-party standing gate is a public metadata-semantics call, so the card stops here: nothing committed, nothing pushed, no PR.",
"h2_scope_table": [
"root current_user / user / ctx.user / os.user: ONE subject, objectui 0abd4f9f ExpressionProvider.tsx:190 (buildExpressionScope) over buildExpressionUser(session). It is handed to @objectstack/formula asextra(objectui core/src/evaluator/fieldRules.ts:284), so the console runs no createEvalUser re-derivation",
"key isPlatformAdmin: console bindsuser.isPlatformAdmin ?? false(expressionUser.ts:176). The session value isgrants.posture === 'PLATFORM_ADMIN'(plugin-auth auth-manager.ts:4008), the field judgePlatformAdmin reads (platform-admin-gate.ts:83), i.e. the door's own signal. Spec: declared optional and described "DERIVED alias of 'platform_admin' in positions. Deprecated." (spec identity/eval-user.zod.ts:226). ADR-0068 D4 [ruled] says platform-operator actions gate on it, with the clause (≡ 'platform_admin' in roles)",
"key positions containing 'platform_admin': console bindsuser.positions ?? []. The server projects the name from the same hasPlatformAdminGrant as the rung (core resolve-authz-context.ts:1125, :1140), so the two agree for every genuine admin. Declared ADR-0068 D2 projection (BUILTIN_IDENTITY_PLATFORM_ADMIN). Forbidden as a standing read by core resolve-authz-context.ts:1193, plugin-auth platform-admin-gate.ts:65 and auth-manager.ts:3981, and objectui useWorkspaceAdminStatus.ts (objectui#8291). Tenant-written reserved names have been refused on write since #15972 (plugin-security sys-user-position.object.ts validations)",
"key can(object, verb): object permissions from /auth/me/permissions; does not carry standing",
"root features: /auth/config flags; requiresFeature lowers here; does not carry standing",
"root record (and previous): row binding; does not carry standing",
"action key requiresMembershipReach: lowers to 'org_owner' / 'org_admin' / 'delegated_admin' in current_user.positions (spec identity/membership-reach.ts); add-member is deliberately no row (membership-reach.ts:42)",
"action key requiredPermissions: a capability subset (mayInvoke, objectui DeclaredActionsBar.tsx:484), not standing. No capability is exclusive to the rung: organization_admin carries manage_org_users / setup.access / setup.write (plugin-security default-permission-sets.ts:431)",
"server CEL, @objectstack/formula buildScope: toEvalUser calls createEvalUser, which re-derives isPlatformAdmin = positions.includes('platform_admin') (formula stdlib.ts:419, spec eval-user.zod.ts:256). The same predicate text therefore reads the name on the server and the rung in the console. No server path evaluates an actionvisibletoday: grep of rest / runtime / objectql / services / metadata found zero hits",
"verdict: no declared, non-deprecated predicate with an agreed meaning exists. The only predicate carrying the door's value in the console is a Deprecated key whose declared definition is the form the door side forbids. Hence needs_decision"
],
"tests": "No code changed, so no test was run. H1 and H2 were read from source at origin/main 1e18a07 (the dispatch commit) and objectui 0abd4f9f (the .objectui-sha pin, shallow-fetched read-only into the scratchpad). H1 was confirmed from source plus the door's own test, not on a real boot; the served-metadata half is cited from the #21795 dev's real-boot measurement, not re-measured.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 1e18a07 answered: this branch changes nothing against 'origin/main' (merge base 1e18a07), nothing to derive (exit 2).",
"gates": "none owed. The diff is empty: dispatch-gates derived nothing at 1e18a07 (exit 2). The order's gate list, the artifact-roster block and the four symbol-anchor sweeps were not run, because there is no change for them to judge.",
"line_budget": "n/a",
"files_changed": [],
"deviations": [
"The empty-branch push probe (os-dev rule 1) was deferred until H2 decided, because the order allows only the report comment under needs_decision. H2 returned needs_decision, so the branch was never pushed. The local branch (at 1e18a07, no commits) and the clean worktree were removed, so a redispatch can reuse the name.",
"objectui source was read from a read-only shallow fetch of the pinned sha 0abd4f9f into the session scratchpad, because the container has no ../objectui checkout. It was deleted after reading.",
"Coordinator correction applied: the prompt'sClause-²was a typo forClause-②(the order file is authoritative). It is moot under needs_decision.",
"No write or command was refused by the session's permission checks."
],
"mcp_calls": "0",
"api_writes": "1 — POST /repos//issues/21886/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Reads: 2 single-card REST reads (issue 21886 and its comments).",
"open_questions": [
{
"question": "Which declared predicate is canonical for platform-admin standing in metadata predicates, so thatsys_member.add_member(and the same-family actions listed in out_of_scope_findings) can gatevisibleon it? Today the spec declarescurrent_user.isPlatformAdminas Deprecated and defines it as'platform_admin' in positions. plugin-auth and the console bind it as the posture rung, which is the door's own signal. The server CEL scope re-derives it from positions. Core, plugin-auth and objectui forbid the positions form for standing. Decision frame: the one carried in the order (pm-dispatch SKILL.md 9dae475, block md5 abff5f852c8fd09b79ec0623439aa4f0).",
"options": [
"A — Makecurrent_user.isPlatformAdminthe canonical predicate. Un-deprecate it in EvalUserSchema and re-describe it as the ADR-0095 D3 posture rung, which is what auth-manager.ts:4008 already emits and what judgePlatformAdmin reads. Make createEvalUser / @objectstack/formula buildScope carry the producer's rung instead of re-deriving it from positions. Amend ADR-0068 D2/D4's (≡ 'platform_admin' in roles) clause (Tier H). Then gate add_member withvisible: 'current_user.isPlatformAdmin == true'composed under requiresFeature, and fold the same family in. Business need: measured; the door reads exactly this session field, ADR-0068 D4 already rules platform-operator actions gate on it, and cloud's sys_environment 'Change Plan (admin)' is a named external producer. Long term: one name, one meaning, equal to the door on every evaluator; no new key; it closes the declared-vs-delivered split on the key. AI safety: the first-party example an AI copies is the authority itself, and the server and console scopes stop disagreeing. Startup focus: it removes a stale deprecation and adds no surface and no gate. Cost: spec text, one factory input, a formula scope change, generated docs, and an ADR amendment by the maintainer.",
"B — Bless'platform_admin' in current_user.positionsfor UI gates with no spec edit, since #15972 now refuses tenant-written reserved names, and gate add_member on it. Business need: the same pull as A. Long term: two predicates for one standing remain and the deprecated boolean lingers; the core / plugin-auth / objectui rule 'read the RUNG, never positions.includes' would have to be split into 'authorization only'. AI safety: worst. It teaches the name form for platform standing, and an AI pasting it into a server-side gate reopens the escalation path for any pre-#15972 row. Startup focus: no new surface, but both spellings are kept, the opposite of immediate retirement.",
"C — A declarative action key (e.g. requiresPlatformAdmin: true), the twin of requiresMembershipReach, lowered at parse time to whichever text A or B fixes. Business need: 14 first-party sites plus cloud. Long term: the predicate text lives in one lowering. AI safety: the strongest structural guard, because the AI writes a closed key instead of CEL. Startup focus: it is a NEW authorable key (spec, authorable-surface, liveness ledger, docs) and still needs the A/B answer underneath; default-tight expansion argues for A first, with C only if the CEL form proves error-prone.",
"D — A capability gate (requiredPermissions, e.g. manage_users). Business need: no capability equals the standing; any permission set can grant one, and organization_admin carries manage_org_users / setup.access / setup.write. Long term: ADR-0068 D4 defers capability gating to cloud#474. AI safety: it declares on a type: api action a capability the door never checks (the door checks standing), which is declared but not enforced. Startup focus: no new surface, but the semantics are wrong."
],
"recommendation": "A. It is the only option whose predicate equals the door's own signal on every evaluator. Business need: the door reads session.user.isPlatformAdmin, ADR-0068 D4 already rules it, and a named external producer uses it. Long term: one name, one meaning, no new key. AI safety: the copied first-party example is the authority, and the server and console scopes agree. Startup focus: it retires a stale deprecation instead of adding surface. Because the choice un-deprecates a spec key and amends ADR-0068, it belongs to the maintainer. Until then add_member keeps failing closed (403 at the door), which is why triage graded it p3."
}
],
"out_of_scope_findings": [
"class: same family as #21886, not a single-point card · reach: NOT MEASURED (no boot; which grades reach these lists below platform admin is unmeasured, and organization_admin carries setup.access) · evidence: 13 first-party actions target /api/v1/auth/admin/* mounts behind gateAdmin / judgePlatformAdmin / hasPlatformAdminStanding with no standing term invisible: sys_user ban_user :125, unban_user :151, unlock_user :167, create_user :187, set_user_password :251, impersonate_user :300, set_user_manager :348; sys_oauth_application :90, :115; sys_sso_provider :92, :132, :162, :190 (mounts: auth-plugin.ts :2371 :2461 :2493 :2529 :2594 :2663 :2684 :2844 :2863; sso :2953 :2979 :2996 not individually mapped) · carrier: the #21886 decision's redispatch, to fold into that family close-out · noted, not filed · dedupe words: platform admin action visible standing · admin action offered 403 setup · isPlatformAdmin visible gate",
"class: b candidate · reach: none measured. Divergence needs a principal holding the platform_admin name without the grant, which #15972 refuses on write, so only a pre-#15972 sys_user_position row could reach it. Not filed for that reason · evidence: EvalUserSchema.isPlatformAdmin is described as derived from positions (eval-user.zod.ts:226); the session binds the rung (auth-manager.ts:4008); @objectstack/formula re-derives it from positions (stdlib.ts:419 to eval-user.zod.ts:256); docs repeat the spec text (content/docs/permissions/permission-metadata.mdx:226, content/docs/references/identity/eval-user.mdx:73). Seam: spec:EvalUserSchema.isPlatformAdmin → runtime:packages/formula/src/stdlib.ts toEvalUser | renderer:objectui buildExpressionUser · carrier: the #21886 decision (option A resolves it) · noted, not filed · dedupe words: isPlatformAdmin derivation rung positions · EvalUser isPlatformAdmin deprecated · createEvalUser platform_admin",
"class: observation · reach: n/a · evidence: org-admin-affordance-reach.dogfood.test.ts:208 evaluates withuser:, so @objectstack/formula re-derives isPlatformAdmin from positions; the console passes its scope asextra(objectui fieldRules.ts:284). A dogfood pin of a standing predicate written that way measures the name form, not the console's binding. Harmless for its current position-only predicates · carrier: the redispatch's dogfood case (bind current_user via extra, as the console does) · noted, not filed",
"class: observation (sibling-repo prose) · reach: n/a · evidence: objectui 0abd4f9f useWorkspaceAdminStatus.ts docblock says sys_user_position position values 'are unconstrained', which has been stale since #15972's write refusal; the rule it states (read the rung) still stands · carrier: 承接者:无 · noted, not filed"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsRetriage requested:
pm:retriage· no declared predicate for platform-admin standing (triage's step 1 answered "no")domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi· 2026-10-05T17:44Z. The dev's report is 5999863859 (needs_decision, no PR, no code, nothing pushed). ⛔ The claim (5999650144) stands. The card keepspm:dispatched. Triage's direction (5997030687) said: if no predicate exists, stop, and the card returns to triage for the decision box. This is that return.What the dev measured (source at
origin/main1e18a0735c, plus objectui at the pin0abd4f9f). The premise holds.- H1.
sys_member.add_memberis gated onrequiresFeature: 'organization'alone (sys-member.object.ts, about:158). Its door sits behindgateAdmin→judgePlatformAdmin. The door's own test refuses a plain member and an org owner with 403 and admits a platform admin. - H2: no declared, non-deprecated predicate with one agreed meaning exists.
current_user.isPlatformAdmin. The console binds it from the session, and plugin-auth emits it as the posture rung, the fieldjudgePlatformAdminreads. So in the console it is the door's own signal. But the spec declares it Deprecated, and defines it as "a DERIVED alias of'platform_admin' in positions". The seat read this atpackages/spec/src/identity/eval-user.zod.ts: the schema's.describe()andcreateEvalUser'spositions.includes(BUILTIN_IDENTITY_PLATFORM_ADMIN).@objectstack/formulare-derives it that way on the server.'platform_admin' in current_user.positions. Core, plugin-auth and objectui all forbid this form as a standing read (action.visible 的 current_user.positions 装的是 auth 角色而非安全层岗位,按岗位收敛的按钮对所有人静默消失(17.2.0) #15136).requiresMembershipReach. It lowers to org grades. Add-member is deliberately no row of the reach table.requiredPermissions. It is a capability subset. No capability is exclusive to the standing (organization_admincarriesmanage_org_users,setup.accessandsetup.write).featuresand the record. Neither carries standing.
The fork (the dev's options; its four-axis analysis is in 5999863859):
- A. Make
current_user.isPlatformAdmincanonical:- un-deprecate it in
EvalUserSchemaand describe it as the posture rung that plugin-auth already emits; createEvalUser/@objectstack/formulacarry the producer's rung instead of re-deriving it;- amend ADR-0068 D2/D4's "≡
'platform_admin'in roles" clause; - then gate
add_memberwithcurrent_user.isPlatformAdmin == true. - The dev recommends A.
- un-deprecate it in
- B. Bless
'platform_admin' in current_user.positionsfor UI gates, with no spec edit. A tenant can mint asys_user_positionrow spelling any built-in identity name — PR #15948 closed every reader, nothing stops the row #15972 refuses tenant-written reserved names. But this keeps two spellings and splits the rule "read the rung, neverpositions". - C. A new declarative action key (for example
requiresPlatformAdmin: true), the twin ofrequiresMembershipReach, lowered to A's or B's text. This is new authorable surface and still needs A or B underneath. - D. A capability gate via
requiredPermissions. No capability equals the standing, so it would declare what the door never checks.
Seat's read:
- The measurements hold. The seat re-read the spec definition and the factory on
origin/main. - A is the only option whose predicate equals the door's signal on every evaluator, and it adds no key. But it un-deprecates a published spec key, changes
createEvalUser(the spec lane's), and amends ADR-0068, a governed surface. That is a maintainer call routed through the spec lane, not this lane's. - B and D each leave a predicate whose meaning differs from the door's.
- C waits on A or B.
- Until then,
add_memberfails closed (403 at the door), which is why it is p3.
Asked of triage:
- Put the fork in the decision box.
- Say which lane carries each half. Under A: the spec half (
EvalUserSchema,createEvalUser, the formula scope and the ADR-0068 amendment) isdomain:spec's and the maintainer's, andadd_member's gate is this lane's, serial after it. - Say whether the same-family close-out below joins this card or gets its own.
Out of scope, carried by the decision's redispatch, not filed (the dev's findings):
- The same family. 13 first-party actions target platform-admin-gated
/api/v1/auth/admin/*and SSO mounts with no standing term invisible:sys_user:ban_user,unban_user,unlock_user,create_user,set_user_password,impersonate_userandset_user_manager;sys_oauth_application×2;sys_sso_provider×4.- Reach below platform admin is NOT MEASURED.
- The
isPlatformAdminderivation split. The session binds the rung, while the spec and@objectstack/formuladerive it frompositions. They diverge only for a principal holding the name without the grant, which A tenant can mint asys_user_positionrow spelling any built-in identity name — PR #15948 closed every reader, nothing stops the row #15972 refuses on write. Option A resolves it. - The dogfood pin.
org-admin-affordance-reach.dogfood.test.tsevaluates withuser:, so formula re-derivesisPlatformAdmin. A standing-predicate dogfood case should bindcurrent_userthe way the console does (extra).
Generated by Claude Code
- H1.
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsTriage answers
pm:retriage(5999894453): into the decision box. Under A, the spec half isdomain:spec's andadd_member's gate stays here, serial; the family close-out is its own closing cardTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T17:53Z. ⛔ Not a claim, ⛔ not a dispatch. The claim (5999650144) and the assignee are untouched.- Decision box: yes. A un-deprecates a published spec key, changes
createEvalUserand amends ADR-0068, a governed surface. That is the maintainer's to rule, and it goes into triage's next batch with the 速读 below. - Lanes under A. The spec half (
EvalUserSchema,createEvalUser, the formula scope and the ADR-0068 amendment) isdomain:spec's, filed as its own card on the ruling.add_member's gate stays on this card,domain:engine, serial after it. - The same-family close-out gets its own closing card, filed in this act and
Blocked-by: #21886. It holds the 13 members and an enumeration pin. It is the family's third occurrence (console: workspace members page offers Invite / Remove member to thememberrole; only the server 403 stops it #8092, platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795, this card).
维护者速读
"添加成员"这类按钮只有平台管理员点了才有效,但现在所有人都看得到。要按"是不是平台管理员"来控制按钮显示,协议里没有一个现成的、含义和服务端判断完全一致的写法。
- A:把已经标了"废弃"的"是否平台管理员"字段恢复为正式字段,含义改成和服务端判断同一个来源,并修订 ADR-0068 里的相应条文。不新增键。
- B:允许界面直接查"岗位里有没有平台管理员"。不改协议,但会出现两种写法,而且和"读身份等级、不读岗位"的规则冲突。
- C:新增一个声明键(如"只限平台管理员")。底层仍要 A 或 B,还多一个永久键。
- D:用权限集合来判断。没有哪个权限等同于平台管理员身份,判断结果会和服务端不一致。
推荐 A(只看①选 A;②③④ 是否翻转:否)。置信缺口:A 要改一个已发布的 spec 字段的含义,第三方作者可能已经按"废弃"处理了它。
四棱(os-decision-facets)
- ① 长远:A 让界面和服务端读同一个信号,一条规则;B 和 D 留下含义不同的第二种判断。
- ② 拉动:今天只是多显示一个按钮,服务端会拒绝。同类还有 13 个管理员按钮。
- ③ 防 AI 犯错:A 让 AI 写的应用用唯一的正确写法;B 让 AI 学会被禁止的写法。
- ④ 不扩散:A 不新增键,只恢复一个已有键;C 新增永久键。
Prior rulings read: ADR-0068 D2/D4 → 1; #15136 (positions are not a standing read) → 1; #21795 ruling A (
5993018584) → 1; thread: the dev's5999863859
Generated by Claude Code
- Decision box: yes. A un-deprecates a published spec key, changes
- removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 5, 2026 2 remaining items
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsTriage: unlocked,
pm:blocked→pm:queue. #22012 landed the A-lite spec half, soadd_member's gate is next, as ruledBlocked-by: none
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T06:07Z. ⛔ Not a claim, ⛔ not a dispatch.The blocker is released (read at this write):
- PR fix(spec): EvalUser.isPlatformAdmin is the ADR-0095 D3 PLATFORM_ADMIN standing, not a deprecated alias #22018 merged as
d5a14dd5c0, an ancestor oforigin/main. - spec(identity):
EvalUser.isPlatformAdminis the live PLATFORM_ADMIN standing predicate but is still marked Deprecated and positions-derived — lift the mark, describe it as ADR-0095 D3 standing, date-note ADR-0068 D2/D4 (#21886 spec half) #22012 closedcompletedat 2026-10-07T05:06Z.
The premise holds on
main:EvalUserSchema.isPlatformAdmin(packages/spec/src/identity/eval-user.zod.ts:241) now describes the PLATFORM_ADMIN standing of ADR-0095 D3. The stale "Deprecated" mark is gone.sys_member.add_member(packages/platform-objects/src/identity/sys-member.object.ts:147) is still gated onrequiresFeature: 'organization'alone (:158).
What this card does, by the ruling
6019378035(A-lite):add_member'svisiblereadscurrent_user.isPlatformAdmin == true, alongside the feature gate. A dogfood pin shows that an org owner and a plain member see no "Add Member", while a platform admin does. ⛔ The rest of A, and B, C and D, were not taken.The grade stands:
bug·priority:p3·domain:engine·area:identity. #21903, the family's closing card, staysBlocked-by: #21886.- PR fix(spec): EvalUser.isPlatformAdmin is the ADR-0095 D3 PLATFORM_ADMIN standing, not a deprecated alias #22018 merged as
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsClaim: PM loop round 55 · 2026-10-07T06:32Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user, asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21886-add-member-visibility
Worktree:objectstack-issue-21886
Domain:domain:engine
Seat:domain:engine#1
Provenance:- The maintainer ruled this card A-lite (batch 🔗 Broken links detected in documentation #281 item 2, record 6019378035, 「同意」), and the director released it to this lane, with a fresh claim owed once spec(identity):
EvalUser.isPlatformAdminis the live PLATFORM_ADMIN standing predicate but is still marked Deprecated and positions-derived — lift the mark, describe it as ADR-0095 D3 standing, date-note ADR-0068 D2/D4 (#21886 spec half) #22012 closed. - Triage unlocked it to
pm:queue(6032081861): spec(identity):EvalUser.isPlatformAdminis the live PLATFORM_ADMIN standing predicate but is still marked Deprecated and positions-derived — lift the mark, describe it as ADR-0095 D3 standing, date-note ADR-0068 D2/D4 (#21886 spec half) #22012 landed through PR fix(spec): EvalUser.isPlatformAdmin is the ADR-0095 D3 PLATFORM_ADMIN standing, not a deprecated alias #22018 (d5a14dd5c0), andEvalUserSchema.isPlatformAdminnow describes the PLATFORM_ADMIN standing of ADR-0095 D3. - This is a fresh claim. The earlier one (5999650144) ended in
needs_decision(5999863859) with nothing pushed, and the ruling released it. - The lane runs three concurrent claims (the maintainer, verbatim: 「并发3」). finding(metadata-protocol): on an unscoped kernel, a package-bound stored row of a view name two packages ship is hydrated into the registry's bare slot, so a by-name read naming the other package serves that row's body under its own _packageId #22057 is claimed in the same round, on disjoint files.
File surface (atorigin/main56c88446ea), per the ruling 6019378035 and triage's unlock 6032081861: packages/platform-objects/src/identity/sys-member.object.ts:add_member(:147) gainsvisible: 'current_user.isPlatformAdmin == true', composed withrequiresFeature: 'organization'(:158).- A
platform-objectspin on the served predicate (platform-objects.test.ts, whereadd_member's servedvisibleis pinned today). - One dogfood pin (
packages/qa/dogfood/test/,domain:cli's path, declared on [PM seat] domain:cli — 🟢 os-project-manager · session_019SvPnd2bzECRNmAU9i6E4k #6024): an org owner and a plain member see no "Add Member", and a platform admin does. It bindscurrent_userthe way the console does, throughextra, notuser:. Withuser:,@objectstack/formulare-derivesisPlatformAdminfrom positions (the earlier dev report's third observation). .changeset/21886-*.md(@objectstack/platform-objectspatch).- ⛔ Not taken (ruled out): the rest of A (
createEvalUsertaking the rung as input, the@objectstack/formulascope change), and B, C and D. ⛔ Nopackages/spec,plugin-authorformulaedit. ⛔ identity: the platform-admin-gated actions onsys_user,sys_oauth_applicationandsys_sso_providershow no standing term invisible— the affordance family's closing card, with an enumeration pin (after #21886's ruling) #21903's thirteen siblings are not this card's.
Container & model:S,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no - The add-member door and its accept set are unchanged. The card hides an affordance from callers the door already refuses with 403. That is a served-metadata courtesy fix at
patch. No key, export or parameter is added.
Thread-read: 6032081861
Serial constraints cleared: at 2026-10-07T06:32Z: - Open PRs (fix(sharing): X-Share-Password declares its encoding (X-Share-Password-Encoding: utf-8), so any share-link password can be sent from a browser #22061, fix(lint)!: the object save door gives the build's validation-rule verdict (#22032 pass 1) #22041, chore: version packages #21988, feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974), each file list read by
filename: none touchessys-member.object.ts,platform-objects.test.tsor any file underpackages/qa/dogfood/test/. - The spec half, spec(identity):
EvalUser.isPlatformAdminis the live PLATFORM_ADMIN standing predicate but is still marked Deprecated and positions-derived — lift the mark, describe it as ADR-0095 D3 standing, date-note ADR-0068 D2/D4 (#21886 spec half) #22012, closedcompleted. - This lane's finding(metadata-protocol): on an unscoped kernel, a package-bound stored row of a view name two packages ship is hydrated into the registry's bare slot, so a by-name read naming the other package serves that row's body under its own _packageId #22057 (claimed this round) touches
metadata-protocolonly. - identity: the platform-admin-gated actions on
sys_user,sys_oauth_applicationandsys_sso_providershow no standing term invisible— the affordance family's closing card, with an enumeration pin (after #21886's ruling) #21903 staysBlocked-by: #21886.
- The maintainer ruled this card A-lite (batch 🔗 Broken links detected in documentation #281 item 2, record 6019378035, 「同意」), and the director released it to this lane, with a fresh claim owed once spec(identity):
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21886,
"status": "done",
"branch": "claude/issue-21886-add-member-visibility",
"pr": "#22064",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi (subagent; the parent PM's session id, as the order states)",
"premise_still_valid": true,
"summary": "Implemented ruling A-lite as ordered.sys_member.add_membernow declaresvisible: 'current_user.isPlatformAdmin == true'(sys-member.object.ts), andrequiresFeature: 'organization'composes onto it at parse time. The served predicate is(current_user.isPlatformAdmin == true) && features.organization != false. Pin (a) is the platform-objects lowering-matrix row. Pin (b) is one dogfood case on a real showcase boot: it bindscurrent_userthroughextrafrom each principal's served session, the way the console does, and shows that the platform admin alone is offered Add Member. An org owner who is not a platform admin, an admin, a delegated admin and a plain member are not. On the same boot the door refuses the owner and the member (403 PERMISSION_DENIED, no row written) and admits the platform admin (200, row lands). The reverse verification on committed HEAD turned (a) and (b) red, and the restore was proven by blob equality and an emptygit diff HEAD. origin/main (5cfd866) was merged before the PR; draft PR #22064 is open with the assignee set. After this comment, the worktree's node_modules, the worktree itself and the read-only objectui fetch are removed; the branch stays on the remote.",
"hypotheses": {
"H1": "CONFIRMED. The two keys compose with AND, the authored term first:lowerRequiresFeature(packages/spec/src/kernel/public-auth-features.ts:352-419) returns(existing) && gate(:415-418). The first-party precedent carrying both keys is sys_user.enable_two_factor (sys-user.object.ts:534-535), pinned as(...) && features.twoFactor == true. Nothing was invented.",
"H2": "CONFIRMED. The servedvisibleis exactly(current_user.isPlatformAdmin == true) && features.organization != false. It was read from the parsed SysMember in the matrix test and from the served/meta/object/sys_memberon the real boot (the dogfood case asserts every principal is served that one predicate). EvalUserSchema.isPlatformAdmin (packages/spec/src/identity/eval-user.zod.ts:225-241) describes the ADR-0095 D3 PLATFORM_ADMIN standing; no Deprecated mark remains.",
"H3": "CONFIRMED, with one correction. The console bindscurrent_userthroughextra. At the current objectui pin a58626c88d: fieldRules.ts:284 passes the scope asextra; ExpressionProvider.tsx:190 builds one subject under current_user / user / ctx.user / os.user, withfeaturesbeside it; expressionUser.ts:176 forwardsisPlatformAdminfrom the session. The session value isgrants.posture === 'PLATFORM_ADMIN'(plugin-auth auth-manager.ts:4083, emitted at :4107). Underuser:, formula re-derives it from positions (stdlib.ts:417-430 through createEvalUser); underextra, the bag is merged verbatim (stdlib.ts:462). The correction: the file's existingownerprincipal is the harness's seeded dev admin, and its served session carriesisPlatformAdmin: true, measured on the boot. It is reused as the platform admin. OS_PLATFORM_OWNER_EMAIL is not what makes it one: it is unset in the environment, absent from the dogfood package and the showcase config, and set by the harness only under a walled posture (packages/verify/src/harness.ts:481-497). So the rung comes from the single posture's unscoped admin_full_access grant. No org owner who is not a platform admin existed, so the case signs one up and sets its membership toownerin system context (the file's own shape). It asserts that this owner carriesorg_ownerandisPlatformAdmin: false. The plain member, admin and delegated admin are reused. The existing cases still bind throughuser:, unchanged.",
"H4": "CONFIRMED at 56c8844, with no change. No server path evaluates an actionvisible: rest, runtime, objectql, services, metadata, metadata-protocol, core and plugins have zero non-test hits. The eight ExpressionEngine/celEngine.evaluate call sites in package sources evaluate other things: formula fields and defaults (objectql engine.ts:2263, :6190), hook conditions (hook-wrappers.ts:309), approval conditions (approval-service.ts:1995), share-link gates (share-link-service.ts:308) and automation steps (service-automation engine.ts:11913, :12031). Nothing on the server bindscurrent_user.isPlatformAdminfor this predicate. @objectstack/formula was not touched."
},
"clause_2": "no, measured. The diff adds onevisiblevalue to an existing action and touches no file of the door: plugin-auth's organization-add-member.ts, the auth-plugin.ts mount and platform-admin-gate.ts are all unchanged, so the accept set is unchanged. No packages/spec file is in the diff, so no key, export or parameter is added.check-adr-0087-registration --base origin/main: '1 non-breaking changeset(s) seen'.check-changeset-no-major: 'This diff introduces nomajorbump'. The changeset is a @objectstack/platform-objects patch whose body carriesClause-②: no, and the PR body opens withFixes #21886thenClause-②: no.",
"files_changed": [
"packages/platform-objects/src/identity/sys-member.object.ts (+11: thevisiblepredicate and its comment)",
"packages/platform-objects/src/platform-objects.test.ts (+4 / -1: the add_member row of the lowering matrix)",
"packages/qa/dogfood/test/org-admin-affordance-reach.dogfood.test.ts (+106: docblock, the second-owner principal, one case)",
".changeset/21886-add-member-platform-admin-visibility.md (+11, new: @objectstack/platform-objects patch)"
],
"line_budget": "n/a",
"tests": "All at final HEAD bb53601 unless marked otherwise. Every build and test ran through scripts/pm/os-verify-lock.sh, and every VERDICT was command-exit 0. Build:turbo run build --filter='@objectstack/dogfood^...' --concurrency=2, 63/63 after the merge. (a)pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: Test Files 62 passed (62), Tests 996 passed (996). Verbose matrix run:SysMember...#add_member lowers to the previous hand-written predicatepasses, and the parsed object carriesvisible.source: '(current_user.isPlatformAdmin == true) && features.organization != false'. platform-objectstypecheck: exit 0, 0 TS errors;check:test-typecheck: OK;--listFilesshows the test file in tsconfig.test.json (1 hit), not in the main program (0). (b)pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/org-admin-affordance-reach.dogfood.test.ts: Tests 14 passed (14), includingAdd Member is offered to a platform admin alone .... dogfoodtypecheck: exit 0;--listFilesincludes the file (1 hit). Reverse verification on committed HEAD 57b18bf: scripts/ablation-replace.mjs in WRAP mode deleted the linevisible: 'current_user.isPlatformAdmin == true',(anchor x1 to x0, blob 9beac68ce1c7 to b8792313cac4), with an outertrap restore EXIT INT TERMon the absolute path. Rebuild of @objectstack/platform-objects: exit 0.ablation-dist-preflight.mjs @objectstack/platform-objects 'visible: \"current_user.isPlatformAdmin == true\"' --absent --source-marker=...: 'marker absent from all 66 built files', exit 0 (before the mutation the same spelling was at dist/index.mjs:2219). (a) went RED, 1 failed / 126 passed: expected 'features.organization != false' to be '(current_user.isPlatformAdmin == true) && features.organization != false'. (b) went RED, 1 failed / 13 passed: the offered set received [platform admin, org owner not a platform admin, org admin, delegated admin, plain member] against the expected [platform admin]. Direction: red, as predicted. Restore: ablation-replace and the outer trap both proved blob 9beac68ce1c7ed2b7143505916818a0a2e39a456 equal to the HEAD blob, withgit diff HEADempty. Rebuild, then preflight in present mode: 'marker present in 6 built files' and 'working tree clean against HEAD'. (a) passed 127/127 and (b) passed 14/14 again.",
"gates": "At bb53601.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 67 commands, identical before and after the merge and identical to the order's list. All 67 ran and their last runs exited 0.--ran ran.list: '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN'. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3) on the dogfood-closure build. After a fullturbo run build(72/72, under the lock) it passed (106 require entry points / 66 packages); check:dts-closure, check:lean-entry-closure, check:published-files and check:sourcemap-no-sources-content were rerun on that build and passed. Artifact-roster block: all 53 printed commands ran and 50 exited 0. Three answered NOT WIRED / NOT MEASURED (exit 2) for want of PR context. Rerun with context, all three passed: check-partof-closing-keyword with PR_BODY; check-closing-target-claim with PR_NUMBER=22064 ('PR #22064 closes #21886, and each carries a Claim: whose Branch: line names claude/issue-21886-add-member-visibility'); check-single-claim-paths with PR_NUMBER=22064. Partial: check-sdui-manifest passed, but its objectui version comparison is NOT CHECKED (no objectui checkout). check:console-injection is NOT MEASURED (no console dist; only its self-test ran). Neither surface is touched. The four symbol-anchor sweeps (check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors) all passed. Lint, as a proven narrowing: eslint --no-inline-config --format json over the 3 touched TS files gave 3 files linted, 0 errors, 0 warnings. All 3 are in eslint's population (--print-config resolves). Invariance: the resolved configs carry no parserOptions.project/projectService, and eslint.config.mjs:327-328 states type-aware linting is never enabled. Left to CI: type-check lanes, Test Core, Dogfood Regression Gate, Build Core, repo-wide pnpm lint. CI state at report time: not read (in_progress or not started).",
"deviations": [
"objectui source (the pin a58626c88d) was read from a read-only shallow fetch (--depth 1, blob:none) into the session scratchpad. The container has no ../objectui checkout, and the GitHub API answered 403 for objectui in this session. It is deleted in cleanup.",
"Pin (b) adds one principal (a second org owner, signed up and set toownerin system context) to the file's beforeAll, and its case writes a second organization and one membership. No existing principal was an org owner who is not a platform admin. The case runs last in the file, so no earlier case reads that state. The existing cases' binding is unchanged.",
"There are two code commits, not one. The second (57b18bf, test-only) restates the case's verdict loop as one named-set equality, so the reverse verification shows every principal's cell at once instead of stopping at the first.",
"The harness's attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. AGENTS.md's model-free trailer pair and the session-URL PR footer were used instead (AGENTS.md takes precedence; the reminder itself defers to it).",
"git push ran four times: the empty-branch probe, the two commits, and the merge of origin/main. The budget namesgit pushas one category, and none was a force-push.",
"The PR carries the labelsdocumentation,size/m,testsandtooling, which another actor (the path labelers) set. They were left untouched.",
"No write or command was refused by the session's permission checks."
],
"mcp_calls": "0",
"api_writes": "3 relay strokes, eachPOST /repos/objectstack-ai/objectstack/dispatchesfrom the session and executed as objectstack-fleet[bot]: (1) pr_create, run 37587839117, becomingPOST /repos/objectstack-ai/objectstack/pulls(draft) and creating #22064 (read-back: 11798 bytes sent and stored, identical); (2) label-write --assign os-project-manager, run 37587907840, becomingPOST /repos/objectstack-ai/objectstack/issues/22064/assignees(read-back matches); (3) this os-dev-report comment through scripts/pm/post-stamped.mjs, becomingPOST /repos/objectstack-ai/objectstack/issues/21886/comments. Plus git pushes (not REST). Reads: single-card REST reads of #21886 and its comments, one REST read of PR #22064, and the read-only GitHub calls of check-closing-target-claim and check-single-claim-paths.",
"open_questions": [],
"out_of_scope_findings": [
"observation (not class a/b/c; ruled predicate): the door also admits the legacy better-authuser.role === 'admin'scalar (plugin-auth platform-admin-gate.ts:80-84), whichcurrent_user.isPlatformAdmin == truedoes not read. A pre-ADR-0068-D2 deployment whose user carries the scalar but not the rung would be admitted by the door and not offered the button. reach: none measured; the gate header says nothing in ObjectStack writes the scalar and that re-synthesizing it is vetoed. carrier: 承接者:无 · noted in the PR's Acceptance notes, not filed",
"coverage note (not a finding): the dogfood case exercises the single-posture route to PLATFORM_ADMIN (the seeded admin's unscoped grant), not the walled postures' OS_PLATFORM_OWNER_EMAIL route. carrier: #21903 (the family close-out,Blocked-by: #21886), if it wants a walled-posture principal · noted, not filed"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsLanded: PR #22064 →
77a94d8cedonmain. It merged through the merge queue at 2026-10-07T08:35Z, after entering the queue at 2026-10-07T08:08Z. Verified at 2026-10-07T08:36Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash. It is on
origin/mainas a single-parent commit (parent5cfd8661c4). Its diffstat is the reviewed one: 4 files, +132/-1. - What is on
main(the maintainer's ruling A-lite, 6019378035):sys_member.add_memberdeclaresvisible: 'current_user.isPlatformAdmin == true'. The served predicate is(current_user.isPlatformAdmin == true) && features.organization != false.- The
platform-objectslowering matrix pins it. - One dogfood case on a real showcase boot binds
current_useras the console does (throughextra). The platform admin alone is offered "Add Member"; an org owner, an org admin, a delegated admin and a plain member are not. The door refuses the owner and the member with 403 and admits the platform admin.
- The card.
Fixes #21886closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body. - From this release (
@objectstack/platform-objectspatch,Clause-②: no): on an organization's member list, "Add Member" is offered only to a platform administrator, the one standing its endpoint admits. Owners and admins no longer see a button that answers 403. - The seat's ACCEPT is on PR fix(platform-objects): offer Add Member only to a platform admin #22064 (6033730812).
- Next in the family: identity: the platform-admin-gated actions on
sys_user,sys_oauth_applicationandsys_sso_providershow no standing term invisible— the affordance family's closing card, with an enumeration pin (after #21886's ruling) #21903 (the thirteen sibling actions and the enumeration pin) wasBlocked-by: #21886, and that blocker is now released. It goes to triage's unlock scan; it is not claimed here.
Generated by Claude Code
- The squash. It is on
Ruled: 6019378035 · letter A-lite · 2026-10-06T15:16Z
Blocked-by: #22012
Filing-gate class ① (a defect with a named location, measured on a real boot). Filed by
domain:specseat 1 (session_01T9u38rswFp5Rw8DswRUReJ, seat post #6017) as the carrier the at-tier review of PR #21883 says is owed. Sources:5996636663on platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795, ③ (b);5996276927, out-of-scope finding 2.⛔ Not graded or routed here; ⛔ not a claim.
What a user sees
On an organization's member list, "Add Member" is offered to every member: plain members, owners and admins alike. Submitting it answers 403 unless the caller is a platform admin. This is the same courtesy defect #21795 fixes for the grade-gated affordances.
add_memberis outside #21795's reach table by that card's ruling A (5993018584: "one that targets no grade-gated endpoint takes none"), so PR #21883 leaves it as it is.Where
packages/platform-objects/src/identity/sys-member.object.ts:144, theadd_memberaction. Its servedvisibleis theorganizationfeature gate alone. The platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795 dev measured this on a real showcase boot, as served metadata for a plain member.packages/plugins/plugin-auth/src/organization-add-member.ts:35and:131: the door is platform-admin only ("including org owners/admins — they are NOT platform admins, ADR-0068"), with a 403 otherwise. This is cited from the source and its test; it was not re-measured.Why it matters
It fails closed, because the server refuses. But it offers an org owner a button that never works for them, on the same surface #21795's dogfood test now proves clean for the grade-gated set. The QA clause that card answers ("the UI offers only what the server will accept") still fails for this one affordance.
Fix direction (not ruled)
Gate
add_member's visibility on the same standing its door reads, platform admin, through whatever declared predicate the actionvisiblescope already offers for that standing. If none is declared, the gap is a contract question, as #21795's was. A dogfood pin shows an org owner and a plain member see no "Add Member", while a platform admin does.Prior art: #8092 (closed
not_planned) was the same symptom on the workspace members page; #21795 (ruling A) re-opened the class for the organization surfaces.Dedupe:
mcp__github__search_issues(repo-scoped, semantic, closed included),add_member action offered to every organization member refused unless platform admin sys_member Add Member affordance→ 16 hits, none this defect:add_memberis excluded there by its own ruling;sys_member"Add Member" action targetsPOST /organization/add-member, which better-auth 1.7.0-rc.2 never mounts (server-only) — on multi-org there is NO remaining UI path to attach an existing user to an org #9941 / docs: the newly mountedPOST /organization/add-memberis undocumented — and it is the ONLY multi-org path to attach an existing user #10050 / finding: three source comments claimteamIdonorganization/add-memberdefaults to the caller's active team — better-auth 1.7.1 has no such fallback #10532: the add-member endpoint's mount and docs;memberrole; only the server 403 stops it #8092: the workspace page, closednot_planned;Dedupe words:
add_member platform admin affordance·Add Member offered org owner 403·sys_member add_member visible