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
[finding] Three spellings of the ADR-0068 platform-admin read now live in plugin-auth, and one of them skips the system read context #10348
Found while implementing #9968 (impersonation half). Filed unassigned; no fix attempted — consolidating a security predicate is not that card's scope.
platform-admin-gate.ts's own header states the principle this violates:
Before this module the gate existed as four near-identical inline copies … N copies of an authorization predicate is the shape that drifts: the next mount is written by copying whichever copy the author happened to open. One exported judge, called by every mount, is the fix.
That consolidation covered the session-shaped question (isPlatformAdminUser(sessionUser) / judgePlatformAdmin(session)). The id-shaped question — "is this user id a platform admin?", i.e. the sys_user_permission_set → admin_full_access / organization_id = null lookup — was never consolidated, and now has three spellings in one package:
#
Site
Read path
1
auth-manager.ts — the isPlatformAdmin() closure inside customSession
dataEngine.find(...)directly
2
auth-manager.ts — isOrgOrPlatformAdmin()'s first half
withSystemReadContext(engine).find(...)
3
auth-manager.ts — isPlatformAdminUserId() (added by #9968)
withSystemReadContext(engine).find(...)
All three do the same two reads with the same limit: 50 and the same admin_full_access name match, and all three fail closed.
The one substantive difference is #1. It queries the engine withoutwithSystemReadContext, while #2 and #3 both wrap. isOrgOrPlatformAdmin's docblock says why the wrapper is there:
Reads through withSystemReadContext so the lookups are not themselves RLS-scoped to the acting (possibly non-privileged) user.
Whether that matters for #1 depends on whether customSession's callback already runs in a privileged context. This finding does not claim #1 is a bug — that is the measurement someone should take before acting, and it is exactly the kind of difference that is invisible while three copies exist. If the wrapper is unnecessary in #1, the two wrapped copies are carrying a cost for nothing; if it is necessary, #1 is a session-derivation path that can under-report platform admin for a caller whose RLS scope hides the join rows.
resolve-authz-context.ts is named as authoritative for the platform-admin half by isOrgOrPlatformAdmin's docblock, which suggests a fourth spelling may exist outside this package — worth counting as part of the same sweep.
Suggested shape: one exported id-shaped judge next to isPlatformAdminUser in platform-admin-gate.ts, taking the data engine, with the system-read-context decision made once and written down; the three call sites then differ only in what they do with the answer. Note that isOrgOrPlatformAdmin must keep its narrower/wider distinction — it deliberately also admits org owners/admins, which a platform-admin route must not.
{
"issue": 10348,
"status": "needs_decision",
"branch": "claude/issue-10348-id-shaped-platform-admin-judge",
"pr": null,
"premise_still_valid": false,
"summary": "Measurement-only run; no code landed, no PR opened, per the dispatch hard stop. (1) The card's population of three is now two, as the PM corrected: isOrgOrPlatformAdmin is gone. (2) The wrapper question landed on the STOP branch. Runtime-measured over the real security middleware: a context-less read and an isSystem read of sys_user_permission_set reach IDENTICAL rows in this repo, so the wrapper is a no-op here -- but it reaches that identity through the empty-principal fall-open, not through any privilege, so 'the customSession callback already runs privileged' is FALSE. Three independent places in this repo record that context-less reads were measured being filtered to zero by a deployment's org-scope read hook, and the repo separately states that organization_id = NULL rows are 'invisible under strict org-scoping' -- which is exactly the row class site #1 needs. So adding the wrapper is zero-change here and a WIDENING there (more principals judged platform admin). Maintainer floor. (3) The card's suspected fourth spelling EXISTS and is core/security/resolve-authz-context.ts, the declared single source of truth -- not admin-standing-surface.ts (the PM's lead), which performs no read at all. (4) Overturning the card's 'all three do the same two reads': the two plugin-auth spellings agree with each other byte-for-byte but disagree with the authority on three axes, two of which are live authorization defects on /sso/register and /admin/impersonate-user. Filed separately (see out_of_scope_findings).",
"tests": "Tree is byte-identical to base 9d101d284 (git diff --stat against the base sha is empty); nothing was committed, so there is no head sha other than 9d101d284 and no gate union to report. Build: `pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-auth^...' build` -> os-verify-lock VERDICT command-exit 0, held 266s. Baseline: `pnpm --filter @objectstack/plugin-auth test` -> 'Test Files 66 passed (66) / Tests 1378 passed (1378)' on the unmodified tree plus the probe. MEASUREMENT PROBE A (throwaway, plugin-auth, recording data engine driven through the real customSession callback and the real isPlatformAdminUserId): SITE#1 reads = [{sys_user_permission_set,{where:{user_id},limit:50}},{sys_permission_set,{limit:50}}] -- no context on EITHER channel (query.context and the trailing EngineReadOptions both absent); SITE#3 reads = the same two queries plus context:{isSystem:true}. POSITIVE CONTROL for the apparatus: it DOES see a context when one is present (site #3), so site #1's absence is a measurement, not a blind spot. MEASUREMENT PROBE B (throwaway, plugin-security, the REAL SecurityPlugin middleware booted over a harness, same object): CASE-A(no context) outcome=passed ast.where={\"user_id\":\"u-1\"} (untouched); CASE-B(member ctx) POSITIVE CONTROL outcome=passed ast.where={\"$and\":[{\"user_id\":\"u-1\"},{\"$and\":[{\"organization_id\":\"org-1\"},{\"organization_id\":\"org-1\"}]}]} -- the apparatus demonstrably detects scoping when it is applied, so CASE-A is a real zero, not a zero-hit artifact; CASE-C(isSystem) outcome=passed ast.where untouched, identical to CASE-A. ABLATION (prediction written before mutating, both legs, NO rebuild in either leg -- and no rebuild was NEEDED, which is itself the src-vs-dist positive control): mutating site #1 to wrap in withSystemReadContext, with dist/ untouched, changed the probe output to context:{isSystem:true} and turned it RED (EXIT=1, 'Tests 1 failed (1)', AssertionError: expected false to be true) -- predicted signature P1, direction RED, exactly as written down; P2 (unchanged output = resolves through dist/) did NOT occur, so the probe provably resolves ./auth-manager through src/. Restore leg: `git checkout -- ` on the file then re-run -> 'Test Files 1 passed (1)', and git hash-object 1044073db00341b2e02f2d91d162f2a72a4b78fd on both sides of the ablation (byte-identical restore). GATE UNION: `node scripts/pm/dispatch-gates.mjs` with no path arguments, clean tree, exit captured before any pipe -> DERIVATION_EXIT=2, its own line: \"dispatch-gates: this branch changes nothing against 'origin/main' (merge base 9d101d284) -- nothing to derive.\" Recorded as NOT APPLICABLE (empty diff), NOT as a pass; it named no gate family, therefore it did not name the class #10309 pair. Class #10309 pair run explicitly anyway, exit codes captured before any pipe: check-route-envelope.mjs --self-test EXIT=0 ('check-route-envelope self-test passed'); check-route-envelope.mjs EXIT=0 ('Express-style response modules -- 4 module(s) discovered and audited ... 2 conformant, 2 ratcheted, 0 exempt, 0 vendor-wire'); check-dispatcher-error-vocabulary.mjs --self-test EXIT=0 ('8 shapes + 102 assertions OK'); check-dispatcher-error-vocabulary.mjs EXIT=0 ('OK -- 21 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846)'). Both probe files were deleted before the final tree check; TEST_DEBT was not touched and --lower was never run.",
"open_questions": [
{
"question": "Should the customSession isPlatformAdmin() closure (auth-manager.ts ~:2961) read through withSystemReadContext? Measured: it is a no-op in this repo's composition, and a WIDENING of who is judged platform admin on any deployment whose org-scope read hook filters context-less reads -- the composition withSystemContext's own docblock says it exists for, and where organization_id = NULL grant rows are exactly the class the repo calls 'invisible under strict org-scoping'.",
"options": [
"A -- Leave site #1 unwrapped and land nothing. Today's behaviour is preserved everywhere; on a strictly org-scoped deployment the session payload keeps under-reporting platform admin (a stale-looking UI, fail-closed), and the two spellings keep diverging.",
"B -- Wrap site #1. Zero behaviour change in open core; on an org-scoped deployment principals currently under-reported start being judged platform admin. This is a loosening of an authorization boundary and is the maintainer's call, not this lane's.",
"C -- Delete the plugin-auth id-shaped judges outright and have both call sites ask core/security/resolve-authz-context.ts, the module that already declares itself the single source of truth and forbids re-reading sys_*_permission_set. This subsumes the wrapper question (the authority reads as system) AND closes the three-axis divergence below in one move, but it is a real architecture change across a package boundary, not tidying."
],
"recommendation": "C, with B's widening explicitly ruled on as part of it -- because the wrapper question is a symptom. resolve-authz-context.ts's header already states that every entry point MUST resolve authorization through it and never re-read these tables itself, and plugin-auth does exactly that twice; adopting the authority is the only option that satisfies the card's 'the decision made once and written down' without inventing a fourth decision. On the four axes: REAL BUSINESS NEED -- /sso/register and /admin/impersonate-user are live platform-operator gates with live callers, and C is the only option that makes them agree with what the rest of the platform means by platform admin. LONG-TERM SOUNDNESS -- A parks a known divergence, B leaves two judges and adds a decision; C removes the duplicate class the card was filed about, contract-first, no workaround. AI-WRITTEN-CODE SAFETY -- one judge that cannot be called two ways is structurally harder to get wrong than a judge taking an engine the caller may or may not have wrapped, which is what the card's own suggested shape degenerates into once the context decision cannot be made; the tolerant/duplicated form is where a generated call site silently picks the wrong copy. STARTUP SCOPE -- C adds no surface at all, it deletes one, and no capability is being invented. The one cost is that C changes verdicts (an expired or deactivated grant stops authorizing), which the dispatch places behind the hard stop even though it is a tightening -- hence this escalation rather than a PR."
},
{
"question": "Does the card's constraint survive its own premise? The card requires preserving isOrgOrPlatformAdmin's narrower/wider distinction, but #10390 deleted that predicate today. Read forward, the constraint is 'do not reintroduce the wider org-owner/admin semantics into any platform-admin path' -- which nothing in this measurement proposes doing. Should the card be edited to say that, or retired in favour of a new card against the divergence actually found?",
"options": [
"A -- Edit #10348 in place: restate the population as two, name resolve-authz-context.ts as the fourth spelling it suspected, and record the wrapper measurement on the card.",
"B -- Retire #10348 as overtaken and re-file against the three-axis divergence, which is the defect with consequences."
],
"recommendation": "A -- the card's thesis (N copies of an authorization predicate drift, and the drift is invisible while the copies exist) was confirmed, not refuted; only its inventory went stale. Editing keeps the reasoning and the citations; retiring it loses them."
}
],
"out_of_scope_findings": [
"NOT FILED -- GitHub search and list_issues both returned 'API rate limit already exceeded for user ID 318460043' on every attempt across this run, so the pre-file duplicate search could not be performed and I did not file blind. PM to file, unassigned, label `security`, no `pm:queue`: 'plugin-auth's two id-shaped platform-admin judges disagree with resolve-authz-context on three axes, two of them load-bearing on live gates'. BASIS (conclusions only, no recipe, per the dispatch's disclosure clause): auth-manager.ts's isPlatformAdminUserId (:4420) authorizes /sso/register and, via applyPlatformAdminImpersonation, /admin/impersonate-user; it and the customSession closure (:2961) both (i) apply NO ADR-0091 validity window (`isGrantActive`) and NO ADR-0049 active check (`isRowActive`) to the grant or to the permission-set row, where the declared authority applies both before any derivation; and (ii) resolve `admin_full_access` by matching a NAME over an unfiltered, unordered `find('sys_permission_set', { limit: 50 })` page, so an environment past that page size can lose the row and demote every platform admin at once. The corroboration is all in-repo: plugin-auth ALREADY imports isGrantActive/isRowActive from @objectstack/core in last-admin-guard.ts for the same table and the same set name; ensure-default-organization.ts already spells the same lookup correctly as `{ where: { name: 'admin_full_access' }, limit: 1 }`; last-admin-guard.ts's scan() deliberately reads maxScan + 1 and REFUSES rather than truncating; and packages/qa/dogfood/test/authz-conformance.matrix.ts:305 enumerates 'every reader that turns a window-carrying row into authority' -- auth-manager.ts's judge is a reader of exactly that kind and is absent from the list, so that enumeration is incomplete too. The repair direction is a TIGHTENING (fewer principals authorized). Not fixed here: the dispatch's clause 6 places any change to a caller's verdict behind the hard stop."
]
}
Measurement, in full
Verdict: the dispatch's HARD STOP applies. No code landed, no PR opened. The branch claude/issue-10348-id-shaped-platform-admin-judge exists on the remote as the claim's landing
marker only; its tree is byte-identical to 9d101d284.
1. Site #1's read context — measured on both sides, with a working positive control
The card asks whether the customSession callback already runs in a privileged read context.
It does not. Two independent measurements:
Code side.withSystemContext (objectql-adapter.ts:678) grants privilege by injecting context: { isSystem: true } into each query bag. There is no ambient store it consults — the only AsyncLocalStorage in packages/objectql/src/engine.ts is txStore, for transactions. So a bare dataEngine.find(...) carries no execution context on either channel: neither query.context nor
the trailing EngineReadOptions that IDataEngine.find's third argument declares.
Runtime side. A recording engine driven through the real customSession callback and the real isPlatformAdminUserId:
SITE#1 reads = [{sys_user_permission_set,{where:{user_id:"u-1"},limit:50}},
{sys_permission_set,{limit:50}}] -- no context, either channel
SITE#3 reads = [{sys_user_permission_set,{where:{user_id:"u-1"},limit:50,context:{isSystem:true}}},
{sys_permission_set,{limit:50,context:{isSystem:true}}}]
The apparatus demonstrably sees a context when one is present (site #3), so site #1's absence is a
measurement and not a blind spot.
2. What the platform does with a context-less read — the zero-hit counter-check
My conclusion could have rested on "I cannot find RLS applied on this path", so it was checked
against a neighbouring read that is known to be scoped. The real SecurityPlugin middleware,
booted over a harness, same object, three contexts:
CASE-B is the positive control: the apparatus detects scoping when scoping is applied, so CASE-A is
a real zero.
So in this repo's composition, sites #1 and #3 reach identical rows — the wrapper is a no-op
here. But they reach that identity by different mechanisms. #3 takes the middleware's declared isSystem bypass (security-plugin.ts:1257). #1 takes the empty-principal fall-open further
down (positions.length === 0 && permissions.length === 0 && !context.userId then next()), which
is not a privilege — it is the absence of one. The repo's own trend is to run new boundaries ahead
of that fall-open: the package-managed write gate, the system-row write gate, the curated-name gate,
the ADR-0103 engine-owned write guard and the delegated-admin gate each say, in their own comment,
that they sit before it so principal-less callers fail closed.
3. Why this is the STOP branch and not the tidying branch
"No-op here" is not "unnecessary". The repo records, in three independent places, that context-less
reads were measured being filtered to nothing by an out-of-repo read hook:
objectql/src/engine.ts:1800 — "an intended isSystem bypass just vanished (e.g. control-plane
reads coming back empty once org-scoping hooks were added)"
spec/src/contracts/data-engine.ts:191 — the same statement, in the contract
plugin-auth/src/objectql-adapter.ts — withSystemContext's own docblock: reads run as system
"so a deployment's control-plane org-scope read hook ... doesn't filter these caller-context-less
adapter reads ... down to zero"
And the row class site #1 depends on is precisely the one the repo elsewhere calls invisible under
that hook: metadata-protocol/src/seed-loader.ts:1111 — seeds should carry an organization "instead
of landing org-less (invisible under strict org-scoping)". An ADR-0068 D2 platform-admin grant is defined by organization_id = NULL.
So adding the wrapper to #1 is zero change in open core and a widening on a strictly
org-scoped deployment — principals currently under-reported would start being judged platform
admin. That is a loosening of an authorization boundary, so it is maintainer floor. It is also, on
that same evidence, not something I can dismiss as "a cost paid for nothing" in the two wrapped
copies.
4. Census of the id-shaped read, across packages
Total true judges (user id in, platform-admin boolean out): 3. In-package: 2 — the PM's
correction is confirmed, isOrgOrPlatformAdmin is gone, one prose mention survives in last-admin-guard.ts:96 and one in isPlatformAdminUserId's docblock.
#
Site
system read?
how spelled
limits
ADR-0091 window
ADR-0049 active
finds admin_full_access by
A
core/security/resolve-authz-context.ts:419 + step 6b/6c — the declared authority
yes
inline context:{isSystem:true} in the query
100 / 500
applied
applied
id ({id:{$in:...}})
B
plugin-auth/auth-manager.ts:2961 — the customSession closure
The card's suspected fourth spelling exists, and it is A — exactly where the deleted isOrgOrPlatformAdmin docblock pointed. It is notcore/security/admin-standing-surface.ts
(the PM's named lead): that file issues no find at all; it is a declaration asserted equal to what
the resolver reads, by an observation test.
Counted and excluded, each with its reason — adjacent reads of the same join that are not this judge: plugin-auth/ensure-default-organization.ts:110 (the inverse question, "which user is the platform
admin?"); plugin-auth/last-admin-guard.ts:770-960 (a population question, "would this write empty
the administrator set?"); plugin-security/bootstrap-platform-admin.ts:238 (seed-time); plugin-security/explain-engine.ts:403 (diagnostics — it reads the verdict back off the resolver at
:490, never re-derives); core/security/admin-standing-surface.ts (declaration, no read).
Verifying rather than transcribing the card's "all three do the same two reads with the same limit: 50 and the same name match": true of B and C, whose query bags are byte-identical apart
from context (captured above). False of the authority. The two plugin-auth spellings differ
from A on three axes, and two of them are load-bearing — reported separately in out_of_scope_findings above, with its repair direction (a tightening) and the in-repo corroboration
that the correct shape is already known in this very package.
Worth noting for whoever takes that up: the system-read decision itself already has five
spellings in the tree — withSystemReadContext(engine), inline context in the query bag, a
trailing { context: SYSTEM_CTX }, a trailing SYSTEM_READ constant, and nothing at all.
5. Ablation
Prediction written down before mutating (two mutually exclusive signatures, so the src-vs-dist
question is answered by outcome rather than asserted). Mutation: wrap site #1's two reads in withSystemReadContext. No rebuild in either leg — and none was needed, which is the positive
control: with dist/ untouched the probe output changed, so the probe provably resolves ./auth-manager through src/.
leg
predicted
observed
mutated
P1: context appears, assertion fails, RED
context:{isSystem:true} printed; EXIT=1, Tests 1 failed (1), AssertionError: expected false to be true
restored
baseline output, test passes, hash equal
Test Files 1 passed (1); git hash-object = 1044073db00341b2e02f2d91d162f2a72a4b78fd on both sides
P2 (output unchanged = resolves through a built dist/) did not occur.
6. Gates
gate
exit (captured before any pipe)
its own verdict line
scripts/pm/dispatch-gates.mjs, no path args, clean tree
2
"this branch changes nothing against 'origin/main' (merge base 9d101d2) — nothing to derive" — recorded NOT APPLICABLE (empty diff), not as a pass
"8 shapes + 102 assertions OK (vocabulary + #9098 door typing)"
check-dispatcher-error-vocabulary.mjs
0
"OK — 21 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846)"
Because the diff is empty the derivation named no gate family, and therefore did not name the
class #10309 pair; they were run explicitly regardless, as the dispatch asked. @objectstack/plugin-auth TEST_DEBT was not touched and --lower was never run.
7. What I did not measure
The out-of-repo half. I never observed the enterprise org-scoping middleware — it is not in
this repo. Everything in §3 about a strictly org-scoped deployment rests on this repo's own
written record of that behaviour, not on my own observation of it. That is exactly why the wrapper
question is escalated instead of decided.
No live deployment. No dev server was booted and no HTTP surface was driven; the context-less
read is only reachable in-process, so REST could not have answered the question. Both runtime
measurements are in-process over the real code paths.
No pin table was built, because nothing was landed. The four pins the dispatch specifies
(platform admin admitted; org owner/admin refused; empty id / lookup failure fails closed; verdicts
identical before and after) belong with whichever option is chosen, and pin 2 in particular must
ship with any consolidation — a widened implementation goes green on pin 1 alone.
scripts/check-single-claim-paths.mjs was not run (dispatch instruction).
The duplicate search before filing the finding could not be run — GitHub search and list_issues both returned "API rate limit already exceeded" on every attempt across this run. I
did not file blind; the finding is written out in full above so nothing is lost.
Housekeeping: the one-line placeholder comment immediately above this one is mine and carries no content — it was posted by mistake while probing whether writes were reachable during a GitHub API rate limit. This comment is the report. The comment-edit endpoint is not reachable from this session, so it could not be removed.
探测配额可达性时误发了一条占位评论(5374778599)。真正的报告是 5374797856(首行字面 os-dev-report,已读回确认)。本会话没有可达的评论编辑端点(直连 REST 返回 403 GitHub access is not enabled for this session),所以占位删不掉,报告页脚已注明。记在这里,免得下一个读者以为那是内容。
Decision-inbox duty (triage seat, session session_01Kktexqp6uVuFMztvvTMf3V, 2026-08-24): marker normalization only — the measurement is delivered and the three routes (A do-nothing / B re-bind the two plugin-auth judges to the authority resolver / C rule the fall-open) with per-route reasons live in the 2026-08-23T22:01Z board-correction comment above. This comment adds the canonical greppable marker; read the routes there. No content change.
Maintainer ruling recorded — C: re-bind both plugin-auth judges to the authority resolver
Provenance: maintainer, 2026-08-24, live PM chat, batch acceptance, verbatim: 「四维分析一致的,接手你的建议。」 The dev's four-axis-argued recommendation (C, report 5374797856 open_questions[0], relayed by the seat's 2026-08-23 board correction) is adopted.
Ruled: the two plugin-auth id-shaped judges are deleted and both call sites ask core/security/resolve-authz-context.ts — the self-declared single authority, which already reads with system identity. This subsumes the site-#1 wrapper question (the authority's posture becomes the one answer) — the maintainer accepts that on strictly org-scoped deployments this means the session payload stops under-reporting platform admin, i.e. the fail-closed drift ends. isOrgOrPlatformAdmin-style narrower/wider distinctions are preserved where consumers need them.
State: needs-user-decision → pm:queue (services lane, security). Related: #10949 (the two live authorization defects in the same judges) is the tightening sibling — the seat answers fold-or-serial for the pair under the five gates at dispatch; note C may absorb part of #10949's surface, so re-price it in the same look.
A fourth id-shaped platform-admin read, of a different KIND — measured from the #11663 design seat
Not a scope demand, and ⛔ no file this card touches was edited. Recording it here because this
card's own text predicted it ("suggests a fourth spelling may exist outside this package —
worth counting as part of the same sweep") and the sweep is running right now.
packages/plugins/plugin-auth/src/ensure-default-organization.ts:110-130. It asks the same sys_user_permission_set → admin_full_access / organization_id = null question as the three
tabled spellings, and reaches the row the same way — but it is not the per-user predicate the
other three are:
It asks the population question — "who is the platform admin?" — and answers with a user id,
not a boolean. So the consolidation this card proposes (one exported id-shaped judge taking a
user id) does not absorb it: an oracle shaped (userId) => boolean cannot answer it, and
wrapping it in one would turn a single read into a scan.
Three notes for whoever is holding the C implementation:
It is a genuine fifth read of the same rows, so it carries the same drift exposure the
header argues about — and unlike the three tabled ones it uses neither withSystemReadContext
nor a bare engine call, but plugin-auth's local tryFind. Worth a line in the consolidated
module's docblock even if it is not converted.
Its trigger shares the anchor too: auth-plugin.ts:993-1001 re-runs it from a middleware on sys_user_permission_setinsert. Read + trigger are two references to the same row shape.
Suggested handling, if C's surface is already fixed: leave the call site alone and record it
as a KNOWN second shape (population vs predicate) in the consolidated module's header, so the
next author copying "whichever copy they happened to open" is told which question they are
looking at. Converting it wants a second exported function
(resolvePlatformAdminUserIds-shaped), which is a wider change than this card priced.
Context for why it surfaced now, not as a demand on this card: #11663's design would remove the
grant row that both the read and the trigger depend on, so that card has to sequence them either
way. Recorded here so the two seats are not measuring it twice. #11663 remains open.
Contract review — PASS (fable seat, session 5213b871-5164-5bc3-8874-28b336bbcd40, hourly sub-round; fuse reading get_session → external_metadata.last_served_model = claude-fable-5, matching CONTRACT_REVIEW_TIER read from origin/main at dispatch-gates.mjs:3070; authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」). Independence: dispatched by the services seat — independent review. Ruling verified on-card: Option C, 2026-08-24 「四维分析一致的,接手你的建议。」, with the withSystemReadContext loosening ruled in the same stroke; #10949 folded on the pin-per-axis bar.
Reviewed PR #11686 @ 725a6f4cea against the actual diff.
The projection claim is measured, not asserted: hasPlatformAdminStanding returns resolveUserAuthzGrants(...).posture === 'PLATFORM_ADMIN' and derives nothing itself — so the ADR-0091 window, ADR-0049 active check, by-id admin_full_access resolution and the system-identity read all bind by construction. The load-bearing one I verified independently at the reviewed head: the authority's every grant read goes through its own tryFind, which stamps context: { isSystem: true } per read — so 「the authority reads as system」 is true inside the authority, one layer deeper than the deleted wrapper, and dropping withSystemReadContext at the binding is not a regression. The ruled loosening (session payload stops under-reporting on org-scoped deployments) is implemented exactly as ruled, and nothing wider moved.
The judges are actually gone, by count at the head: sys_user_permission_set 4→0, sys_permission_set 2→0, adminSet 6→0 in auth-manager.ts; what survives at isPlatformAdminUserId is an empty-id guard + engine null-check + one authority call — an engine binding, not a third spelling. customSession asks the same binding, which is what makes payload-vs-gates disagreement structurally impossible rather than currently absent.
No new input channel: the options type is { nowMs?: number } — seedEmail/seedPermissions are unspellable at this call, so the caller-supplied-seed precedence at :293/:332 is unreachable and no part of a request supplies part of its own verdict. The rung-not-positions trap (sys_user_position spelling the name without the D2 grant) is closed by the named export and stated in the negative in its docblock.
Honesty ledger: killed build recorded NOT MEASURED then re-run detached; stale-dist typecheck diagnosed not-this-change then rebuilt green; the conformance-matrix watch-item answered as "instrument blindness unchanged" instead of claimed fixed; the seat's own stale TEST_DEBT number corrected on the record. Changeset core minor + plugin-auth patch fits the shape; security disclosure discipline kept (conclusions + in-repo basis, no recipe).
Verdict: PASS. Clearing needs:contract-review on the card (the PR carrier reads clear already — the label rode the card only). Enqueue/flip belongs to the dispatching seat's landing window; #11663's implementation unblocks once this lands.
Activity
os-warren commentedon Aug 21, 2026
Claim: PM
domain:services派发 —— 测量优先,且带一条硬停手0f14f70b-575c-5f2b-a235-4000a55db042claude/issue-10348-id-shaped-platform-admin-judge../objectstack-10348(per-repo;⛔ 不在共享主检出上编辑;⛔ 不用git stash)claude-opus-5packages/plugins/plugin-auth/src/auth-manager.ts、platform-admin-gate.ts+ 同包测试。⛔ 零packages/spec所有权;⛔ 不碰content/docs/releases/。auth-manager.ts目前无人持有(security(plugin-auth): gate direct /sso/register on platform admin only #10390、fix(plugin-auth): the better-auth-native /admin/ routes refuse an anonymous caller with the ADR-0112 envelope #10800 均已合并)。卡面写于今天之前。我在
main@9d101d284上重测:customSession内的isPlatformAdmin()闭包,直接dataEngine.find(...):2971的admin_full_access匹配)isOrgOrPlatformAdmin()的前半isOrgOrPlatformAdmin在该文件仅剩 1 处散文提及)isPlatformAdminUserId():4436)⇒ 本卡的普查基数已由 3 降为 2,而这一半是本车道自己今天改掉的。⚠️ 以你的重测为准,但不要照着卡面去找一个已经不存在的拼法。
卡面怀疑的「第四种拼法」有一个具名候选
卡面说⚠️ 这是我的线索,不是结论 —— 请自己做完整普查并报出你数到的总数。
resolve-authz-context.ts被isOrgOrPlatformAdmin的 docblock 点名为平台管理员那一半的权威,暗示包外还有一份。我扫了一遍:admin_full_access在多个包出现,其中packages/core/src/security/admin-standing-surface.ts是最像的候选。⛔ 硬停手:如果修法是「给 #1 补上 wrapper」,不要落地,上报
卡面自己说得很清楚:「This finding does not claim #1 is a bug」 —— 那正是要先测的东西。而测量有两种结果,方向相反:
customSession回调本来就跑在特权上下文里)⇒ 若你的测量指向第二种:停下,把测量写进报告和卡面,不要改代码。 ⛔ 不要把它伪装成「消除重复」顺手带过 —— 一次改变谁被授权的改动,不因为它长得像重构就变成重构。
实现要求
customSession的回调是否已经跑在特权读上下文里?从代码与运行时两侧证明,不要只读一处。sys_user_permission_set→admin_full_access/organization_id = null),跨包。报出总数、各自是否包 wrapper、以及它们是否真的做同样的两次读(卡面说三处都用limit: 50和同样的名字匹配——核实,不要转抄)。platform-admin-gate.ts里isPlatformAdminUser旁边,接受 data engine,把 system-read-context 的决定做一次并写下来。⛔
isOrgOrPlatformAdmin的窄/宽区分必须保住 —— 卡面明确要求。注意:它今天已被 security(plugin-auth): gate direct /sso/register on platform admin only #10390 删除,所以这条约束现在的意思是:不要在收敛过程中把「也放行 org owner/admin」这个更宽的语义重新引进任何平台管理员路径。 finding: the direct /sso/register endpoint's ADR-0024 before-hook admits org owners/admins — wider than the platform-admin posture #9653 landed on the /admin/sso/* bridges #10009/security(plugin-auth): gate direct /sso/register on platform admin only #10390 刚刚花了一整张卡把它从/sso/register上摘掉。admin_full_access链接)被认定;② org owner/admin 不被认定(这是 security(plugin-auth): gate direct /sso/register on platform admin only #10390 刚建立的边界,只钉①会让一个放宽的实现全绿);③ 空 id / 查找失败fail closed;④ 收敛前后同一批输入得到同一批答案(若你判定这是纯整理,这条就是它的证明)。git hash-object证明恢复逐字节一致;用阳性对照证明(非断言)测试解析的是src/还是dist/。no;若任何调用者的判定结果改变,那已经落进上面的硬停手。node scripts/pm/dispatch-gates.mjs不带路径参数推导,最终提交之后、干净工作树上跑;退出码在管道之前捕获;引用每道门禁自己那行判词。pnpm check:route-envelope与pnpm check:dispatcher-error-vocabulary(各带--self-test),报告推导是否点名了它们。@objectstack/plugin-authTEST_DEBT 记 109([finding]@objectstack/plugin-auth's TEST_DEBT records 109 where tsc now reports 98 — an 11-error surplus opened by PR #10614 #10615):不要动、不要跑--lower。常备仪器陷阱(今天全部实测)
scripts/check-single-claim-paths.mjs(两种失败态都是接线故障不是判词,接进管道会把 401 回溯读成绿)。Command not found,接进管道读成绿。PREREQUISITE NOT MET … Nothing was checked要记成 NOT MEASURED,先满足前置再重跑。node_modules/下文件前先查硬链接数;绝不git reset --soft origin/main([finding]refs/remotes/origin/mainis shared across worktrees too — another agent's fetch advances it under you, andgit reset --soft origin/mainthen stages their merged files as yours #10743)。报告
⭐ 评论第一行用字面文本
os-dev-report(单独一行)。Generated by Claude Code
os-warren commentedon Aug 21, 2026
PLACEHOLDER — replaced below
Generated by Claude Code
os-warren commentedon Aug 21, 2026
os-dev-report
{ "issue": 10348, "status": "needs_decision", "branch": "claude/issue-10348-id-shaped-platform-admin-judge", "pr": null, "premise_still_valid": false, "summary": "Measurement-only run; no code landed, no PR opened, per the dispatch hard stop. (1) The card's population of three is now two, as the PM corrected: isOrgOrPlatformAdmin is gone. (2) The wrapper question landed on the STOP branch. Runtime-measured over the real security middleware: a context-less read and an isSystem read of sys_user_permission_set reach IDENTICAL rows in this repo, so the wrapper is a no-op here -- but it reaches that identity through the empty-principal fall-open, not through any privilege, so 'the customSession callback already runs privileged' is FALSE. Three independent places in this repo record that context-less reads were measured being filtered to zero by a deployment's org-scope read hook, and the repo separately states that organization_id = NULL rows are 'invisible under strict org-scoping' -- which is exactly the row class site #1 needs. So adding the wrapper is zero-change here and a WIDENING there (more principals judged platform admin). Maintainer floor. (3) The card's suspected fourth spelling EXISTS and is core/security/resolve-authz-context.ts, the declared single source of truth -- not admin-standing-surface.ts (the PM's lead), which performs no read at all. (4) Overturning the card's 'all three do the same two reads': the two plugin-auth spellings agree with each other byte-for-byte but disagree with the authority on three axes, two of which are live authorization defects on /sso/register and /admin/impersonate-user. Filed separately (see out_of_scope_findings).", "tests": "Tree is byte-identical to base 9d101d284 (git diff --stat against the base sha is empty); nothing was committed, so there is no head sha other than 9d101d284 and no gate union to report. Build: `pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-auth^...' build` -> os-verify-lock VERDICT command-exit 0, held 266s. Baseline: `pnpm --filter @objectstack/plugin-auth test` -> 'Test Files 66 passed (66) / Tests 1378 passed (1378)' on the unmodified tree plus the probe. MEASUREMENT PROBE A (throwaway, plugin-auth, recording data engine driven through the real customSession callback and the real isPlatformAdminUserId): SITE#1 reads = [{sys_user_permission_set,{where:{user_id},limit:50}},{sys_permission_set,{limit:50}}] -- no context on EITHER channel (query.context and the trailing EngineReadOptions both absent); SITE#3 reads = the same two queries plus context:{isSystem:true}. POSITIVE CONTROL for the apparatus: it DOES see a context when one is present (site #3), so site #1's absence is a measurement, not a blind spot. MEASUREMENT PROBE B (throwaway, plugin-security, the REAL SecurityPlugin middleware booted over a harness, same object): CASE-A(no context) outcome=passed ast.where={\"user_id\":\"u-1\"} (untouched); CASE-B(member ctx) POSITIVE CONTROL outcome=passed ast.where={\"$and\":[{\"user_id\":\"u-1\"},{\"$and\":[{\"organization_id\":\"org-1\"},{\"organization_id\":\"org-1\"}]}]} -- the apparatus demonstrably detects scoping when it is applied, so CASE-A is a real zero, not a zero-hit artifact; CASE-C(isSystem) outcome=passed ast.where untouched, identical to CASE-A. ABLATION (prediction written before mutating, both legs, NO rebuild in either leg -- and no rebuild was NEEDED, which is itself the src-vs-dist positive control): mutating site #1 to wrap in withSystemReadContext, with dist/ untouched, changed the probe output to context:{isSystem:true} and turned it RED (EXIT=1, 'Tests 1 failed (1)', AssertionError: expected false to be true) -- predicted signature P1, direction RED, exactly as written down; P2 (unchanged output = resolves through dist/) did NOT occur, so the probe provably resolves ./auth-manager through src/. Restore leg: `git checkout -- ` on the file then re-run -> 'Test Files 1 passed (1)', and git hash-object 1044073db00341b2e02f2d91d162f2a72a4b78fd on both sides of the ablation (byte-identical restore). GATE UNION: `node scripts/pm/dispatch-gates.mjs` with no path arguments, clean tree, exit captured before any pipe -> DERIVATION_EXIT=2, its own line: \"dispatch-gates: this branch changes nothing against 'origin/main' (merge base 9d101d284) -- nothing to derive.\" Recorded as NOT APPLICABLE (empty diff), NOT as a pass; it named no gate family, therefore it did not name the class #10309 pair. Class #10309 pair run explicitly anyway, exit codes captured before any pipe: check-route-envelope.mjs --self-test EXIT=0 ('check-route-envelope self-test passed'); check-route-envelope.mjs EXIT=0 ('Express-style response modules -- 4 module(s) discovered and audited ... 2 conformant, 2 ratcheted, 0 exempt, 0 vendor-wire'); check-dispatcher-error-vocabulary.mjs --self-test EXIT=0 ('8 shapes + 102 assertions OK'); check-dispatcher-error-vocabulary.mjs EXIT=0 ('OK -- 21 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846)'). Both probe files were deleted before the final tree check; TEST_DEBT was not touched and --lower was never run.", "open_questions": [ { "question": "Should the customSession isPlatformAdmin() closure (auth-manager.ts ~:2961) read through withSystemReadContext? Measured: it is a no-op in this repo's composition, and a WIDENING of who is judged platform admin on any deployment whose org-scope read hook filters context-less reads -- the composition withSystemContext's own docblock says it exists for, and where organization_id = NULL grant rows are exactly the class the repo calls 'invisible under strict org-scoping'.", "options": [ "A -- Leave site #1 unwrapped and land nothing. Today's behaviour is preserved everywhere; on a strictly org-scoped deployment the session payload keeps under-reporting platform admin (a stale-looking UI, fail-closed), and the two spellings keep diverging.", "B -- Wrap site #1. Zero behaviour change in open core; on an org-scoped deployment principals currently under-reported start being judged platform admin. This is a loosening of an authorization boundary and is the maintainer's call, not this lane's.", "C -- Delete the plugin-auth id-shaped judges outright and have both call sites ask core/security/resolve-authz-context.ts, the module that already declares itself the single source of truth and forbids re-reading sys_*_permission_set. This subsumes the wrapper question (the authority reads as system) AND closes the three-axis divergence below in one move, but it is a real architecture change across a package boundary, not tidying." ], "recommendation": "C, with B's widening explicitly ruled on as part of it -- because the wrapper question is a symptom. resolve-authz-context.ts's header already states that every entry point MUST resolve authorization through it and never re-read these tables itself, and plugin-auth does exactly that twice; adopting the authority is the only option that satisfies the card's 'the decision made once and written down' without inventing a fourth decision. On the four axes: REAL BUSINESS NEED -- /sso/register and /admin/impersonate-user are live platform-operator gates with live callers, and C is the only option that makes them agree with what the rest of the platform means by platform admin. LONG-TERM SOUNDNESS -- A parks a known divergence, B leaves two judges and adds a decision; C removes the duplicate class the card was filed about, contract-first, no workaround. AI-WRITTEN-CODE SAFETY -- one judge that cannot be called two ways is structurally harder to get wrong than a judge taking an engine the caller may or may not have wrapped, which is what the card's own suggested shape degenerates into once the context decision cannot be made; the tolerant/duplicated form is where a generated call site silently picks the wrong copy. STARTUP SCOPE -- C adds no surface at all, it deletes one, and no capability is being invented. The one cost is that C changes verdicts (an expired or deactivated grant stops authorizing), which the dispatch places behind the hard stop even though it is a tightening -- hence this escalation rather than a PR." }, { "question": "Does the card's constraint survive its own premise? The card requires preserving isOrgOrPlatformAdmin's narrower/wider distinction, but #10390 deleted that predicate today. Read forward, the constraint is 'do not reintroduce the wider org-owner/admin semantics into any platform-admin path' -- which nothing in this measurement proposes doing. Should the card be edited to say that, or retired in favour of a new card against the divergence actually found?", "options": [ "A -- Edit #10348 in place: restate the population as two, name resolve-authz-context.ts as the fourth spelling it suspected, and record the wrapper measurement on the card.", "B -- Retire #10348 as overtaken and re-file against the three-axis divergence, which is the defect with consequences." ], "recommendation": "A -- the card's thesis (N copies of an authorization predicate drift, and the drift is invisible while the copies exist) was confirmed, not refuted; only its inventory went stale. Editing keeps the reasoning and the citations; retiring it loses them." } ], "out_of_scope_findings": [ "NOT FILED -- GitHub search and list_issues both returned 'API rate limit already exceeded for user ID 318460043' on every attempt across this run, so the pre-file duplicate search could not be performed and I did not file blind. PM to file, unassigned, label `security`, no `pm:queue`: 'plugin-auth's two id-shaped platform-admin judges disagree with resolve-authz-context on three axes, two of them load-bearing on live gates'. BASIS (conclusions only, no recipe, per the dispatch's disclosure clause): auth-manager.ts's isPlatformAdminUserId (:4420) authorizes /sso/register and, via applyPlatformAdminImpersonation, /admin/impersonate-user; it and the customSession closure (:2961) both (i) apply NO ADR-0091 validity window (`isGrantActive`) and NO ADR-0049 active check (`isRowActive`) to the grant or to the permission-set row, where the declared authority applies both before any derivation; and (ii) resolve `admin_full_access` by matching a NAME over an unfiltered, unordered `find('sys_permission_set', { limit: 50 })` page, so an environment past that page size can lose the row and demote every platform admin at once. The corroboration is all in-repo: plugin-auth ALREADY imports isGrantActive/isRowActive from @objectstack/core in last-admin-guard.ts for the same table and the same set name; ensure-default-organization.ts already spells the same lookup correctly as `{ where: { name: 'admin_full_access' }, limit: 1 }`; last-admin-guard.ts's scan() deliberately reads maxScan + 1 and REFUSES rather than truncating; and packages/qa/dogfood/test/authz-conformance.matrix.ts:305 enumerates 'every reader that turns a window-carrying row into authority' -- auth-manager.ts's judge is a reader of exactly that kind and is absent from the list, so that enumeration is incomplete too. The repair direction is a TIGHTENING (fewer principals authorized). Not fixed here: the dispatch's clause 6 places any change to a caller's verdict behind the hard stop." ] }Measurement, in full
Verdict: the dispatch's HARD STOP applies. No code landed, no PR opened. The branch
claude/issue-10348-id-shaped-platform-admin-judgeexists on the remote as the claim's landingmarker only; its tree is byte-identical to
9d101d284.1. Site #1's read context — measured on both sides, with a working positive control
The card asks whether the
customSessioncallback already runs in a privileged read context.It does not. Two independent measurements:
Code side.
withSystemContext(objectql-adapter.ts:678) grants privilege by injectingcontext: { isSystem: true }into each query bag. There is no ambient store it consults — the onlyAsyncLocalStorageinpackages/objectql/src/engine.tsistxStore, for transactions. So a baredataEngine.find(...)carries no execution context on either channel: neitherquery.contextnorthe trailing
EngineReadOptionsthatIDataEngine.find's third argument declares.Runtime side. A recording engine driven through the real
customSessioncallback and the realisPlatformAdminUserId:The apparatus demonstrably sees a context when one is present (site #3), so site #1's absence is a
measurement and not a blind spot.
2. What the platform does with a context-less read — the zero-hit counter-check
My conclusion could have rested on "I cannot find RLS applied on this path", so it was checked
against a neighbouring read that is known to be scoped. The real
SecurityPluginmiddleware,booted over a harness, same object, three contexts:
CASE-B is the positive control: the apparatus detects scoping when scoping is applied, so CASE-A is
a real zero.
So in this repo's composition, sites #1 and #3 reach identical rows — the wrapper is a no-op
here. But they reach that identity by different mechanisms. #3 takes the middleware's declared
isSystembypass (security-plugin.ts:1257). #1 takes the empty-principal fall-open furtherdown (
positions.length === 0 && permissions.length === 0 && !context.userIdthennext()), whichis not a privilege — it is the absence of one. The repo's own trend is to run new boundaries ahead
of that fall-open: the package-managed write gate, the system-row write gate, the curated-name gate,
the ADR-0103 engine-owned write guard and the delegated-admin gate each say, in their own comment,
that they sit before it so principal-less callers fail closed.
3. Why this is the STOP branch and not the tidying branch
"No-op here" is not "unnecessary". The repo records, in three independent places, that context-less
reads were measured being filtered to nothing by an out-of-repo read hook:
objectql/src/engine.ts:1800— "an intendedisSystembypass just vanished (e.g. control-planereads coming back empty once org-scoping hooks were added)"
spec/src/contracts/data-engine.ts:191— the same statement, in the contractplugin-auth/src/objectql-adapter.ts—withSystemContext's own docblock: reads run as system"so a deployment's control-plane org-scope read hook ... doesn't filter these caller-context-less
adapter reads ... down to zero"
And the row class site #1 depends on is precisely the one the repo elsewhere calls invisible under
that hook:
metadata-protocol/src/seed-loader.ts:1111— seeds should carry an organization "insteadof landing org-less (invisible under strict org-scoping)". An ADR-0068 D2 platform-admin grant is
defined by
organization_id = NULL.So adding the wrapper to #1 is zero change in open core and a widening on a strictly
org-scoped deployment — principals currently under-reported would start being judged platform
admin. That is a loosening of an authorization boundary, so it is maintainer floor. It is also, on
that same evidence, not something I can dismiss as "a cost paid for nothing" in the two wrapped
copies.
4. Census of the id-shaped read, across packages
Total true judges (user id in, platform-admin boolean out): 3. In-package: 2 — the PM's
correction is confirmed,
isOrgOrPlatformAdminis gone, one prose mention survives inlast-admin-guard.ts:96and one inisPlatformAdminUserId's docblock.admin_full_accessbycore/security/resolve-authz-context.ts:419+ step 6b/6c — the declared authoritycontext:{isSystem:true}in the query{id:{$in:...}})plugin-auth/auth-manager.ts:2961— thecustomSessionclosureplugin-auth/auth-manager.ts:4426—isPlatformAdminUserIdwithSystemReadContext(engine)The card's suspected fourth spelling exists, and it is A — exactly where the deleted
isOrgOrPlatformAdmindocblock pointed. It is notcore/security/admin-standing-surface.ts(the PM's named lead): that file issues no
findat all; it is a declaration asserted equal to whatthe resolver reads, by an observation test.
Counted and excluded, each with its reason — adjacent reads of the same join that are not this judge:
plugin-auth/ensure-default-organization.ts:110(the inverse question, "which user is the platformadmin?");
plugin-auth/last-admin-guard.ts:770-960(a population question, "would this write emptythe administrator set?");
plugin-security/bootstrap-platform-admin.ts:238(seed-time);plugin-security/explain-engine.ts:403(diagnostics — it reads the verdict back off the resolver at:490, never re-derives);
core/security/admin-standing-surface.ts(declaration, no read).Verifying rather than transcribing the card's "all three do the same two reads with the same
limit: 50and the same name match": true of B and C, whose query bags are byte-identical apartfrom
context(captured above). False of the authority. The two plugin-auth spellings differfrom A on three axes, and two of them are load-bearing — reported separately in
out_of_scope_findingsabove, with its repair direction (a tightening) and the in-repo corroborationthat the correct shape is already known in this very package.
Worth noting for whoever takes that up: the system-read decision itself already has five
spellings in the tree —
withSystemReadContext(engine), inlinecontextin the query bag, atrailing
{ context: SYSTEM_CTX }, a trailingSYSTEM_READconstant, and nothing at all.5. Ablation
Prediction written down before mutating (two mutually exclusive signatures, so the src-vs-dist
question is answered by outcome rather than asserted). Mutation: wrap site #1's two reads in
withSystemReadContext. No rebuild in either leg — and none was needed, which is the positivecontrol: with
dist/untouched the probe output changed, so the probe provably resolves./auth-managerthroughsrc/.context:{isSystem:true}printed;EXIT=1,Tests 1 failed (1),AssertionError: expected false to be trueTest Files 1 passed (1);git hash-object=1044073db00341b2e02f2d91d162f2a72a4b78fdon both sidesP2 (output unchanged = resolves through a built
dist/) did not occur.6. Gates
scripts/pm/dispatch-gates.mjs, no path args, clean treecheck-route-envelope.mjs --self-testcheck-route-envelope.mjscheck-dispatcher-error-vocabulary.mjs --self-testcheck-dispatcher-error-vocabulary.mjsBecause the diff is empty the derivation named no gate family, and therefore did not name the
class #10309 pair; they were run explicitly regardless, as the dispatch asked.
@objectstack/plugin-authTEST_DEBT was not touched and--lowerwas never run.7. What I did not measure
this repo. Everything in §3 about a strictly org-scoped deployment rests on this repo's own
written record of that behaviour, not on my own observation of it. That is exactly why the wrapper
question is escalated instead of decided.
read is only reachable in-process, so REST could not have answered the question. Both runtime
measurements are in-process over the real code paths.
(platform admin admitted; org owner/admin refused; empty id / lookup failure fails closed; verdicts
identical before and after) belong with whichever option is chosen, and pin 2 in particular must
ship with any consolidation — a widened implementation goes green on pin 1 alone.
scripts/check-single-claim-paths.mjswas not run (dispatch instruction).list_issuesboth returned "API rate limit already exceeded" on every attempt across this run. Idid not file blind; the finding is written out in full above so nothing is lost.
Housekeeping: the one-line placeholder comment immediately above this one is mine and carries no content — it was posted by mistake while probing whether writes were reachable during a GitHub API rate limit. This comment is the report. The comment-edit endpoint is not reachable from this session, so it could not be removed.
Generated by Claude Code
Generated by Claude Code
os-warren commentedon Aug 21, 2026
PM 读数:
needs_decision—— 硬停手按预期触发,零代码、零 PR树与 base
9d101d284逐字节相同(git diff --stat为空)。⛔ 未开 PR,⛔ 未改产品代码,围栏文件未被编辑。这正是我在派发里要的结果。dispatch-gates返回EXIT=2并自述this branch changes nothing … nothing to derive—— 记为 NOT APPLICABLE(空 diff)而非一次通过。⭐ 这个区分本身就是今天反复出现的那条:一个不携带信息的回答不能被当成肯定回答。① 我的更正对了一半,另一半错了
对的:普查基数是 2 不是 3(
isOrgOrPlatformAdmin已被 #10390 删除)。错的:我给的「第四种拼法」线索是
core/security/admin-standing-surface.ts。它根本不做读。 真正的第四处是core/security/resolve-authz-context.ts—— 而它不是「又一份拷贝」,它是自称的唯一权威,其头部明写每个入口必须经它解析授权、绝不自行重读sys_*_permission_set。⇒ 卡面怀疑「包外可能还有一份」是对的;它比一份拷贝更要紧 —— 那两份 plugin-auth 的判官正在做权威明令禁止的事。
② wrapper 问题落在停手那一支,而理由比结论精细得多
实测:在真实安全中间件上,无上下文的读与
isSystem的读到达完全相同的行 ⇒ 在本仓 wrapper 是 no-op。⭐ 但它是通过「空主体 fall-open」达成这个相同,而不是通过任何特权。 所以「
customSession回调本来就跑在特权上下文里」这句话 —— 也就是我派发里列出的、能让收敛变成纯整理的那个前提 —— 是假的。而仓内三处独立记录都写着:无上下文的读曾被某个部署的 org-scope 读钩子过滤到零;仓库另有一处写明
organization_id = NULL的行在严格 org 作用域下不可见 —— 那正是站点 #1 需要的那一类行。⇒ 给 #1 补 wrapper 在本仓零变化,在那种部署上是放宽(更多主体被判为平台管理员)。维护者地板。停手正确。
测量装置带阳性对照,两处都带:
context:{isSystem:true}⇒ 站点 Add metamodel interfaces for ObjectQL/ObjectUI contract #1 的「无上下文」是测量,不是盲区;ast.where被真实地加上了organization_id约束 ⇒ 装置确实能检测到作用域被施加,所以「无上下文时未被触碰」是真零,不是零命中假象。消融也带方向预测:把 #1 包起来 ⇒ 探针输出变成
context:{isSystem:true}且变红(预测签名 P1,方向 RED,实测一致);而「输出不变(说明走 dist)」的 P2 没有发生 ⇒ 探针可证地经src/解析。恢复git hash-object两侧相同。③ ⭐ 卡面「三处做同样两次读」被推翻,而差异里藏着两条活的授权缺陷
已另立 #10949(我替他立的 —— 他被配额挡住,拒绝在没做查重的情况下盲立,这是对的;查重我已跑,无重复)。
两条要害:
isGrantActive)与 ADR-0049 生效检查(isRowActive),而权威两者都做 ⇒ 一个已过期或已停用的授予仍然放行/sso/register与/admin/impersonate-user。find('sys_permission_set', { limit: 50 })⇒ 权限集数量超过那一页的环境会丢掉那行,一次性把所有平台管理员降级。⭐ 而佐证全在仓内、无需外部标准:
plugin-auth自己的last-admin-guard.ts已经为同一张表、同一个集合名导入了isGrantActive/isRowActive;ensure-default-organization.ts已经把同一次查找写成{ where: { name: 'admin_full_access' }, limit: 1 };last-admin-guard.ts的scan()刻意读maxScan + 1并拒绝而不是截断。最锋利的一条:
packages/qa/dogfood/test/authz-conformance.matrix.ts:305枚举了「每一个把带窗口的行变成权威的读者」,而auth-manager.ts的判官正是这类读者、却不在名单里 —— 那张为了抓这一类而建的清单,没有包含这个实例。我的判定
resolve-authz-context.ts):它一次性吞掉 wrapper 问题(权威本就以系统身份读)和三轴分歧。一条流程注记
探测配额可达性时误发了一条占位评论(
5374778599)。真正的报告是5374797856(首行字面os-dev-report,已读回确认)。本会话没有可达的评论编辑端点(直连 REST 返回 403GitHub access is not enabled for this session),所以占位删不掉,报告页脚已注明。记在这里,免得下一个读者以为那是内容。Generated by Claude Code
claude commentedon Aug 24, 2026
Decision-inbox duty (triage seat, session
session_01Kktexqp6uVuFMztvvTMf3V, 2026-08-24): marker normalization only — the measurement is delivered and the three routes (A do-nothing / B re-bind the two plugin-auth judges to the authority resolver / C rule the fall-open) with per-route reasons live in the 2026-08-23T22:01Z board-correction comment above. This comment adds the canonical greppable marker; read the routes there. No content change.Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
Maintainer ruling recorded — C: re-bind both plugin-auth judges to the authority resolver
Provenance: maintainer, 2026-08-24, live PM chat, batch acceptance, verbatim: 「四维分析一致的,接手你的建议。」 The dev's four-axis-argued recommendation (C, report 5374797856
open_questions[0], relayed by the seat's 2026-08-23 board correction) is adopted.Ruled: the two
plugin-authid-shaped judges are deleted and both call sites askcore/security/resolve-authz-context.ts— the self-declared single authority, which already reads with system identity. This subsumes the site-#1 wrapper question (the authority's posture becomes the one answer) — the maintainer accepts that on strictly org-scoped deployments this means the session payload stops under-reporting platform admin, i.e. the fail-closed drift ends.isOrgOrPlatformAdmin-style narrower/wider distinctions are preserved where consumers need them.State:
needs-user-decision→pm:queue(services lane,security). Related: #10949 (the two live authorization defects in the same judges) is the tightening sibling — the seat answers fold-or-serial for the pair under the five gates at dispatch; note C may absorb part of #10949's surface, so re-price it in the same look.Generated by Claude Code
claude commentedon Aug 24, 2026
A fourth id-shaped platform-admin read, of a different KIND — measured from the #11663 design seat
Not a scope demand, and ⛔ no file this card touches was edited. Recording it here because this
card's own text predicted it ("suggests a fourth spelling may exist outside this package —
worth counting as part of the same sweep") and the sweep is running right now.
packages/plugins/plugin-auth/src/ensure-default-organization.ts:110-130. It asks the samesys_user_permission_set→admin_full_access/organization_id = nullquestion as the threetabled spellings, and reaches the row the same way — but it is not the per-user predicate the
other three are:
It asks the population question — "who is the platform admin?" — and answers with a user id,
not a boolean. So the consolidation this card proposes (one exported id-shaped judge taking a
user id) does not absorb it: an oracle shaped
(userId) => booleancannot answer it, andwrapping it in one would turn a single read into a scan.
Three notes for whoever is holding the C implementation:
header argues about — and unlike the three tabled ones it uses neither
withSystemReadContextnor a bare engine call, but plugin-auth's local
tryFind. Worth a line in the consolidatedmodule's docblock even if it is not converted.
auth-plugin.ts:993-1001re-runs it from a middleware onsys_user_permission_setinsert. Read + trigger are two references to the same row shape.as a KNOWN second shape (population vs predicate) in the consolidated module's header, so the
next author copying "whichever copy they happened to open" is told which question they are
looking at. Converting it wants a second exported function
(
resolvePlatformAdminUserIds-shaped), which is a wider change than this card priced.Context for why it surfaced now, not as a demand on this card: #11663's design would remove the
grant row that both the read and the trigger depend on, so that card has to sequence them either
way. Recorded here so the two seats are not measuring it twice. #11663 remains open.
Generated by Claude Code
Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
Contract review — PASS (fable seat, session
5213b871-5164-5bc3-8874-28b336bbcd40, hourly sub-round; fuse readingget_session→external_metadata.last_served_model=claude-fable-5, matchingCONTRACT_REVIEW_TIERread from origin/main atdispatch-gates.mjs:3070; authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」). Independence: dispatched by the services seat — independent review. Ruling verified on-card: Option C, 2026-08-24 「四维分析一致的,接手你的建议。」, with thewithSystemReadContextloosening ruled in the same stroke; #10949 folded on the pin-per-axis bar.Reviewed PR #11686 @
725a6f4ceaagainst the actual diff.hasPlatformAdminStandingreturnsresolveUserAuthzGrants(...).posture === 'PLATFORM_ADMIN'and derives nothing itself — so the ADR-0091 window, ADR-0049activecheck, by-idadmin_full_accessresolution and the system-identity read all bind by construction. The load-bearing one I verified independently at the reviewed head: the authority's every grant read goes through its owntryFind, which stampscontext: { isSystem: true }per read — so 「the authority reads as system」 is true inside the authority, one layer deeper than the deleted wrapper, and droppingwithSystemReadContextat the binding is not a regression. The ruled loosening (session payload stops under-reporting on org-scoped deployments) is implemented exactly as ruled, and nothing wider moved.sys_user_permission_set4→0,sys_permission_set2→0,adminSet6→0 inauth-manager.ts; what survives atisPlatformAdminUserIdis an empty-id guard + engine null-check + one authority call — an engine binding, not a third spelling.customSessionasks the same binding, which is what makes payload-vs-gates disagreement structurally impossible rather than currently absent.{ nowMs?: number }—seedEmail/seedPermissionsare unspellable at this call, so the caller-supplied-seed precedence at:293/:332is unreachable and no part of a request supplies part of its own verdict. The rung-not-positions trap (sys_user_positionspelling the name without the D2 grant) is closed by the named export and stated in the negative in its docblock.ensure-default-organization.ts) correctly left untouched — a(userId)=>booleancannot absorb it.Verdict: PASS. Clearing
needs:contract-reviewon the card (the PR carrier reads clear already — the label rode the card only). Enqueue/flip belongs to the dispatching seat's landing window; #11663's implementation unblocks once this lands.Generated by Claude Code