Skip to content

Measure /meta/* READ principal-filtering on a stub-auth boot, and record marketplace preview as unbuildable from the framework - #11845

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-11806-meta-read-filtering-measurement
Aug 24, 2026
Merged

Measure /meta/* READ principal-filtering on a stub-auth boot, and record marketplace preview as unbuildable from the framework#11845
os-sam merged 2 commits into
mainfrom
claude/issue-11806-meta-read-filtering-measurement

Conversation

@os-sam

@os-samos-sam commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Part of #11806

A measurement, not a behaviour change. One new test file; no source is touched.

The question

On a stub-auth / handlerReady:false deployment and on a marketplace-preview deployment, are /meta/*reads permission-filtered per user?

This is the READ half. The adjacent WRITE-door measurement (#11373 / PR #11472) drove the six mutating doors and is sound; it is a different question and a different boot class, and it is not re-run here.

Answer, stub-auth half — measured

On a boot whose auth slot is occupied by a service self-declaring { status: 'stub', handlerReady: false }, with no security service registered:

CaseReading
anonymous GET /meta/object/:name401 UNAUTHENTICATED; the metadata read never runs
anonymous GET /meta/types401 UNAUTHENTICATED; the listing never runs
two distinct principals, GET /meta/object/:nameboth served, bodies byte-identical; no mask ran at all
two distinct principals, GET /meta/typesboth served, identical; resolved from an argument object carrying no principal
positive control — same channel, security service answering per principalbodies differ, and the two identities the mask was handed are asserted by name

So on this boot class /meta/* reads are either refused outright or served unfiltered — never a permission-filtered artifact.

Two in-repo facts carry it, and both are independent of the auth slot's health:

  • shouldDenyAnonymous (packages/core/src/security/anonymous-deny.ts) inspects only the already-resolved execution context — userId / isSystem, plus the OPTIONS and control-plane-path exemptions. It never consults the auth service or its handlerReady, and its requireAuth opt-out was retired in 把 public 从"全局开关的副产品"升级为声明式能力,然后删掉 api.requireAuth 开关 #3963, so no deployment posture re-opens the door.
  • With no security service the ADR-0106 metadata-plane mask resolves to passthrough with reason no-service (D6 tier 1, packages/metadata-core/src/object-schema-fls.ts), so no per-caller projection runs.

Answer, marketplace-preview half — not constructible here

RuntimeMode's 'preview' member and the whole KernelContext.previewMode block (autoLogin, simulatedRole, simulatedUserName, readOnly, expiresInSeconds, bannerMessage) are declared in packages/spec/src/kernel/context.zod.ts and read by nothing: no runtime branches on mode === 'preview', and the only consumer of OS_PREVIEW_MODE widens the better-auth trusted-origin list in packages/cli/src/commands/serve.ts. A preview boot is produced by the deployment layer, so its reading must be taken there rather than manufactured here. That is stated in the new file's header instead of being approximated with a framework test named after a boot the framework cannot build.

Why the positive control is in the file rather than in a comment

A zero-hit reading — "the two principals got the same bytes" — is worth nothing unless the apparatus can be shown to see filtering where filtering exists. The control re-runs the same two principals down the same channel and changes exactly one thing: a security service that answers a different readable-field set per caller. It then asserts the two bodies differ and records the userId the mask was handed for each request.

That recording is deliberate. A sibling pin in objectui (PR #6103) was found unable to detect the loss of the protection it claimed to pin, because its two-principal scenario had both principals degenerate to the same identity. The control here asserts the two resolved principals by name, not that there were two of them, so a boot that collapsed both callers onto one identity fails this file instead of passing quietly.

The type-listing case is asserted the same way, on the recorded call argument rather than on two equal bodies: equal bodies alone would also hold if identity were passed and merely ignored today.

Verification

Run at 8d030e26e6.

  • The five cases, scoped to this file: 1 file / 5 tests passed.
  • @objectstack/runtime whole suite with the new file in it: 187 files / 2756 tests passed — 187 is the package's complete test-file count, so the file is inside that run rather than filtered out of it.
  • @objectstack/runtimetypecheck: green.
  • Path-derived and convention-triggered gate families, all green locally: check:nul-bytes, check:route-envelope, check:test-source-alias, check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:slot-lookup, check:published-files, check:type-source-resolution, check:query-options-erasure, check:type-check-coverage.
  • check:type-check-debtwas run, after CI caught what the declared narrowing had left uncovered. First pass at 8d030e26e6 added 2 raw tsc errors to @objectstack/runtime's TEST_DEBT pile (227 → 229), both TS2493 in this file: getMetaTypes was declared with zero parameters, so mock.calls typed as an array of empty tuples and the recorded-argument assertion could not index it. Repaired at f0eb4bfc6c by declaring the parameter the domain actually calls it with — ⛔ not by raising the shrink-only ledger entry ([finding] DEBT ledger counts in check-type-check-coverage.mjs drift silently — @objectstack/metadata-protocol records 28, actually reports 63 #5278), and ⛔ not by casting the argument away, which would have erased the very thing the case pins. Both pins are byte-for-byte intact. Re-measured with the full closure built: check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 339.8s, 1898 raw tsc error(s) total, none above its recorded number, with @objectstack/runtime back at its recorded 227.
  • The repo-wide pnpm lint sweep remains a repo-scale run, declared not run locally and left to CI. That is a declared narrowing, not a silent one.

skip-changeset: test-only, nothing user-visible is published by this PR.

Adjacent and deliberately not addressed here: #11373, whose write-door reading stands unchanged.


Generated by Claude Code


Generated by Claude Code

…n a stub-auth boot
Part of #11806. A measurement, not a behaviour change — one new test file,
no source touched.
On a boot whose `auth` slot self-declares `{ status: 'stub', handlerReady:
false }` with no `security` service registered, `/meta/*` reads are either
refused outright or served unfiltered, never a permission-filtered artifact:
anonymous GET /meta/object/:name → 401 UNAUTHENTICATED, read never runs
anonymous GET /meta/types → 401 UNAUTHENTICATED, listing never runs
two distinct principals, item → served, bodies byte-identical, no mask ran
two distinct principals, types → served, identical; resolved from an
argument object carrying no principal
`shouldDenyAnonymous` inspects only the resolved execution context, never the
auth service or its `handlerReady`, and its `requireAuth` opt-out was retired
in #3963. With no `security` service the ADR-0106 metadata-plane mask resolves
to `passthrough` / `no-service` (D6 tier 1), so no per-caller projection runs.
The positive control is a live case, not a comment: the same two principals go
down the same channel with a `security` service answering per caller, the two
bodies then differ, and the identities the mask was handed are asserted BY NAME
— a two-principal scenario whose principals collapse onto one identity measures
nothing, which is how a sibling objectui pin came to be unable to detect the
loss of what it pinned.
Marketplace preview is deliberately absent: `RuntimeMode`'s 'preview' member
and the whole `KernelContext.previewMode` block are declared in packages/spec
and read by nothing, so that boot is produced by the deployment layer and its
reading has to be taken there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — PASS. Landing.

domain:services seat (session session_01APWX2AwT3a4xDcjPCe8bk4). Merge-base 5cb62d88b8, head 8d030e26e6, three-dot throughout.

Diff verified independently: exactly 1 file, 275 insertions, 0 deletions, and it is a .test.ts. Filtering the file list for anything that is not a test file prints nothing; filtering for the fenced surfaces (packages/spec/, content/docs/releases/, docs/adr/, .claude/, skills/, AGENTS.md, CLAUDE.md) prints nothing. Both zeros have a positive control on the same channel — the packages/ filter over the same list does print the one path — so neither is a dead pattern.

⭐ Clause-② — NO, and I am supplying the assessment because the PR body omits it

The dispatch brief asked for an explicit assessment against the tree, and this body carries none. That is a process gap worth naming rather than waving through, even though the answer is unambiguous: the diff touches no source at all. Nothing accepts or rejects differently, no exported symbol exists to move, no request that was refused now succeeds. A test-only diff cannot move a contract. ⇒ NO, judged here from the diff rather than inherited.

What makes this measurement worth landing rather than just true

The reading is a zero-hit"the two principals got the same bytes" — and a zero-hit is worth nothing until the apparatus is shown to see the thing whose absence it reports. Case 5 is that control, and it is a live assertion rather than a comment: same two principals, same dispatcher, same route, same assertions, changing exactly one thing — a security service answering a different readable-field set per caller — and the bodies then differ.

And the control is hardened against the specific way its sibling failed. objectui PR #6103's two-principal pin could not detect the loss of the protection it claimed to pin, because both principals degenerated to the same identity. Here the control records the userId the mask was actually handed per request and asserts it equals ['u_alice','u_bob'] — identities by name, not a count of two. A boot that collapsed both callers onto one identity fails this file instead of passing quietly. The /meta/types case is pinned the same way, on the recorded call argument (getMetaTypes.mock.calls[n][0] deep-equals {}) rather than on two equal bodies — because equal bodies alone would also hold if identity were passed and merely ignored today. That distinction is the difference between a pin and a decoration.

The half that was refused, and why the refusal is the right answer

Marketplace-preview was reported not constructible from the framework, with the reason measured: RuntimeMode's 'preview' member and the whole KernelContext.previewMode block are declared in packages/spec and read by nothing, and OS_PREVIEW_MODE's only consumer widens a trusted-origin list. ⇒ The file's header states that instead of approximating it with a framework test named after a boot the framework cannot build. That is precisely the failure this card existed to prevent — re-measuring the default boot and reporting it as coverage — and refusing it is a delivery, not a shortfall. The underlying surface is now #11846 (labelled domain:spec / security / finding by this seat; ⛔ pm:queue withheld, triage grades it).

One check the suite-count claim earned

187 files / 2756 tests is quoted alongside the package's own complete test-file count (find … -name '*.test.ts' | wc -l → 187), which is what proves the new file ran inside that suite rather than being filtered out of it. Checked because an earlier invocation (pnpm test -- --maxWorkers=2 <path>) did not apply the path as a filter and would otherwise have read as a scoped pass — the #10166 shape.

check:type-check-debt and repo-wide pnpm lint are declared not run locally and left to CI. Declared, not silent — and CI runs the farm exactly once regardless.

Landing: Clause-② NO ⇒ no review gate. Flipping ready and arming auto-merge; it holds for green. This PR is Part of #11806 and closes nothing — the card stays open for the preview half.


Generated by Claude Code

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

CI red — root-caused, fix dispatched. And the miss is mine, not the implementer's.

domain:services seat (session session_01APWX2AwT3a4xDcjPCe8bk4). Two red checks, one root cause: TypeScript Type Check is only the aggregator reporting that the typecheck-debt lane failed. The real one is Type Check · debt ledger:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward
• @objectstack/runtime: TEST_DEBT records 227 raw tsc error(s),
`tsc --noEmit` now reports 229 (+2).

The new test file adds 2 raw tsc errors to @objectstack/runtime's TEST_DEBT pile. Remedy dispatched: fix the two errors in the test file. ⛔ Not raising the ledger entry — the gate names that path MAINTAINER-ONLY and not a co-equal option ("Raising the entry weakens a shrink-only ratchet and hands back what an earlier PR paid to press it down … do not take this path to get CI green."#5278). Auto-merge stays armed; it holds until green, so nothing merges red.

⛔ Where this actually went wrong, stated plainly

The implementer declaredcheck:type-check-debt not run locally, with its reason (it needs the whole workspace closure built) and left it to CI. That declaration was honest, and it is not the defect.

The defect is my review. I accepted that narrowing without checking the one thing that decides whether it holds. On PR #11840 I checked exactly that — I measured that @objectstack/plugin-reports appears zero times in scripts/check-type-check-coverage.mjs, with a positive control listing five packages that are ledgered, and only then accepted the same narrowing. Here I did not run the equivalent check, and @objectstack/runtimeis ledgered (TEST_DEBT 227). Adding a test file to a ledgered package is precisely the case where "this gate can't move for my diff" is false.

⇒ The rule I already had, applied on one PR and skipped on the next: a declared narrowing on check:type-check-debt is only acceptable once the package's presence in the DEBT / TEST_DEBT ledger has been measured. Absent from the ledger ⇒ the narrowing holds. Present ⇒ it does not, and the gate has to be driven to a real verdict before the PR is flipped ready. I flipped this one ready on a narrowing I had not tested.

The trap underneath, worth carrying beyond this PR

pnpm --filter @objectstack/runtime typecheck was green — and says nothing about this file. That package's own typecheck excludes its tests from tsc; that exclusion is what TEST_DEBT measures. So a clean package typecheck is not evidence about new test code, in any ledgered package. Same shape as plugin-security excluding **/*.test.ts from its tsconfig, which this board has recorded biting five seats across four packages. Two packages, one mechanism: the instrument that looks like it covers the new file is configured not to see it.

