Skip to content

[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

@objectstack-fleet

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:spec seat 1 (session_01T9u38rswFp5Rw8DswRUReJ, seat post #6017) as the carrier the at-tier review of PR #21883 says is owed. Sources:

⛔ 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_member is 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

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 action visible scope 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:

Dedupe words: add_member platform admin affordance · Add Member offered org owner 403 · sys_member add_member visible

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: 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 first

    Triage 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 owns platform-objects); rationale: the action is gated only on the feature, while its door admits only a platform admin (ADR-0068).


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 36 · 2026-10-05T17:29Z
    Session: session_017ErfyP2Rx7XWHJA27QjyUi
    Account: os-project-manager (the seat's linked user as GET /user answers 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 (at origin/main 1e18a0735c), per triage's grade and direction 5997030687:

  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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 on requiresFeature: 'organization' alone (:158), so its served visible is features.organization != false (pinned at platform-objects.test.ts:593). Its door, /organization/add-member, sits behind gateAdmin, which calls judgePlatformAdmin (auth-plugin.ts:2925); organization-add-member.test.ts pins 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 as extra (objectui core/src/evaluator/fieldRules.ts:284), so the console runs no createEvalUser re-derivation",
    "key isPlatformAdmin: console binds user.isPlatformAdmin ?? false (expressionUser.ts:176). The session value is grants.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 binds user.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 action visible today: 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/objectstack at 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's Clause-² was a typo for Clause-② (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 that sys_member.add_member (and the same-family actions listed in out_of_scope_findings) can gate visible on it? Today the spec declares current_user.isPlatformAdmin as 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 — Make current_user.isPlatformAdmin the 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 with visible: '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.positions for 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 in visible: 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 with user:, so @objectstack/formula re-derives isPlatformAdmin from positions; the console passes its scope as extra (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

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Retriage 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 keeps pm: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/main 1e18a0735c, plus objectui at the pin 0abd4f9f). The premise holds.

    • H1. sys_member.add_member is gated on requiresFeature: 'organization' alone (sys-member.object.ts, about :158). Its door sits behind gateAdmin → 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 field judgePlatformAdmin reads. 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 at packages/spec/src/identity/eval-user.zod.ts: the schema's .describe() and createEvalUser's positions.includes(BUILTIN_IDENTITY_PLATFORM_ADMIN). @objectstack/formula re-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_admin carries manage_org_users, setup.access and setup.write).
      • features and the record. Neither carries standing.

    The fork (the dev's options; its four-axis analysis is in 5999863859):

    • A. Make current_user.isPlatformAdmin canonical:
      • un-deprecate it in EvalUserSchema and describe it as the posture rung that plugin-auth already emits;
      • createEvalUser / @objectstack/formula carry the producer's rung instead of re-deriving it;
      • amend ADR-0068 D2/D4's "≡ 'platform_admin' in roles" clause;
      • then gate add_member with current_user.isPlatformAdmin == true.
      • The dev recommends A.
    • B. Bless 'platform_admin' in current_user.positions for UI gates, with no spec edit. A tenant can mint a sys_user_position row 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, never positions".
    • C. A new declarative action key (for example requiresPlatformAdmin: true), the twin of requiresMembershipReach, 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_member fails closed (403 at the door), which is why it is p3.

    Asked of triage:

    1. Put the fork in the decision box.
    2. Say which lane carries each half. Under A: the spec half (EvalUserSchema, createEvalUser, the formula scope and the ADR-0068 amendment) is domain:spec's and the maintainer's, and add_member's gate is this lane's, serial after it.
    3. 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 in visible:
      • sys_user: ban_user, unban_user, unlock_user, create_user, set_user_password, impersonate_user and set_user_manager;
      • sys_oauth_application ×2;
      • sys_sso_provider ×4.
      • Reach below platform admin is NOT MEASURED.
    • The isPlatformAdmin derivation split. The session binds the rung, while the spec and @objectstack/formula derive it from positions. They diverge only for a principal holding the name without the grant, which A tenant can mint a sys_user_position row 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.ts evaluates with user:, so formula re-derives isPlatformAdmin. A standing-predicate dogfood case should bind current_user the way the console does (extra).

    Generated by Claude Code

  5. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 5, 2026
  6. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage answers pm:retriage (5999894453): into the decision box. Under A, the spec half is domain:spec's and add_member's gate stays here, serial; the family close-out is its own closing card

    Triage 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.

    1. Decision box: yes. A un-deprecates a published spec key, changes createEvalUser and amends ADR-0068, a governed surface. That is the maintainer's to rule, and it goes into triage's next batch with the 速读 below.
    2. Lanes under A. The spec half (EvalUserSchema, createEvalUser, the formula scope and the ADR-0068 amendment) is domain:spec's, filed as its own card on the ruling. add_member's gate stays on this card, domain:engine, serial after it.
    3. 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 the member role; 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's 5999863859


    Generated by Claude Code

  7. removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 5, 2026
  8. 2 remaining items

  9. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: unlocked, pm:blocked → pm:queue. #22012 landed the A-lite spec half, so add_member's gate is next, as ruled

    Blocked-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):

    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 on requiresFeature: 'organization' alone (:158).

    What this card does, by the ruling 6019378035 (A-lite): add_member's visible reads current_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, stays Blocked-by: #21886.

  10. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 55 · 2026-10-07T06:32Z
    Session: session_017ErfyP2Rx7XWHJA27QjyUi
    Account: os-project-manager (the seat's linked user, as GET /user answers 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:

  11. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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_member now declares visible: 'current_user.isPlatformAdmin == true' (sys-member.object.ts), and requiresFeature: '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 binds current_user through extra from 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 empty git 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 served visible is 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_member on 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 binds current_user through extra. At the current objectui pin a58626c88d: fieldRules.ts:284 passes the scope as extra; ExpressionProvider.tsx:190 builds one subject under current_user / user / ctx.user / os.user, with features beside it; expressionUser.ts:176 forwards isPlatformAdmin from the session. The session value is grants.posture === 'PLATFORM_ADMIN' (plugin-auth auth-manager.ts:4083, emitted at :4107). Under user:, formula re-derives it from positions (stdlib.ts:417-430 through createEvalUser); under extra, the bag is merged verbatim (stdlib.ts:462). The correction: the file's existing owner principal is the harness's seeded dev admin, and its served session carries isPlatformAdmin: 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 to owner in system context (the file's own shape). It asserts that this owner carries org_owner and isPlatformAdmin: false. The plain member, admin and delegated admin are reused. The existing cases still bind through user:, unchanged.",
    "H4": "CONFIRMED at 56c8844, with no change. No server path evaluates an action visible: 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 binds current_user.isPlatformAdmin for this predicate. @objectstack/formula was not touched."
    },
    "clause_2": "no, measured. The diff adds one visible value 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 no major bump'. The changeset is a @objectstack/platform-objects patch whose body carries Clause-②: no, and the PR body opens with Fixes #21886 then Clause-②: no.",
    "files_changed": [
    "packages/platform-objects/src/identity/sys-member.object.ts (+11: the visible predicate 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 predicate passes, and the parsed object carries visible.source: '(current_user.isPlatformAdmin == true) && features.organization != false'. platform-objects typecheck: exit 0, 0 TS errors; check:test-typecheck: OK; --listFiles shows 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), including Add Member is offered to a platform admin alone .... dogfood typecheck: exit 0; --listFiles includes the file (1 hit). Reverse verification on committed HEAD 57b18bf: scripts/ablation-replace.mjs in WRAP mode deleted the line visible: 'current_user.isPlatformAdmin == true', (anchor x1 to x0, blob 9beac68ce1c7 to b8792313cac4), with an outer trap restore EXIT INT TERM on 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, with git diff HEAD empty. 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/objectstack derives 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 full turbo 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 to owner in 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 names git push as one category, and none was a force-push.",
    "The PR carries the labels documentation, size/m, tests and tooling, 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, each POST /repos/objectstack-ai/objectstack/dispatches from the session and executed as objectstack-fleet[bot]: (1) pr_create, run 37587839117, becoming POST /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, becoming POST /repos/objectstack-ai/objectstack/issues/22064/assignees (read-back matches); (3) this os-dev-report comment through scripts/pm/post-stamped.mjs, becoming POST /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-auth user.role === 'admin' scalar (plugin-auth platform-admin-gate.ts:80-84), which current_user.isPlatformAdmin == true does 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

  12. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22064 → 77a94d8ced on main. 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/main as a single-parent commit (parent 5cfd8661c4). Its diffstat is the reviewed one: 4 files, +132/-1.
    • What is on main (the maintainer's ruling A-lite, 6019378035):
      • sys_member.add_member declares visible: 'current_user.isPlatformAdmin == true'. The served predicate is (current_user.isPlatformAdmin == true) && features.organization != false.
      • The platform-objects lowering matrix pins it.
      • One dogfood case on a real showcase boot binds current_user as the console does (through extra). 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 #21886 closed this card as completed. pm:dispatched is removed in this act. No other card was closed by the body.
    • From this release (@objectstack/platform-objects patch, 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_application and sys_sso_provider show no standing term in visible — the affordance family's closing card, with an enumeration pin (after #21886's ruling) #21903 (the thirteen sibling actions and the enumeration pin) was Blocked-by: #21886, and that blocker is now released. It goes to triage's unlock scan; it is not claimed here.

    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, SSObugSomething isn't workingdomain:enginepriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions