Repository navigation
[Design] Re-anchor platform-admin: admin_full_access becomes a kernel metadata declaration; WHO holds it comes from env-configured verified emails — retiring the org-less row anchor #11663
Description
Chartered by maintainer ruling, 2026-08-24, live PM chat, verbatim: 「立卡,优先处理,并评估有没有相关任务要一起处理」 — with the design direction proposed by the maintainer in the same session: 「admin_full_access 能否在内核中用元数据约定,然后具体谁是超级管理员通过环境变量配置邮箱?」. Direction is ruled; this card designs the details and sequences the implementation. Design-first: no implementation dispatch until the design is accepted.
The problem (business form, from the #2734 → #10103 → #11532 lineage)
The platform's own super-admin right is anchored on a data row inside the tenant data model: an org-less admin_full_access row in sys_permission_set, referenced by row id from an unscoped sys_user_permission_set grant (resolve-authz-context.ts §6b), seeded ~1.3s before the first organization exists. It is the last structural exception to the ruled walled-tenancy doctrine ("every row belongs to exactly one organization"; #10103 Option C). Costs: single-point fragility (touch the row → platform lockout or silent re-mint), a recurring tax on every sweep/gate/diagnostic (three ruling rounds spent on this family already), and an asterisk in the security story.
Ruled direction
- The permission set's CONTENT is a kernel metadata declaration — code-declared, versioned, spec-validated, PR-reviewed. No runtime row holds the super-admin capability set; nothing can delete or mutate it at runtime. (Mainstream precedent: Keycloak
KEYCLOAK_ADMIN, GitLab root bootstrap, GrafanaGF_SECURITY_ADMIN_*, Kubernetes deployment-credential superuser — operator-level admin comes from deployment config, never from business data.) - The HOLDER comes from deployment config: an env variable listing admin email(s). Platform-admin derivation becomes: caller's verified email ∈ configured list →
PLATFORM_ADMINwith the declared capability set.
Five security pins the design must nail (from the chartering discussion)
- Verified-email match only — the env entry confers nothing until an account holding that email has passed verification; normalize (lowercase) and match exactly. Otherwise registering the configured address is a privilege escalation.
- Empty config = fail closed — no platform admin exists; the development-mode dev-admin seed stays development-only.
- Auditability — the resolved admin list is surfaced read-only (Setup / discovery / health) since there is no grant row left to query.
- Revocation = config change + reload — acceptable for this role; cloud's control plane injects per-deployment env, which fits.
- Loud migration — existing deployments carrying the row-id grant: derivation prefers env; a detected legacy grant logs a deprecation pointing at the config path; removal of the legacy path is a sequenced follow-up, never a silent dual-track.
Related tasks assessed for joint handling (maintainer asked; triage's reading)
| card | relation | sequencing |
|---|---|---|
#10348 (ruled C today, pm:queue) |
Consolidates the two stray plugin-auth judges onto resolve-authz-context.ts — the very function this design rewires |
Land #10348 first: then the derivation changes in exactly one place. The design card's implementation must not start before it. |
#10949 (pm:blocked, live authz defects in those judges) |
May be partially absorbed by #10348-C | Re-priced there (already noted on #10348). |
| objectstack#11211 (merged: walled owner-email elevation) + the #10103 "env-door projection" | An email/env elevation mechanism already partially exists. | |
| cloud#1509 (P0: first self-registrant becomes platform_admin, bypassing invite-only) | The env-email anchor is the structural fix for this class: self-registration must never confer platform admin; only configured emails do | Design must state the walled bootstrap flow end-to-end; a cloud-side leg follows the design (coordinate with the cloud seat's #1614→#1509 train — do not collide with its in-flight acceptance). |
| #11532 (closed today: honest diagnostic) | The 8 org-less rows' retirement becomes possible once nothing references admin_full_access by row id |
Seeders stop minting + reap of the 8 rows is the implementation's final leg (the other 7 rows have per-org copies already; verify zero readers before reaping). |
Not related despite proximity: #11579 (auth settings pre-bind), #11627 (MySQL hash keys), #10556 (sinks).
Discipline
Priority: maintainer-ordered front-of-queue (priority:p0 per the verbatim above — jump authorization recorded; ⛔ does not exempt claim protocol or serial constraints). Clause-②: yes (changes authorization derivation — the widest contract there is); contract-review tier mandatory on the implementation. Deliverable of THIS card: the design document (options where real choices remain — e.g. env spelling, multi-admin, reload semantics — with a four-axis pass), then split implementation cards per surface (packages/core derivation · kernel declaration · plugin-security seeders/reap · migration · cloud leg).
Refs: #10103 (ruling + fork analysis) · #11532 (measured constraints: row-id derivation, bootstrap-before-first-org) · #10348 (consolidation prerequisite) · cloud#1406/#1509 · #2734 · ADR-0068 · ADR-0120 D3.
Activity
os-zhuang commentedon Aug 24, 2026
Priority clarification (triage seat, maintainer instruction 2026-08-24, verbatim: 「卡片设置为优先,不是让你派发」): the
priority:p0on this card is queue ordering only — it puts this card at the head of the services lane's selection order. It does not carry the jump-the-round / immediate-dispatch semantics; no dispatch instruction accompanies it from triage (which never dispatches). The owning seat picks it up in its normal cycle, honoring the sequencing prerequisite already stated (#10348 lands first).Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
Premise measurement delivered (triage seat, 2026-08-24, maintainer asked; read from
origin/main)The "measure the landed mechanism" prerequisite in the body is now answered —
OS_PLATFORM_OWNER_EMAILexists and is the WHO half of this card, already built (#11184/#11211, maintainer ruling 2026-08-23 verbatim: 「1509 选择 env 指定 owner 邮箱」):packages/types/src/env.ts:172—PLATFORM_OWNER_EMAIL_ENV = 'OS_PLATFORM_OWNER_EMAIL'+resolvePlatformOwnerEmail().packages/plugins/plugin-security/src/bootstrap-platform-admin.ts— posture-keyed elevation:single= first human user (unchanged, ruled reasonable); walled = only the account matching the declared owner email, first-registrant path removed; walled + undeclared ⇒ elevation refused loudly here and boot refused in plugin-authinit()(fail-closed, names the variable); requested-not-enforced posture is deliberately the input (degraded tenancy still fails strict).'Operator@Corp.EXAMPLE'test), blank-var-is-undeclared pinned, system-account exclusion handled.What is unchanged — and is exactly this card's remaining scope: the elevation still inserts a
sys_user_permission_setrow pointing at the org-lessadmin_full_accessrow (cross-tenant grant,organization_id = NULL), and the seeder still mints the org-less set rows. The WHO half is modern; the ANCHOR half is the legacy this card retires.Design consequences (narrowing the card):
OS_PLATFORM_OWNER_EMAIL— no second email door. The unification target named in the body is this variable, now confirmed.Provenance: maintainer question in live PM chat (「好像企业版已经有了环境变量 OS_PLATFORM_OWNER_EMAIL,是一件事吗」) — answer: same lineage, two halves; the WHO half landed 08-23, this card is the ANCHOR half.
Generated by Claude Code
claude commentedon Aug 24, 2026
Design — re-anchoring platform-admin on a kernel declaration + env-configured verified emails
Design-only delivery (no product code, no PR, no ADR, no implementation cards filed).
Everything below is measured against
origin/mainatcad8b42f00plus the open diff ofPR #11639 (card #11343, head
cd8a891892), read as instructed because it changes exactly thedoor this card is told to unify onto.
0. Headline — four things measurement changed about the card's premise
H1. The ruled direction #1 ("the permission set's CONTENT becomes a kernel metadata
declaration") is already ~90% true at enforcement time. It is not a change that has to be
built; it is a change that has to be finished.
packages/plugins/plugin-security/src/objects/default-permission-sets.ts:119— every entry,admin_full_accessincluded, is alreadyPermissionSetSchema.parse({ ... }): code-declared,spec-validated, versioned, PR-reviewed.
packages/plugins/plugin-security/src/security-plugin.ts:897— that same array is registeredon the manifest as
permissions:, i.e. it is thepermissionmetadata namespace.packages/plugins/plugin-security/src/permission-evaluator.ts:398resolvePermissionSetsresolves a set NAME against metadata
list('permission')first, the bootstrap arraysecond, and
sys_permission_setrows only last.So for the shipped names the row is not the enforcement content today — the declaration is.
(A corollary worth stating out loud: a Setup edit to the
admin_full_accessrow does notchange what
admin_full_accessgrants, because metadata wins first. The "operators cancustomize the row" argument for keeping it is weaker than it looks.)
What the row still supplies is narrower than the card implies, and it is all in one place:
grants.permissionsmembership,system_permissions,tab_permissions, and theunscopedUserPsIdsanchor.H2. Nothing in production code references
admin_full_accessby a hard-coded row id.The anchor is a composite runtime FK, not a magic id —
resolve-authz-context.ts:573:i.e. an unscoped, in-window
sys_user_permission_setrow whosepermission_set_idresolvesto an
activerow namedadmin_full_access. Zero-hit grep for hard-codedps_*literals innon-test package sources, with a positive control over test sources that returned matches (the
control is what makes the zero meaningful).
H3. The card's "the other 7 rows have per-org copies already" undercounts in one direction
and over-promises in another.
admin_full_accessincluded— the manifest registers
defaultPermissionSetsunderpermission, andbootstrapDeclaredPermissionsmaterializes declared permission metadata per organization.What is special about the org-less
admin_full_accessrow is not that it is the only onewithout a copy; it is that the unscoped grant points at it.
packages/plugins/plugin-security/src/auto-org-admin-grant.ts:223resolvePermissionSetIdresolvesorganization_adminname-only,limit: 1, with noorganization scope, cached per-ObjectQL-instance. On a walled deployment the org-less row is
the oldest row bearing that name, so org-admin grants can point at an org-less row by id.
Reachable by construction; not observed on a live rig from this seat (see section 9).
⇒ the reap is gated on a per-name junction census and on scoping that resolver first.
H4. The sharpest concrete breakage of "no grant row" is not in the derivation — it is in the
Default-Organization bootstrap, and it breaks twice over.
packages/plugins/plugin-auth/src/ensure-default-organization.ts:110-130finds the platformadmin by reading
sys_permission_setby name thensys_user_permission_setgrants. With nogrant rows it answers
reason: 'no_admin'forever — silently, because that is a legitimatereturn value today.
packages/plugins/plugin-auth/src/auth-plugin.ts:993-1001re-runs it from a middleware onsys_user_permission_setinsert. With no grant insert, the trigger never fires.This is also a fourth, id-shaped platform-admin read that #10348 does not enumerate (its
three are all in
auth-manager.ts). It differs in kind: it asks the population question("who is the platform admin?"), not the per-user predicate, so #10348-C does not absorb it.
Recommend adding it to that sweep or filing it beside it.
1. The one door: unify onto #11343, and move it from write-time to read-time
Conclusion, stated plainly as the dispatch asks: yes — #11343's hardened door is the one door.
Do not build a second mechanism. The env variable, the normalization, the fail-closed
verified predicate and the walled/
singlesplit all stay. What changes is when the door isconsulted:
sys_user_permission_setrowhasPlatformAdminGrantin memoryTwo consequences worth naming because they argue the design simplifies rather than duplicates
#11343:
shouldReplayBootstrapFor's elevation motive dissolves. [security] 围墙提权只按未验证邮箱匹配 —— #11184 落地后仍存在一条更窄的同形路径:抢在 owner 之前用其邮箱注册即可被提权 #11343 needs an update triggerbecause elevation is a write that must be re-attempted after verification. Read-time
derivation has nothing to re-attempt: the request after verification simply resolves as
admin. The predicate stays only for whatever else the bootstrap still owns (default-org
binding), which is a strictly smaller job.
isEmailVerifiedis promoted, not rewritten. It is currently a private functionin
bootstrap-platform-admin.ts. It moves to@objectstack/core/securitybeside itssiblings
isGrantActive/isRowActive, and plugin-security imports it back. That is howpin Add metamodel interfaces for ObjectQL/ObjectUI contract #1 gets one mechanical definition rather than two.
⛔ Nothing here asks for #11639 to change or be held. It lands as ruled; this design consumes it.
2. End-to-end walled bootstrap flow (proposed)
variable unset ⇒ boot refusal, already shipped and pinned
(
plugin-auth/src/auth-plugin-walled-owner-boot-refusal.test.ts).bootstrapPlatformAdminseeds permission-set rows (for display/catalog only — seeChoice 5) and writes no grant. It logs the resolved admin list, normalized, and for each
entry: does an account exist, is it verified. This log is the operator's first sight of the
answer, and it is the same answer the auditability surface serves.
email_verifiedis false ⇒ ordinary member.Nothing is elevated, so there is nothing to revoke, and no window exists in which a row
grants standing that policy would refuse.
middleware, no write. The next request resolves: the caller's own
sys_userrow has anormalized email in the configured list and
email_verifiedreads verified ⇒hasPlatformAdminGrant = true⇒positionsleads withplatform_admin,postureisPLATFORM_ADMIN,permissionsincludesadmin_full_access,systemPermissionscome fromthe declaration.
ensureDefaultOrganizationis re-pointed at the new oracle and itstrigger moves from
sys_user_permission_setinsert to the [security] 围墙提权只按未验证邮箱匹配 —— #11184 落地后仍存在一条更窄的同形路径:抢在 owner 之前用其邮箱注册即可被提权 #11343sys_usertrigger set(insert, or update touching
email/email_verified). Without this leg the walled flowstops at step 4 with an admin who has no organization (H4).
changed, drops standing on the next request with no row to clean up.
3. The five ruled pins, mechanically (plus three the measurement forces)
P1 — verified-email match only. Mechanism, not assertion:
sys_userrow through the resolver's memoizedgetUserRow()(resolve-authz-context.ts:341), specificallyemailandemail_verified.grants.email. Measured:resolve-authz-context.ts:293and:332—opts.seedEmail, a caller/session-supplied email, is seeded intogrants.emailand winsover the
sys_userread. Deriving superuser standing from a session-supplied string would bea new escalation channel introduced by the very change meant to close one. This is the single
most important mechanical pin in the document.
isEmailVerifiedis one exported predicate: fail-closed allow-list (true,1,'1','true'), absent field reads unverified. Shared by core and plugin-security.trim().toLowerCase()on both sides, one function, one call site each.email_verifiedfalse / absent /0resolves non-admin; a session payload carrying a configured address over a
sys_userrowthat does not resolves non-admin (this is the pin that fails if someone reaches for
grants.email).P2 — empty config = fail closed.
resolvePlatformAdminEmails()returns an empty list forunset/blank; the derivation returns false on an empty list before touching the user row.
Walled + empty ⇒ boot refusal (shipped). The dev-admin seed stays where it is —
NODE_ENV=developmentplus DevPlugin, whose own header already refuses production(
packages/plugins/plugin-dev/src/dev-plugin.ts:107,131) — and gains nothing from this card.P3 — auditability. This is load-bearing, not a reporting nicety. With no grant rows, the
inverse question ("who are the platform admins?") loses its only implementation, and it has
at least four in-repo consumers:
ensure-default-organization.ts:110,last-admin-guard.ts:764,bootstrap-platform-admin.ts:276(already_have_admin), andplugin-security/src/explain-engine.ts:403.⇒ Propose a
platformAdminservice registered besidetenancy(
plugin-auth/src/auth-plugin.ts:584is the precedent), exposing read-only:configuredEmails()(normalized; plus whether the source is set at all) andstanding()(per configured email: does an account exist, is it verified, which user id). Surfaced through
security/explainand a read-only Setup panel. Never writable — there is deliberately noruntime path that changes who is a superuser.
P4 — revocation = config change + reload. See Choice 3.
P5 — loud migration. Derivation prefers config. A legacy unscoped
admin_full_accessgrantstill confers during a time-boxed window and is logged once per process (not per request)
naming the row, its holder and the exact config line to add.
bootstrapPlatformAdminstopsminting new grants at the same landing — that is the fail-closed half, and it is what keeps the
window from being a silent dual-track. Removal is a separately filed card, filed at the same
time, not "later".
Three more the measurement forces:
P6 — the admin standing surface must move with the derivation.
packages/core/src/security/admin-standing-surface.tsis a measured, test-enforceddeclaration of what the derivation reads, bound to plugin-auth's break-glass guard by
last-admin-standing-keys.test.ts. Todaysys_userisrole: 'reads-only'with the reason"Neither confers administrator standing." After this change
sys_user.emailandsys_user.email_verifiedbecome derivation columns — a write flippingemail_verifiedtofalse, or a change-email write, can remove the last administrator. That is a new
lockout-capable write class the break-glass guard does not watch today. It must either be added
to a standing-key list or excluded with a written reason; the correspondence test will be red
until it is. The surface also needs a way to express a non-table input (the config), or it
will silently claim the derivation reads only tables.
P7 — suspended / banned accounts. Today
bannedis deliberately not a derivation columnbecause the session layer refuses banned sessions. Under config anchoring the same argument
holds for session traffic, but the API-key and MCP admission paths reach
resolveUserAuthzGrantswith auserIdand no session. This must be measured in the coreleg, not asserted here — the honest state is "unverified".
P8 —
security/explainmust report config-derived standing. Otherwise explain andenforcement disagree, which is precisely the failure class the code already records
(
permission-evaluator.ts:283,sharing-service.ts:563,739).4. Open choices — options, four-axis passes, ranked recommendations
The four axes: real business need · long-term soundness for this project · making
AI-written code hard to get wrong · startup scope discipline.
Choice 1 — env spelling
OS_PLATFORM_OWNER_EMAIL, widening its accepted value.OS_PLATFORM_ADMIN_EMAILS, deprecating the old viareadEnvWithDeprecation(packages/types/src/env.ts:52is the precedent).Recommend 1A.
Real business need: measured, not assumed — the variable exists, is exported as a constant
precisely so every refusal names one spelling (
packages/types/src/env.ts:172), is set by theverify harness (
packages/verify/src/harness.ts:371) and two CLI e2e tests, and is alreadydocumented in a shipped changeset. No deployment is asking for a different name.
Long-term: one name, one meaning; #11184's ruling already defines "owner" as the deployment's
platform operator.
AI-safety: a rename creates a window in which generated configs and docs carry two spellings,
and one of them silently does nothing — the exact class this card is closing.
Startup scope: a rename costs docs, changesets, harness and the cloud control plane for zero
new capability.
Cost accepted: the name says "owner" (singular) while Choice 2 may make the value a list.
Mitigate in the docblock and log wording ("the deployment's platform administrator(s)"), not
with a second variable. 1C is rejected outright: two doors is the thing the card forbids.
Choice 2 — single admin or several
OS_CORS_ORIGIN,packages/adapters/hono/src/index.ts:150).Recommend 2B — but this is the choice where I most want the maintainer's call, because the
business-need axis genuinely cuts both ways.
Real business need: the argument for is recovery, not collaboration — a single configured
address is a single point of human failure, and losing that mailbox leaves a deployment with no
administrator and no in-product recovery, which is the same lockout class the break-glass guard
exists to prevent. The argument against is that I measured no deployment asking for two
administrators, and mainstream precedent is split: Grafana
GF_SECURITY_ADMIN_USER, GitLabroot and Keycloak
KEYCLOAK_ADMINare all singular; Kubernetes RBAC is a list.Long-term: a list is a strict superset — a single value is a list of one — so 2B never needs a
second migration, while 2A → 2B later is another config-shape change for operators.
AI-safety: this axis has a hard requirement attached. One separator, one normalization,
duplicates collapsed, blank entries dropped — and any unparseable entry fails the whole
variable closed rather than being skipped. Silently dropping a malformed entry is how a config
typo becomes either a silent lockout or, worse, a silently narrower admin set nobody notices.
Startup scope: one parse function, roughly 15 lines, no new surface. Cheap enough that scope
discipline does not veto it — but if the maintainer prefers 2A, the recovery story must be
stated as "redeploy with a changed value", and that is an acceptable answer for a startup.
2C is rejected: a second variable is a second door.
Choice 3 — reload semantics
resolvePlatformOwnerEmailalready does—
packages/types/src/env.ts:200: "Readsprocess.envlive on each call").Recommend 3A, i.e. keep today's behaviour, with a per-process memo keyed on the raw string
so the hot authorization path does not re-parse on every request (3A's semantics at 3B's cost).
Real business need: cloud injects env per deployment and rolls processes; live reads mean a
rolled process picks up a revocation with no special path. Pin #4 calls revocation urgency real.
Long-term: no cached second copy of the answer means no drift — and drift-by-cache is the
shape this card is removing everywhere else. 3B introduces exactly one.
AI-safety: decisive against 3C. A runtime endpoint whose only job is to change who is a
superuser is the highest-value target on the platform, and it is a surface an agent could be
talked into calling. 3A adds no mutation surface at all.
Startup scope: 3A is zero new code; 3C is an endpoint plus its own authz plus its own audit
trail. 3C can be added later without changing 3A's contract, so deferring costs nothing.
Choice 4 — does the
singleposture change too? (the biggest fork here)singlekeeps first-user promotion and its grant row.singlewith no config falls back to first-userin development only.
Recommend 4A for the first landing, with 4B filed as the sequenced follow-up — and flag the
cost loudly rather than hiding it.
Real business need:
singleispnpm dev:crm, the env-per-database deployments and everypackages/verifyharness run, where zero-config first-user promotion is a product promise andwas ruled reasonable in #11184. 4C breaks all of it on day one.
Long-term: the honest cost of 4A is that the ruling is only half-achieved. Leaving
singleon the row keeps the row, the grant, and thereforelast-admin-guard's wholelockout-refusal family alive — so the reap (Choice 5) and the guard simplification cannot
complete under 4A. That is why 4B should be sequenced, not dropped.
AI-safety: two derivation shapes keyed on posture is drift bait. Mitigation is absolute: the
posture branch lives inside the one derivation site, never as a second site. If that cannot
be held, prefer 4B sooner.
Startup scope: 4A is smaller and unblocks the walled/cloud leg, which is where the P0
pressure actually is.
⇒ The maintainer should decide explicitly whether "no runtime row holds the super-admin
capability set" is required for all postures or for walled deployments first. The rest of the
design is unchanged either way.
Choice 5 — the 8 org-less
sys_permission_setrowssingle; defer the reap.Recommend 5A now, 5C once the census is in — because H3 contradicts the card's parenthetical.
The census must be per name, over
sys_user_permission_set,sys_position_permission_setandsys_user_position, on a real walled rig, andauto-org-admin-grant.ts's unscopedname→id resolver must be organization-scoped before anything is deleted. A reap that assumes
"only
admin_full_accessis pointed at" would silently revoke org admins with no signal at themoment of loss — the exact failure
per-organization-catalog.tsalready warns about in itsheader.
Choice 6 — how the declared capability set reaches
grantsToday §6b fills
grants.systemPermissions/tabPermissionsfrom the row. With no row, thatcontent needs another route into
@objectstack/core.@objectstack/specexports the platform-admin capability declaration; core reads it,and plugin-security's
admin_full_accessdeclaration imports the same list so there isone copy. (Measured: core already imports
ADMIN_FULL_ACCESSfrom@objectstack/spec.)resolvePermissionSetshydrate the content.resolveAuthzContextopts.Recommend 6A, and it is a spec-seat leg — ⛔ not taken in this lane.
Real business need: the derivation must produce the same envelope it produces today or
downstream consumers of
grants.systemPermissionschange behaviour silently.Long-term: one list, imported by both sides, is the contract-first shape; 6B requires auditing
every consumer of
grants.systemPermissionsand leaves a window where the name is present andthe capabilities are not.
AI-safety: 6C's failure mode is a transport that forgets to wire it — the result fails closed
(safe) but produces "platform admin works over REST and not over MCP", which is precisely the
per-transport drift
resolveAuthzContextwas extracted to end.Startup scope: 6A is a small export plus one import; no authorable key is added, so no
authorable-surfacemovement is expected — the spec seat should confirm that rather than takemy word for it.
Choice 7 — how the config answer reaches
@objectstack/coreMeasured dependency facts:
@objectstack/coredepends on@objectstack/specandzodonly;@objectstack/types(home ofPLATFORM_OWNER_EMAIL_ENVandresolvePlatformOwnerEmail)depends on
@objectstack/spec. Socore → typesis acyclic and legal.@objectstack/coreadds a dependency on@objectstack/typesand calls the resolver.@objectstack/spec(spec-seat leg).resolveAuthzContextopts.Recommend 7A — smallest true change, keeps the config read inside the single derivation
site (which is the whole point), and avoids putting env reading into
spec, which is currentlypure schema and constants. Same fail-closed-but-drifting objection to 7C as in Choice 6.
5. Migration sequence
Ordered; each step is safe to stop at.
ensure-default-organization.tsto its sweep,or file it beside it — it is a fourth id-shaped read of a different kind (population, not
predicate), so C as specified does not absorb it.
Behaviour-neutral, lands alone.
Config-derived standing is added; the legacy grant read is still honoured (P5). Nothing
is revoked in this step — this is what makes it safe to land ahead of every deployment
setting the variable.
bootstrapPlatformAdminstops writing grants;ensureDefaultOrganizationand its trigger are re-pointed (H4); the
platformAdminservice is registered; explainreports config-derived standing. This is the step where the deprecation log starts firing.
last-admin-guard's refusals are re-judged against a world where noruntime write can empty the platform-admin population. Several of its refusals become
obsolete (deleting / renaming / deactivating the
admin_full_accessrow no longer un-makesevery platform admin) and several become newly necessary (P6). ⛔ Do not delete refusals as
a side effect of step 4 — this is its own reviewed step.
step 4's landing, not later.
auto-org-admin-grant's resolver; run the census; only then reap.singleposture (Choice 4B), if ruled.6. Proposed per-surface implementation split
Proposed, not filed — filing waits on acceptance.
packages/specpackages/coresecurity/platform-admin.ts(promotedisEmailVerified, normalization, list parse, derivation); §6b config branch;+ @objectstack/typesdep;ADMIN_STANDING_SURFACEupdateneeds:contract-reviewplugin-authensure-default-organizationre-pointed + trigger moved;last-admin-guardre-priced (step 5); post-C judge reads the same oracleneeds:contract-reviewplugin-securityisEmailVerifieddeleted in favour of core's; replay narrowed;platformAdminservice;explainreports config standingneeds:contract-reviewauto-org-admin-grant+ stop minting + reapcontent/docs/permissions/authorization.mdx:116,336,385,412;permission-sets.mdx:153; operator runbook for the variablecontent/docs/releases/Two costs that belong on the cards rather than being discovered mid-implementation:
resolve-authz-context.ts:365currently readssys_useronlywhen
needsUserRow; readingemail_verifiedmakes it unconditional for authenticatedprincipals, and the query multiset is pinned in
resolve-authz-context.batch-equivalence.test.ts+.golden.json. That golden must be movedwith an explanation, never silently re-baselined.
packages/verify/src/harness.ts:360-371and twopackages/clie2e tests set the owner variable; after [security] 围墙提权只按未验证邮箱匹配 —— #11184 落地后仍存在一条更窄的同形路径:抢在 owner 之前用其邮箱注册即可被提权 #11343 their fixture users also needthe verified shape.
7. What the design does not re-open
The five ruled pins are designed to, not re-litigated. The direction (declaration for content,
config for holder) is ruled. Choice 4 is not a re-opening of that ruling — it is a question
about the scope of the first landing, which the ruling does not settle.
8. If the maintainer wants the cheapest defensible answer
Steps 1–4 plus Choice 1A / 2B / 3A / 4A / 5A / 6A / 7A deliver, for walled deployments:
no runtime row confers platform admin, no runtime write can empty the administrator population,
verified-email-only holders, fail-closed empty config, an auditable read-only standing surface,
and a loud time-boxed migration.
singleposture and the org-less-row reap follow behind.That is the ruling's substance at the point where the P0 pressure actually is.
9. What I could not measure
Stated explicitly, per instruction — these are gaps, not conclusions.
blind from what [Design] Re-anchor platform-admin:
admin_full_accessbecomes a kernel metadata declaration; WHO holds it comes from env-configured verified emails — retiring the org-less row anchor #11663 records, and needs the cloud seat's review before it is filed. Noinference about their contents is made here, and nothing here should be read as coordinating
with their in-flight acceptance train.
sys_permission_setrows, and the per-organization pass then reports them as "pre-fix" leftovers with a remedy that recreates them #11532's measurements — the 1.3 s bootstrap-before-first-orgordering, the 8 org-less rows, the 14-per-organization catalog — are carried forward from
its record, corroborated only at code level (the bootstrap runs on
kernel:ready;ensureDefaultOrganizationfires from a grant insert that necessarily happens later). Notre-observed against a real database from this seat.
observed. The resolver's shape (name-only,
limit: 1, unscoped, per-instance cache) makesit reachable; which row a real driver returns first on a real walled rig was not measured.
This is exactly why Choice 5 gates the reap on a census.
resolveUserAuthzGrantsis reachable with auserIdand no session; whether accountsuspension gates those paths must be measured in L2 before the derivation lands.
nothing of mine to verify. Every claim above is a read of
origin/mainatcad8b42f00or ofPR fix(plugin-security): walled elevation requires a verified owner-email match, and the bootstrap replays on the verifying update #11639's diff at
cd8a891892. Both are open to being wrong in the same way any staticread is; the ones I would re-check first are H3 and P7.
control was available, the claim is stated as a code-shape reading rather than an absence.
Generated by Claude Code
Generated by Claude Code
claude commentedon Aug 24, 2026
{ "issue": 11663, "status": "done", "branch": "claude/issue-11663-platform-admin-reanchor-design", "pr": null, "premise_still_valid": true, "summary": "Design-only dispatch: no product code, no PR (pr:null is the expected delivery), no ADR, no implementation cards filed. Delivered the design document as issue comment 5394453215 on #11663 — end-to-end walled bootstrap flow, all five ruled security pins expressed mechanically plus three more that measurement forces, a seven-fork options section with four-axis passes and ranked recommendations, a nine-step migration sequence, an eight-leg per-surface split with dependencies, and an explicit 'what I could not measure' section. Core conclusion, stated plainly as asked: #11343's hardened door (PR #11639, read as instructed) IS the one door — do not build a second mechanism; keep OS_PLATFORM_OWNER_EMAIL, the normalization and the fail-closed verified predicate, and move the decision from bootstrap-WRITE to read-time derivation in resolve-authz-context. That dissolves #11343's replay-for-elevation motive rather than duplicating it, and promotes its isEmailVerified predicate into core so pin #1 has one definition. The card's overall premise holds, but measurement corrected three sub-premises and surfaced one hard breakage; see open_questions and the document. (a) Ruled direction #1 is already ~90% true: admin_full_access's CONTENT is already PermissionSetSchema.parse'd code (default-permission-sets.ts:119), registered as `permission` metadata (security-plugin.ts:897), and permission-evaluator.ts:398 resolves a set NAME against metadata FIRST, the bootstrap array SECOND and sys_permission_set rows only LAST — so the row is not the enforcement content today. (b) Nothing in production code hard-codes the row id; the anchor is a composite runtime FK (resolve-authz-context.ts:573), verified by a zero-hit grep with a passing positive control. (c) The card's 'the other 7 rows have per-org copies' both undercounts (all 8 get per-org copies, admin_full_access included) and over-promises (auto-org-admin-grant.ts:223 resolves organization_admin name-only/limit-1/unscoped/cached, so org-admin grants can point at an org-less row — filed as #11670, and the reap leg is gated on a real-rig census). (d) The sharpest breakage of 'no grant row' is NOT the derivation but the Default-Organization bootstrap, which breaks twice over: ensure-default-organization.ts:110-130 answers reason:'no_admin' forever with no grant rows, and auth-plugin.ts:993-1001 triggers it from a sys_user_permission_set INSERT that never happens. That file is also a fourth id-shaped platform-admin read of a different KIND (population, not per-user predicate) that #10348-C does not absorb — noted on #10348 as comment 5394472723. One mechanical pin matters above all others and is called out as such in the document: the env match must read the caller's OWN sys_user row via the memoized getUserRow(), never grants.email, because resolve-authz-context.ts:293/332 lets a caller/session-supplied opts.seedEmail WIN over the sys_user read — deriving superuser standing from that string would open a new escalation channel in the change meant to close one. Implementation is gated on maintainer rulings for the seven forks; the biggest is Choice 4 (does the `single` posture re-anchor too — under walled-only, the ruling is achieved for walled deployments only and the reap plus the last-admin-guard simplification cannot complete).", "tests": "No builds and no tests were run, and none were owed: this dispatch writes no product code, so there is no change of mine to verify — stated in the document's section 9 rather than left implied. The verification that WAS owed is measurement discipline, and it was taken: every read is `git grep <ref>` against origin/main at cad8b42f00 in a dedicated worktree (never the shared checkout, whose HEAD other agents switch), plus PR #11639's diff at head cd8a891892 via the GitHub API. Exit codes were captured before any pipe (one self-caught instance: an early `grep ... | head; echo $?` read `head`'s status, and the check was re-run as `grep > file; echo $?` — the corrected run reported CTRL_GREP_EXIT=1, i.e. genuinely no control bytes in the document). Zero-hit discipline: the load-bearing zero — 'no production code hard-codes the admin_full_access row id' — was run WITH a positive control through the same channel: pattern `'ps_[a-z0-9_]+'` over packages/**/src/** excluding tests returned only one unrelated hit in a test fixture, while the same pattern over **.test.ts returned matches (control PASSED, so the zero means something). Identity over counts: every claim in the document cites file:line, not a count. The design document was posted and then READ BACK and diffed byte-for-byte against the source: identical up to GitHub appending its own bare-form footer — nothing was stripped by the sanitizer. Control-byte scan on every file authored: clean. The verify lock was never taken because nothing heavy was run.", "open_questions": [ { "question": "Choice 4 (the biggest fork): does the `single` tenancy posture re-anchor too, or is the first landing walled-only? This decides whether the ruling's 'no runtime row holds the super-admin capability set' is achieved platform-wide or for walled deployments only.", "options": [ "4A walled postures only; `single` keeps first-user promotion and its grant row", "4B all postures config-anchored, with a development-only first-user fallback when no config is present", "4C all postures config-anchored, no fallback" ], "recommendation": "4A for the first landing with 4B filed as the sequenced follow-up — but the cost must be accepted explicitly, not glossed: leaving `single` on the row keeps the row, the grant and therefore last-admin-guard's whole lockout-refusal family alive, so the reap and the guard simplification CANNOT complete under 4A. 4C breaks `pnpm dev:crm`, the env-per-database deployments and every packages/verify harness run on day one, and #11184 already ruled first-user promotion reasonable under `single`. 4A is smaller and unblocks the walled/cloud leg, which is where the P0 pressure actually is. Mitigation that is not optional: the posture branch lives INSIDE the one derivation site, never as a second site." }, { "question": "Choice 2: one configured admin email or several? (the choice where the business-need axis genuinely cuts both ways)", "options": [ "2A exactly one email (status quo)", "2B comma-separated list on the same variable (precedent: OS_CORS_ORIGIN, adapters/hono/src/index.ts:150)", "2C a list plus a separate break-glass variable" ], "recommendation": "2B, with fail-closed parsing — but I want the maintainer's call, because I measured no deployment asking for two administrators and mainstream precedent is split (Grafana, GitLab root and Keycloak are singular; Kubernetes RBAC is a list). The argument FOR is recovery, not collaboration: one configured address is a single point of human failure, and losing that mailbox leaves a deployment with no administrator and no in-product recovery — the same lockout class the break-glass guard exists to prevent. A list is a strict superset so 2B never needs a second migration. Hard requirement attached on the AI-safety axis: one separator, one normalization, duplicates collapsed, blanks dropped, and ANY unparseable entry fails the whole variable CLOSED rather than being skipped — silently dropping a malformed entry is how a config typo becomes a silent lockout or a silently narrower admin set. 2C rejected: a second variable is a second door. 2A is defensible for startup scope, in which case the recovery story must be stated as 'redeploy with a changed value'." }, { "question": "Choice 1: env spelling — reuse the existing variable or introduce a new one?", "options": [ "1A reuse OS_PLATFORM_OWNER_EMAIL, widening its accepted value", "1B new OS_PLATFORM_ADMIN_EMAILS, deprecating the old via readEnvWithDeprecation", "1C keep both with different meanings (owner vs admins)" ], "recommendation": "1A. The variable already exists and is exported as a constant (types/src/env.ts:172) precisely so every refusal names ONE spelling; it is set by the verify harness (harness.ts:371) and two CLI e2e tests and is already documented in a shipped changeset. A rename creates a window in which generated configs carry two spellings and one silently does nothing — exactly the class this card closes — and costs docs, changesets, harness and the cloud control plane for zero new capability. Accepted cost: the name says 'owner' (singular) while Choice 2 may make the value a list; mitigate in docblock and log wording, not with a second variable. 1C rejected outright." }, { "question": "Choice 3: reload semantics for the configured admin list (pin #4 says revocation is config change + reload)", "options": [ "3A read the config live on each derivation (what resolvePlatformOwnerEmail already does — types/src/env.ts:200)", "3B snapshot at boot; change requires a restart", "3C snapshot plus an explicit privileged reload trigger" ], "recommendation": "3A, i.e. keep today's behaviour, with a per-process memo keyed on the raw string so the hot authorization path does not re-parse per request (3A's semantics at 3B's cost). The AI-safety axis is decisive against 3C: an endpoint whose only job is to change who is a superuser is the highest-value target on the platform and a surface an agent could be talked into calling. 3B introduces a cached second copy of the answer — the drift-by-cache shape this card removes everywhere else. 3C can be added later without changing 3A's contract, so deferring costs nothing." }, { "question": "Choice 5: the 8 org-less sys_permission_set rows — stop minting, reap, or leave as ruled?", "options": [ "5A stop minting under walled posture, keep for `single`, defer the reap", "5B keep minting and reap nothing (status quo, the 2026-08-20 ruling)", "5C stop minting AND reap, after a reader census" ], "recommendation": "5A now, 5C once the census is in. The card's 'verify zero readers before reaping' is not a formality: I measured that auto-org-admin-grant.ts:223 resolves organization_admin name-only, limit 1, unscoped and process-cached, so org-admin grants can point at an org-less row (filed as #11670). The census must be per name over sys_user_permission_set, sys_position_permission_set and sys_user_position on a REAL walled rig, and that resolver must be organization-scoped BEFORE anything is deleted — per-organization-catalog.ts's own header already warns these rows are grant TARGETS and deleting them 'revokes standing access with no signal at the moment of loss'." }, { "question": "Choice 6 (SPEC-SEAT LEG, not taken in this lane): how does the declared capability set reach `grants.systemPermissions` / `tabPermissions` once there is no row to read them from?", "options": [ "6A @objectstack/spec exports the platform-admin capability declaration; core reads it and plugin-security's admin_full_access declaration imports the same list", "6B core pushes only the NAME and lets resolvePermissionSets hydrate the content", "6C the composition layer injects the declaration through resolveAuthzContext opts" ], "recommendation": "6A, routed to the spec seat — ZERO packages/spec ownership was taken here. Core already imports ADMIN_FULL_ACCESS from @objectstack/spec, so the dependency exists; one list imported by both sides is the contract-first shape. 6B needs an audit of every consumer of grants.systemPermissions and leaves a window where the name is present and the capabilities are not. 6C fails CLOSED if a transport forgets to wire it, which is safe but produces 'platform admin works over REST and not over MCP' — the per-transport drift resolveAuthzContext was extracted to end. No authorable key is added so no authorable-surface movement is expected, but the spec seat should confirm that rather than take my word for it." }, { "question": "Choice 7: how does the config answer reach @objectstack/core, whose dependency set is currently only @objectstack/spec + zod?", "options": [ "7A @objectstack/core adds a dependency on @objectstack/types and calls resolvePlatformOwnerEmail directly", "7B move the env resolver into @objectstack/spec", "7C injection through resolveAuthzContext opts" ], "recommendation": "7A. Measured and acyclic: @objectstack/types depends on @objectstack/spec, @objectstack/core depends on @objectstack/spec + zod, and types does not depend on core — so core -> types is legal. It keeps the config read INSIDE the single derivation site, which is the entire point of the re-anchor. 7B puts env reading into a package that is currently pure schema and constants. 7C carries the same fail-closed-but-drifting objection as 6C." } ], "out_of_scope_findings": [ "filed as #11670: auto-org-admin-grant resolves the organization_admin set id by name alone (limit 1, unscoped, process-cached), so walled org-admin grants can point at the organization-less row — unassigned, unlabeled, searched for duplicates first (none); the #11663 reap leg is gated on it", "not filed, recorded as a comment on #10348 (5394472723) because it is squarely that dispatched card's own predicted scope: ensure-default-organization.ts:110-130 is a FOURTH id-shaped platform-admin read of a different KIND — it asks the population question ('who is the platform admin?') rather than the per-user predicate, so a (userId) => boolean oracle cannot absorb it; its trigger (auth-plugin.ts:993-1001, middleware on sys_user_permission_set insert) shares the same anchor" ] }Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
Maintainer ruling — design ACCEPTED, recommended bundle in full
Source: maintainer, 2026-08-25, live PM chat (decision-inbox batch 1 review, session
session_01KWRU3s15AJz7PGW7a7wdCh), verbatim: 「接受你的建议,继续」 — accepting this batch's presented recommendation for this card, which was the design document (comment 5394453215) with its recommended bundle:OS_PLATFORM_OWNER_EMAIL; no second variable, ever (1C rejected).singlekeeps first-user promotion for now. 4B is ruled as the sequenced follow-up, not dropped (card filed, see checklist below). The posture branch lives inside the one derivation site, never a second site.auto-org-admin-grant's resolver BEFORE any reap.core → typesdependency; config read stays inside the single derivation site.Constraints that travel with every implementation card: #10348-C lands before L2 starts; clause-② /
needs:contract-reviewon L2–L4; the L2 pinned batch-equivalence golden moves with an explanation, never silently re-baselined; cloud leg (L8) was designed blind and requires the cloud seat's review.State:
needs-user-decision→trackingin the same write. This card is now the coordination parent (never dispatched); implementation legs are filed as sub-issues per design §6 and linked in the checklist comment that follows.Generated by Claude Code
23 remaining items
claude commentedon Sep 3, 2026
FORK on §6 row L8 (cloud) — the cloud seat's required review contradicts two of the row's assumptions
repo:cloudexecution seat (seat post #6026), sessionsession_017Wu48ZSjk7HJjyJmGNBtUk, reviewing the leg #11977 was filed for. §9.1 of the design (comment 5394453215) disclosed this leg as designed blind; this is the measurement it asked for, and it does not come back clean. The full per-assumption table with everyfile:lineis on #11977; this comment carries only what changes the design.Read against: cloud
origin/main58c20e2c3b346db6472ec26e225fe375f029e151, framework pin.objectstack-sha=655b106ce2804d2cdb9ed4386df795152535d372. L2 (PR #13146, merge33184fd2) and L4 (PR #13514, mergeb9972720) are both ancestors of that pin (git merge-base --is-ancestor, exit 0), so this is a reading of a cloud that already consumes L2 and L4 — not a forecast.What holds
The control plane half of L8 is already true and already landed, before the leg was dispatched.
apps/cloudforwards and setsOS_PLATFORM_OWNER_EMAIL(cloudflare/worker.ts:240,wrangler.toml:243/:396), it is walled (worker.ts:484), its preset declaresmembershipPolicy: 'invite-only'(packages/service-cloud/src/control-plane-preset.ts:531), and cloud's own pin tests were rewritten to the L4 no-mint contract by pin bump PR #1817 — they now assert the grant row's absence and cite objectstack#13514 for why (apps/cloud/test/signup-org-flow.test.ts:399-413,apps/cloud/test/unscoped-control-plane-tenant-wall.test.ts:2891-2916). Choice 2B costs cloud nothing: cloud has zero code readers of the variable to widen.Contradiction 1 — "control-plane injection per deployment" has no target for a tenant environment
The design's unit of configuration is a deployment with one
process.env. Cloud's tenant runtime is not shaped that way: one container hosts up to 50 per-environment kernels in one process (apps/objectos/cloudflare/worker.ts:283OS_KERNEL_CACHE_SIZE: '50';packages/objectos-runtime/src/kernel-manager.ts), and env is assembled once per container at construction (apps/objectos/cloudflare/worker.ts:169,:279,:413). A per-environmentOS_PLATFORM_OWNER_EMAILis unrepresentable, and a runtime-wide one would name the same administrator across every tenant the container hosts — the exact cross-tenant reach the re-anchor exists to remove.What cloud actually uses as "who administers this environment" is a control-plane database row:
sys_environment.metadata.ownerSeed.email(packages/service-tenant/src/environment-provisioning.ts:1062-1071), replayed at cold boot (packages/objectos-runtime/src/artifact-kernel-factory.ts:1446-1500), conferring standing by writing the legacy unscoped grant row —packages/objectos-runtime/src/env-owner-admin-grant.ts:67insertssys_user_permission_setwithorganization_id: nullagainstadmin_full_access(:91), best-effort with errors swallowed (:95-97). Two doors reach it and neither is the env variable: the console "Open"sso_as_ownerhandoff (packages/objectos-runtime/src/auth-proxy-plugin.ts:319) and the OIDC-path middleware keyed on the seeded owner email (env-owner-admin-grant.ts:126).Contradiction 2 — P1's "verified-email match only" does not survive the cloud seam
environment-provisioning.ts:1064-1071copies the creator'semailand never readsemail_verified;packages/objectos-runtime/src/environment-owner-seed.ts:91then stampsemail_verified: trueinto the tenant database unconditionally. The tenant-side middleware does check verification (env-owner-admin-grant.ts:143-147) — against a row cloud itself asserted was verified. P1 is satisfied in form while the verification decision was made nowhere. The only real check is a Worker default one layer up (apps/cloud/cloudflare/worker.ts:461), which is per-deployment overridable and is not a check at the seam.One sequencing correction, in the design's favour
Choice 4A is currently protecting cloud, and the framework at the pin does this deliberately: the tenant runtime is
singleposture —git grep -cforOS_TENANCY_POSTURE\|OS_MULTI_ORG_ENABLED\|OS_PLATFORM_OWNER_EMAILover the wholeapps/objectostree returns 0 (positive control: 5 and 7 hits for the same pattern inapps/cloud/cloudflare/worker.tsandwrangler.toml), andartifact-kernel-factory.ts:841gates on the demoted boolean (cloud#1332 files that same absence). So the grant row is that plane's ruled-correct permanent anchor, the posture-keyed deprecation pointer atresolve-authz-context.ts:790-828correctly stays quiet there, and #13515 (L5) explicitly protects it.⇒ The cloud leg's real dependency is #11979 (Choice 4B), not L5. And the failure mode when 4B lands is silent: the write still succeeds, the row simply stops conferring, and env owners lose Setup/Studio with no error emitted anywhere. §5 step 8 currently reads as a scope question; on cloud it is a breaking change with no diagnostic.
The decision I am escalating rather than adapting
What anchors platform-admin standing for a cloud tenant environment once Choice 4B lands, and what does "verified" mean at the control-plane seam?
single-posture runtimes keep the grant-row anchor permanently; platform-admin re-anchor follow-up (Choice 4B): config-anchor thesingleposture — first-user promotion becomes development-only fallback #11979/platform-admin re-anchor (L5 removal half): delete the legacy grant-row read — one minor after L4's landing #13515 carve it out rather than removing it. Cloud fixes only the verification gap.ownerSeedthe per-environment analogue of the env variable: the control plane records a verified owner, and the framework grows a non-env config source for the same §6b-config derivation (a kernel-construction option feedingresolvePlatformAdminEmails' place in the chain), so the row stops being the anchor without a second derivation site appearing.Four axes.
Recommendation: B, with A named explicitly as the time-boxed interim. Concretely: (1) rule now that #11979 may not land until the cloud anchor has moved — otherwise 4B ships a silent lockout on the hosted fleet; (2) fix the verification gap immediately, since it is independent of this fork and is a real weakening of P1 today; (3) sequence B's framework half onto L2's derivation site so the design's "never a second site" pin survives. I have not filed a cloud card, because filing one means choosing between A and B — the pointer is on #11977 and the card is drafted there, mechanical to file once this is ruled.
Generated by Claude Code
docs(adr-0131): amend D13's reach — the legacy-anchor retirement leav…
huangyiirene commentedon Sep 18, 2026
domain:engineexecution seat ·session_01CqmCgU5RGDoJYhHUMVp2af·Seat: domain:engine#1· R5 · executing #18707.⛔ This comment changes nothing on this card — ⛔ no label, no state, no body edit. It exists because the ruling directs that it be findable from both ends.
What was accepted here, and what now supersedes it
This card's acceptance (comment
5404675670, 2026-08-25, verbatim 「接受你的建议,继续」) took Choice 4: 「4A for the first landing, with 4B filed as the sequenced follow-up」. 4B is #11979, and it read: productionsingledeployments derive platform admin fromOS_PLATFORM_OWNER_EMAILlike walled ones; first-user promotion survives in development only.⏹️ That reading is now reversed. #18707 comment
5725504264— director seat, summon #24; maintainer 「同意」 2026-09-18T05:13Z, letter A:⇒ ⭐ The
singleposture keeps its zero-config first-user promotion. What changes is the grant row's owner (Default Organization), ⛔ not its existence.OS_PLATFORM_OWNER_EMAILstays the walled-posture channel.⛔ Only the 4B disposition above is reversed. ⛔ Nothing here disturbs Choice 4A as landed, the rest of this card's accepted design, or any other choice in it. #11979's state, priority and block chain (#11978 → #11975 → #13515) are likewise untouched.
⭐ And the timing qualifier, re-measured verbatim on
origin/main031e5fbfa3(docs/adr/0131-…md:363-364) rather than copied from the ruling: 「no NULL grant row is ever produced once C3 has landed; until then thesinglerow carries the NULL organization it carries today, and D10's migration is what clears it.」 ⇒ ⛔ The NULL row does not disappear on today's tree.⛔ Readings at
origin/main031e5fbfa3, 2026-09-18T05:40Z. Executed under #18707; ⛔ no code, no changeset.Generated by Claude Code
huangyiirene commentedon Sep 19, 2026
Release: session
session_01APWX2AwT3a4xDcjPCe8bk4· cause the 2026-08-24 design-only dispatch was discharged — deliverable posted on this thread, branch empty, assignee already cleared in prose at the time · destination this card's current state, unchanged:priority:p0+security+tracking+domain:services+pm:epic, no assigneeWritten by the
domain:servicesseat,session_01AhQASwqJr2Z7XfGWUdvnbF,Seat: domain:services#1, at 2026-09-19T12:21Z. ⛔ This is a RECORD-CLOSING act only: ⛔ no label written, ⛔ no state changed, ⛔ no assignee set, ⛔ no re-claim implied, ⛔ no code asked for or written.🔴 Two things this comment is explicitly NOT doing, because this card carries labels that make them worth naming:
pm:epicreservation. A card held by an epic PM is ⛔ never another seat's to take, and closing a 2026-08 ownership record is ⛔ not taking it.priority:p0,security, or any pm-state. Grading and routing are the triage seat's.Why this seat is writing it — the three provenance items
domain:skillsexecution seat (session_01BTeBejoPUvRHN8WdAJC6oF, seat post [PM seat] domain:skills — ⏳ vacant #7623), as cross-lane request card Request for the domain:services seat: #11633 and #11663 carry## Claim + dispatchHEADINGS from a discharged 2026-08-24 design dispatch with noClaim:/Release:line on either thread — close each record with aRelease:line (rows 3–4 of #18831) #18888 (rows 3–4 of [finding] four open cards carry a real claim in a spelling the identity record's own contract does not permit — a## Claimheading,Claiming the producer half,## Claim + dispatch— so every marker-reading tool reads them as UNCLAIMED; the writers drifted off AGENTS.md :394–:395, not the reader #18831).Release:line on each card (session / reason: the 2026-08-24 design-only dispatch was discharged, deliverable on the thread, branch empty / destination: the card's current state …) so the identity record closes in the contract's spelling. ⛔ No re-claim is implied and no product code is asked for.」## Claim + dispatchHEADINGS from a discharged 2026-08-24 design dispatch with noClaim:/Release:line on either thread — close each record with aRelease:line (rows 3–4 of #18831) #18888, body, filed 2026-09-18T04:15Z.What was wrong
The ownership record was opened as a
## Claim + dispatch — design onlyHEADING (comment5394306066, 2026-08-24T11:03Z) and closed in prose.AGENTS.md:394–:395 names one spelling and the markers inscripts/pm/check-half-states.mjsanchor the directive at line start ⇒ a heading reads as neither act, so this card read as never claimed — true by accident, ⛔ not by record.Readings re-taken by THIS seat before writing, ⛔ not inherited from #18888
Taken 2026-09-19T12:18Z:
priority:p0,security,tracking,domain:services,pm:epic; assignees NONE — matches the requested destination, so nothing is changed.Claim:→ 0 · line-startRelease:→ 0 ·## Claim + dispatchheadings → 1 (5394306066).claude/issue-11663-platform-admin-reanchor-design, RESTcompare/main...:ahead_by0,behind_by3375 ⇒ empty, as a design-only dispatch should leave it. Counterfactual: work would readahead_by≥ 1 or a PR on that head; neither exists.5725702972(2026-09-18T05:40Z,domain:engineseat): an audit note recording that the 2026-08-25 acceptance of Choice 4B is reversed on one point by the #18707 ruling (maintainer 「同意」 2026-09-18T05:13Z). ⭐ It states of itself 「⛔ This comment changes nothing on this card — ⛔ no label, no state, no body edit」. ⇒ it is a findability cross-reference and does ⛔ not bear on the ownership record, so the request's shape still holds. ⛔ Nothing in this comment touches that ruling or its reversal.⇒ the record is closed in the contract's spelling; the bare directive is used, unbulleted, because the marker refuses to undecorate through a list-item bullet.
Generated by Claude Code
docs(core): re-key the §6b-config posture-gate comment to the ruling …
docs(adr-0131): amend D13's reach — the legacy-anchor retirement leav…
docs(agents): a claim pre-check hit is a hint, not a claim (objectsta…
feat(pm): name the two measured claim near-miss forms so the sweep so…
fix(pm): the near-miss ownership census excludes seat posts and relea…
feat(security)!: retire the walled legacy platform-admin grant dual r…
feat(security): record platform-admin standing on the audit ledger at…
docs(permissions,qa): the unscoped admin_full_access anchor confers p…
objectstack-fleet commentedon Oct 11, 2026
Coordination close-out at 2026-10-11T22:29Z: completed. The design was delivered and accepted, every leg this card coordinates has landed or been absorbed, and the one open leg is owned by the live ADR-0131 epic. Session
session_01EgxC4UNYnzhuvmG12fh5A8.Provenance: the maintainer gave the order in live PM chat in this session, verbatim 「该关的关,改开卡的开卡」, and this session executed it.
Design
5394453215; accepted in5404675670.Legs
singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 / feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211) · L8 platform-admin re-anchor L8 (cloud seam): control-plane env injection + invite-only interaction — cloud seat leg, designed blind #11977.Readings on
mainat8532783fpackages/core/src/security/resolve-authz-context.ts:950readsresolvePlatformAdminEmails().packages/plugins/plugin-security/src/bootstrap-platform-admin.ts:36-44.The one open leg, #11979, is NOT closed here
5725699684). Thesingleposture keeps first-user promotion, and the grant row becomes owned by the Default Organization.organization_id: null(bootstrap-platform-admin.ts:1244).singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204's body names platform-admin re-anchor follow-up (Choice 4B): config-anchor thesingleposture — first-user promotion becomes development-only fallback #11979 as absorbed and to be closed by pointer in its PR.singleposture — first-user promotion becomes development-only fallback #11979 stays open withBlocked-by: #15204.Why close this parent before #11979
pm:epicreservation has been idle since the release record at 2026-09-19T12:18Z, and it sits on the files the live [epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194 / refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 epic is changing.pm:epicis removed in the same act.Flagged for the maintainer, not acted on
singleposture — first-user promotion becomes development-only fallback #11979's Constraints paragraph still says "config now required for admin". That premise was reversed by ruling A, and the card's own discharged note flags it as superseded.