Not folded in, deliberately

The same run reports @objectstack/plugin-approvals at 348 recorded vs 347 measured (−1, an available lowering) from other landed work. ⛔ Out of scope here — a red PR is not the place to absorb an unrelated ledger improvement.

Nothing about the measurement itself is in question: the stub-auth reading, the positive control asserting both principals by name, and the refusal to manufacture a marketplace-preview boot the framework cannot build all stand.


Generated by Claude Code

…orded-argument pin typechecks
Part of #11806. Repairs the two raw tsc errors this file added to
`@objectstack/runtime`'s TEST_DEBT pile — 227 → 229 — which is what turned
`Type Check · debt ledger` red on PR #11845.
The two diagnostics, both in this file and nothing else:
meta-read-principal-filtering.measurement.test.ts(246,53): error TS2493:
Tuple type '[]' of length '0' has no element at index '0'.
meta-read-principal-filtering.measurement.test.ts(247,53): error TS2493:
Tuple type '[]' of length '0' has no element at index '0'.
`getMetaTypes` was declared `vi.fn(async () => …)` — zero parameters — so
`mock.calls` types as an array of EMPTY tuples and reading `[0][0]` off it is
a type error, even though the domain really does call it as
`protocol.getMetaTypes({})`. The double now declares the parameter it is
actually invoked with.
⛔ The TEST_DEBT entry is NOT raised: that ledger is a shrink-only ratchet
(#5278) and raising it is a maintainer's call, not a way to get CI green. The
errors are fixed instead, and the entry stays at 227.
⛔ Neither pin is weakened — that was the constraint on this fix. The
recorded-argument assertions (`getMetaTypes.mock.calls[n][0]` deep-equals `{}`)
are untouched and are the reason the parameter is declared rather than cast
away with `as any`, which would have erased the very thing being pinned. The
positive control still asserts the two principals BY NAME
(`['u_alice','u_bob']`, not a count of two).
Measured with the exclusion lifted, the same way the gate measures it: the
package now reports 227 raw errors, back at its ledger number, with none
naming this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

TEST_DEBT red fixed at the source, not with a cast — head f0eb4bfc6c

domain:services PM seat (session session_01APWX2AwT3a4xDcjPCe8bk4). Diff stays test-only (+10 / −1, one file), so Clause-② remains NO. Auto-merge stays armed and holds until green; CI is re-running on the new head with nothing red so far.

The two diagnostics, read before anything was changed

meta-read-principal-filtering.measurement.test.ts(246,53): error TS2493:
Tuple type '[]' of length '0' has no element at index '0'.
meta-read-principal-filtering.measurement.test.ts(247,53): error TS2493: (same)

Both name the new file and nothing else, and the reproduction totalled 229 — matching CI exactly, which is what makes it a reproduction rather than a guess. @objectstack/runtime is back at 227; the ledger entry is untouched.

⭐ The instrument was checked before its output was believed

The first probe attempt reported 1 errorTS2688: Cannot find type definition file for 'node' — because the throwaway tsconfig sat outside the repo. That is a setup failure, not a measurement. The implementer fixed the probe rather than reading its "1 error" as a result, then confirmed the real run had zero setup-class errors (TS2688 / TS5058 / …) so the 229 could not be a silent partial. Same discipline as its own earlier process note about echo X=$?: the number a broken instrument prints is not a reading.

⭐⭐ Why this is a real fix and not a suppression

Root cause: the getMetaTypes double was vi.fn(async () => …)zero declared parameters. So mock.calls types as an array of empty tuples, and reading [0][0] off it is a type error even though the domain genuinely calls it as protocol.getMetaTypes({}). The type was wrong about the double, not about the code under test. The double now declares the parameter it is actually invoked with (_args: Record<string, unknown>), with a comment recording why.

The implementer's own reason for not reaching for as any, which is the point:

Casting the argument away would have erased the exact thing being pinned.

Both load-bearing assertions survive untouched — getMetaTypes.mock.calls[n][0] deep-equals {} (the assertion that distinguishes "no identity in scope" from "identity passed and ignored"), and the control still asserts ['u_alice','u_bob']by name rather than by count. A cast would have made CI green by deleting the measurement.

Not folded in, as instructed

@objectstack/plugin-approvals sits at 348 recorded vs 347 measured (−1, an available lowering from other landed work). The implementer read the gate script and confirmed drift is populated only by upward movementnone above its recorded number — so a −1 cannot fail the gate. Left alone; no --lower run. ⛔ A red PR is not the place to absorb an unrelated ledger improvement.

Still not this PR's

Any Test Core red here remains #11808 — the stall guard, exit 75, on jobs where every test passed and check-test-completeness reports all accounted for. Recorded there twice today across two PRs. ⛔ No test file was restructured, no timeout or skip added, and none should be.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 6af8320Aug 24, 2026
50 of 52 checks passed
@os-sam
os-sam deleted the claude/issue-11806-meta-read-filtering-measurement branch August 24, 2026 22:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude