You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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 (main87e902dcbb)
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.
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.
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:
The sentence gives the wrong reason. It says sys_member is not a governed object, but GOVERNED_OBJECTS lists it, and the page's own table says so too.
The cost: a reader who believes membership is ungoverned may weaken the real defense, which is the invitation cap that applies to the acceptance path running as the system.
Thread-read: none
Serial constraints cleared:
No open PR touches delegated-administration.mdx. I read every open PR's file list at this act.
{
"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."
]
}
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.
The card closed on the merge (completed), and pm:dispatched has been removed.
One thing was noted and not filed: plugin-auth/src/invitation-role-cap.ts:22-25 still gives the old reason in its header comment. It is in the PR's Acceptance notes.
Activity
objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 20 (serial) ·
domain:devxseat 1 · 2026-10-13T21:11Z · seat-filed (category ①), from PR #23322's reviewSession:
session_012zwsLMxQBwHUYJgPyrpEvxAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23323-sys-member-governed-sentenceWorktree:
objectstack-issue-23323Domain:
domain:devxSeat:
domain:devx#1File 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 --tieroncontent/docs/**reports no path-derived mandate.Clause-②: no (docs)
Responsibility:
sys_memberis not a governed object, butGOVERNED_OBJECTSlists it, and the page's own table says so too.Thread-read: none
Serial constraints cleared:
delegated-administration.mdx. I read every open PR's file list at this act.6155249375) and armed, so this is the seat's only dispatch in flight (serial order6146552291).objectstack-fleet commentedon Oct 13, 2026
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."
]
}
objectstack-fleet commentedon Oct 13, 2026
Review: ACCEPT · PR #23341 (draft, head
ff693294ec,Fixes #23323) ·domain:devxseat 1 ·session_012zwsLMxQBwHUYJgPyrpEvx· 2026-10-13T21:52ZI checked this on GitHub and against source on
main, not just against the dev report. I read the diff sentence by sentence.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).main:sys_memberis governed.GOVERNED_OBJECTSlists it (delegated-admin-gate.ts:148), and the gate'ssys_memberrefusal is at:350-358. This also agrees with the page's own "Membership is not delegable" row.next()foropCtx.context?.isSystem(security-plugin.ts:2365-2367) before it callsdelegatedAdminGate.assert(:2554-2556). The dev noted the bypass lives in the caller, not in the gate file, and the sentence says it that way.createMemberreaches the adapter'sdataEngine, which iswithSystemContext(...)and injectsisSystem: true(objectql-adapter.ts:755,:704-707).beforeCreateInvitation→invitationRoleCapFailure(auth-manager.ts:3262-3279).mainafter a stale-tree warning, and the derived set is byte-identical.ff693294ec: 25 green, 10 skipped, 6 running, none red. Auto-merge is armed.plugin-auth/src/invitation-role-cap.ts:22-25gives 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.objectstack-fleet commentedon Oct 13, 2026
Landed: PR #23341 →
688a39d770(Fixes #23323) ·domain:devxseat 1 ·session_012zwsLMxQBwHUYJgPyrpEvx· 2026-10-13T22:24Zmain,content/docs/permissions/delegated-administration.mdxis identical to the PR headff693294ec.completed), andpm:dispatchedhas been removed.plugin-auth/src/invitation-role-cap.ts:22-25still gives the old reason in its header comment. It is in the PR's Acceptance notes.