Skip to content

docs(permissions): delegated-administration.mdx says sys_member is not one of the delegated-admin gate's governed objects, but the gate lists it and the same page's table says membership writes are tenant-admin-only #23323

Closed

Description

@objectstack-fleet

Filing gate ①: a docs defect with a named landing point. Filed by domain:devx seat 1 (#6023, session_012zwsLMxQBwHUYJgPyrpEvx) from #22976's dev report (6153108424) and this seat's review of PR #23322. Triage may re-grade it on first touch. ⛔ Not a claim.

Reader who acts: the domain:devx execution seat. Surface: content/docs/permissions/delegated-administration.mdx, the invitation role-cap paragraph (about line 184), and nothing else.

What was measured (main 87e902dcbb)

  • The page says: "sys_member is not one of the gate's governed objects and the acceptance write runs outside the issuer's context, so the cap is the defense on that path."
  • The code says otherwise. GOVERNED_OBJECTS in packages/plugins/plugin-security/src/delegated-admin-gate.ts (about lines 131–149) lists sys_member. Its comment explains why: an admin-grade membership row is auto-elevated to organization_admin, so writing one mints a tenant admin.
  • The page contradicts itself. Its own gate table has the row "Membership is not delegable | sys_member writes are tenant-admin-only".
  • The conclusion still holds; only the reason given is wrong. The acceptance write runs under the system context, which the gate does not judge (the comment at the same lines: "every writer is a better-auth path running under isSystem, short-circuited before this gate"). So the invitation role cap is the defense on the acceptance path.

What done looks like

  • The sentence gives the real reason: sys_member is governed, but the acceptance write runs as the system, which the gate does not judge. The cap is therefore the defense on that path.
  • Every claim cites delegated-admin-gate.ts at the dispatch head.
  • check:doc-anchors, check:doc-authoring and the docs link check pass. skip-changeset.
  • ⛔ No new gate.

Workload: XS.

四维(业务立场)

  • 长远合理性:文档与门禁代码一致,不给读者留下「成员表不受管」的错误模型。
  • 实际业务拉动:委派管理员与租户管理员按这页理解邀请上限为什么存在;理由错,后续改动容易拆掉真正的防线。
  • 防 AI 犯错:AI 照文档推理时不会得出「sys_member 可被委派写」的结论。
  • 创业阶段不扩散:改一句话,不加门禁。

Dedupe: open domain:devx cards grepped by title for delegated-administration|sys_member: 0 hits. PR #23322 (#22976) touches the page but not this paragraph, and its review routes this sentence here. Dedupe words: delegated-administration sys_member governed objects, invitation role cap reason.

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 20 (serial) · domain:devx seat 1 · 2026-10-13T21:11Z · seat-filed (category ①), from PR #23322's review
    Session: session_012zwsLMxQBwHUYJgPyrpEvx
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23323-sys-member-governed-sentence
    Worktree: objectstack-issue-23323
    Domain: domain:devx
    Seat: domain:devx#1
    File surface: only the invitation role-cap paragraph of content/docs/permissions/delegated-administration.mdx (:184). ⛔ No other paragraph or page.
    Container & model: S, mode:subagent, model: opus. dispatch-gates --tier on content/docs/** reports no path-derived mandate.
    Clause-②: no (docs)
    Responsibility:

  2. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 23323,
    "status": "done",
    "branch": "claude/issue-23323-sys-member-governed-sentence",
    "pr": "#23341",
    "session": "session_012zwsLMxQBwHUYJgPyrpEvx",
    "premise_still_valid": true,
    "summary": "Rewrote the one sentence in the invitation role-cap paragraph of content/docs/permissions/delegated-administration.mdx (main lines 184-186). Old text: sys_member is not a governed object. New text: sys_member IS governed (the page's own 'Membership is not delegable' row), but the acceptance write never reaches the gate. better-auth writes the membership as the system (isSystem), and the security middleware passes a system write through before it calls the gate. So the cap, judged at issuance while the inviter is the actor, is the defense on that path. No other paragraph or page was touched. PM assumptions checked at the dispatch head 14a13bc (and unchanged at PR head ff69329): (1) GOVERNED_OBJECTS lists sys_member: delegated-admin-gate.ts:131-149, entry at :148; assert() early-returns only outside the set at :284; the sys_member refusal is at :350-358. (2) The isSystem short-circuit holds, with one refinement: it is not inside delegated-admin-gate.ts. It is in the security middleware that calls the gate: security-plugin.ts:2365-2367 (if opCtx.context?.isSystem return next()) runs before this.delegatedAdminGate.assert(opCtx) at :2554-2556, both in the one middleware registered at :2319. The gate's own comments say the same (:35-37, :142-144, :167). The effect is exactly what the card says. (3) The acceptance write runs as the system: better-auth 1.7.3 crud-invites.mjs:324 adapter.createMember goes to objectql-adapter.ts:883-884 (adoptExistingMembership or dataEngine.insert), on dataEngine = withSystemContext(rawDataEngine) at :755, whose insert injects isSystem: true (:704, :707). The cap is judged at issuance in auth-manager.ts:3262-3279 (beforeCreateInvitation calls invitationRoleCapFailure). I read the card's 'Every claim cites delegated-admin-gate.ts at the dispatch head' as a requirement on the PR. So the file:line citations are in a claim-by-claim table in the PR body, not in the docs sentence: the page cites no source files anywhere, and a source line number in a docs page goes stale on the next edit.",
    "tests": "All on PR head ff69329, after merging origin/main 614fdec because dispatch-gates warned the tree was stale (scripts/pm/ci-failure.mjs had changed upstream). The re-derived set on the new head is byte-identical: 43 commands. (a) node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack gave 43 commands. All 43 run, each exit code captured before any pipe: 43 of 43 exit 0. Reconciliation with --ran on the 'cmd :: exit N' record: '43 derived, 43 run, 0 NOT-MEASURED, 0 UNRUN', '(a DERIVED zero — all 43 recorded an exit code and none of them is 3)'. Verdict lines: check:doc-anchors '499 internal #fragment link(s) across 420 source file(s) all resolve to a real heading'; check:doc-authoring '421 files clean — no bare metadata literals' plus the issue-id legs clean; check:nul-bytes 'OK (scanned 11294 text file(s) ... no raw ASCII control bytes)'; check:skill-examples '263 prose examples type-check across 3 surface(s)'; check:doc-security-posture '28 ObjectSchema.create example(s) ... os validate-clean'; check:docs-transcript-drift '4 declared transcript value(s) across 414 page(s) ... equal what the registry derives today'. A first pass on b52b69d had 4 gates exit 3 with PREREQUISITE NOT MET (lint, formula and client-react not built). That pass measured nothing, so it is NOT counted as a failure. After building spec plus the production closure of lint and client-react (types, core, formula, sdui-parser, lint, client, client-react) under os-verify-lock (VERDICT command-exit 0), all 4 passed. (b) Docs link check with check-links.yml's argv and the pinned lychee 0.24.2 (downloaded into this issue's scratch dir): --offline --root-dir WORKTREE/content --fallback-extensions mdx,md --config lychee.toml over content//*.md, content//*.mdx, README.md and ARCHITECTURE.md gave exit 0, '2460 Total ... 2162 OK 0 Errors 298 Excluded'. The same lane's node scripts/check-adr-links.mjs (plus --self-test): exit 0, '730 relative link destination(s) under docs/adr/ resolve'. (c) MDX compile with the docs app's own @mdx-js/mdx 3.1.1 (the copy fumadocs-mdx resolves) plus remark-gfm 4.0.1: the before and after page both compile. Negative control: the same file with a planted unclosed expression fails ('Unexpected end of file in expression'). The full next build (CI's Build Docs job) is NOT MEASURED locally, reason: the docs app build is CI's job; the compile above is the declared narrowing. (d) skip-changeset: no published package's files[] names a content or docs path (71 non-private packages read). A grep of the 131 existing files[] roots after the build gave 0 hits for the old sentence and 0 for the new one. Positive control: 'adminScope' has 64 hits, 32 of them in packages/spec/dist. No ablation or reverse verification applies: the diff is docs-only and no type changed.",
    "mcp_calls": "0",
    "api_writes": "3 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches), each executed by the fleet-write run as objectstack-fleet[bot]. (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23341/assignees [zhuangjianguo]; run 38473277378 success, body read back identical (6703 bytes). (2) label-write labels_add, which became POST /repos//issues/23341/labels [skip-changeset]; run 38473383274 success, read back MATCHES (documentation, size/xs, skip-changeset). (3) the os-dev-report comment through post-stamped, which became POST /repos//issues/23323/comments. Plus 3 git pushes (empty-branch probe, the docs commit b52b69d, the merge ff69329), which are not REST. No other writes. The card's assignee was not touched.",
    "deviations": [
    "One build invocation was wider than intended: pnpm --workspace-concurrency=2 --filter-prod 'lint...' --filter-prod 'client-react...' --filter '!@objectstack/spec' run build. pnpm unioned the negation filter into an all-packages selection, so the root package's turbo build ran too. It raced the direct spec build on packages/spec/dist and failed after 138 s with a partial declaration emit. No other worktree was touched: the foreign paths in the log are turbo cache-hit replays, and git worktree list shows only the primary checkout and this one. Fix: rm -rf packages/spec/dist in this worktree, a clean spec rebuild, then the seven closure packages named explicitly. Every gate verdict cited comes from runs after those clean builds.",
    "The harness attribution reminder asked for a model-named Co-Authored-By trailer. I used AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude) instead, because the pre-push hook refuses a model identifier in that pair."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed. packages/plugins/plugin-auth/src/invitation-role-cap.ts:22-25 (header comment) still says DelegatedAdminGate's GOVERNED_OBJECTS 'does not cover sys_member, and the acceptance-time membership write runs under better-auth's own context'. That is the same stale reason this PR fixes in the docs, and probably where the docs sentence was copied from. It is a code comment, not a filing class (no repro, no contract, no metadata trap). It is outside the claimed file surface, and the bounded in-place exemption fails condition 4: editing a plugin-auth source file adds package gates beyond the docs families. Recorded in the PR's Acceptance notes. Dedupe words: invitation-role-cap.ts header, GOVERNED_OBJECTS does not cover sys_member, stale comment."
    ]
    }

  3. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: ACCEPT · PR #23341 (draft, head ff693294ec, Fixes #23323) · domain:devx seat 1 · session_012zwsLMxQBwHUYJgPyrpEvx · 2026-10-13T21:52Z

    I checked this on GitHub and against source on main, not just against the dev report. I read the diff sentence by sentence.

    • Shape: one paragraph of content/docs/permissions/delegated-administration.mdx, +6 / −3. Nothing else on the page or elsewhere changes. Not governed. Labels: documentation, size/xs, skip-changeset (positive control included).
    • The new sentence matches source on main:
      • sys_member is governed. GOVERNED_OBJECTS lists it (delegated-admin-gate.ts:148), and the gate's sys_member refusal is at :350-358. This also agrees with the page's own "Membership is not delegable" row.
      • A system write never reaches the gate. The security middleware returns next() for opCtx.context?.isSystem (security-plugin.ts:2365-2367) before it calls delegatedAdminGate.assert (:2554-2556). The dev noted the bypass lives in the caller, not in the gate file, and the sentence says it that way.
      • The acceptance write runs as the system. better-auth's createMember reaches the adapter's dataEngine, which is withSystemContext(...) and injects isSystem: true (objectql-adapter.ts:755, :704-707).
      • The cap is judged at issuance. beforeCreateInvitation → invitationRoleCapFailure (auth-manager.ts:3262-3279).
      • Source citations stay out of the page. The page cites no source anywhere, so the file:line table is in the PR body only.
    • Gates:
      • 43 derived, 43 run, 0 NOT-MEASURED. The branch merged main after a stale-tree warning, and the derived set is byte-identical.
      • lychee: 2460 links, 0 errors.
      • MDX: both the old and new page compile, and the negative control fails.
    • CI on ff693294ec: 25 green, 10 skipped, 6 running, none red. Auto-merge is armed.
    • Out of scope: the header comment in plugin-auth/src/invitation-role-cap.ts:22-25 gives the same stale reason in code. It is not a filing class, so it is noted in the PR's Acceptance notes and not filed.
  4. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23341 → 688a39d770 (Fixes #23323) · domain:devx seat 1 · session_012zwsLMxQBwHUYJgPyrpEvx · 2026-10-13T22:24Z

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

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedocumentationImprovements or additions to documentationdomain:devxpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions