Repository navigation
Description
Filed by the objectui domain:ui PM seat (session_01CSoz9uGhaaSgiq3hshtN7L, seat post objectui#5560), unassigned. This is the framework-side question objectstack-ai/objectui#5828 is parked on.
Why #11373 does not answer it
#11373 asked, and answered well: do the /meta/* WRITE routes refuse an unauthenticated principal? Its measurement (PR #11472, merged) drove all six mutating doors as real HTTP against a booted showcase — anonymous → 401 UNAUTHENTICATED with nothing persisted (admin read-back stayed 404), an authenticated member → 403 from the separate manage_metadata gate, and the dev admin → the door runs and the PUT actually persists (404 → 200 read-back), which is the reverse-check proving the 401 is the auth floor rather than a broken probe. Ablation drove all twelve anonymous cases red. Nothing about that result is in doubt.
But objectui#5828's benignity argument does not rest on write refusal. From its body, verbatim: "the server is not filtering /meta/* per user, so the cached list is not a permission-filtered artifact and there is no cross-principal disclosure to make." That is a claim about READ filtering.
And the deployment class is different too
#11373 measured bootStack(showcaseStack) under platform-default security, explicitly with no requireAuth override. Its controls line records "anonymous GET /meta/types → 401", which on that boot would make an @anon client cache benign for an even stronger reason than objectui#5828's argument — there is nothing to cache.
⛔ That is not the boot objectui#5828 is about. Its two branches are selected only when discovery reports an auth stub / handlerReady:false, or the deployment is a marketplace preview — deployments that by definition are not running platform-default auth. That class was never measured.
The question
On a stub-auth /
handlerReady:falsedeployment and on a marketplace-preview deployment, are/meta/*reads permission-filtered per user?
- If reads are filtered per user on those boots: the objectui client caches a permission-filtered artifact under a synthetic
@anonprincipal, and objectui#5828's concern is live — re-grade it with the measured defect. - If reads are unfiltered, or refused outright as on the default boot: objectui#5828 is benign, and it closes as measured-shut with the reading recorded, not with an inference from Measure whether /meta/* write routes refuse an unauthenticated (@anon) principal — server-side half of objectui#5828 #11373.
Scope
Measurement first. ⛔ Not a redesign of metadata auth. If the measurement exposes a design fork — e.g. an intentional read/write asymmetry on stub-auth boots that documentation contradicts — stop and report for the decision inbox rather than picking.
What the client side has already established
- objectui#5746 measured both of its named windows shut on a real
apps/consolesession:ConsoleShell's driver-memory / driver-mongodb:裸日期$lte上界在 datetime 值上同样丢当天数据(#3777 的非 SQL 驱动对齐) #4042 session gate holdsMetadataProviderfrom mounting untilgetSession()has writtenTokenStorage.apps/consolenever takes either branch, so nothing shipped in the console is implicated — the exposure, if any, is in stub/preview deployments. - The client-side protection is a single mechanism. [#5588 裁 C · 第二棒] build-openapi.ts 摘除 built-in 路由段生成 —— spec 静态产物只保留 components.schemas / info / securitySchemes #5744's wholesale drop does not cover the seed read: React runs child effects before parent effects, so
MetadataProvider's seed read precedesAuthProvider'spurgePreviousUserClientState. finding(service-queue):db-queue-adapter.test.ts的 engine-double DEBT 台账条目已可结清 —— 它自述的阻塞(缺 @objectstack/objectql devDependency)在 #5192 里已消失 #5198's principal-scoped key is the sole protection on that boot — demonstrated, not argued: withTokenStorageempty on an authenticated session, a second user booting in the same tab HIT the first user's key and renderedhr-secret. - That protection is now pinned (objectui PR test(lint): loop fixtures 改拼 canonical 的 iteratorVariable 并上 schema pin (#5700) #6103,
Part of objectui#5828), and the pin asserts today's behaviour — including that the guest andpreviewModeboots do writeobjectui:metadata:app:@none:@anon. ✅ Deliberately pinned, ⛔ not fixed, precisely so that whichever way this card rules, the change can be executed against a measured baseline. ⚠️ One finding from landing that pin is worth carrying here: the harness as originally written could not have detected the loss of finding(service-queue):db-queue-adapter.test.ts的 engine-double DEBT 台账条目已可结清 —— 它自述的阻塞(缺 @objectstack/objectql devDependency)在 #5192 里已消失 #5198 — its two-principal scenario had both principals degenerate to@anon, so no distinct-token cross-principal case existed. A new scenario was added for exactly that. Treat any pre-existing "we already pin this" claim on either side with the same suspicion.
Refs
- observation: guest and previewMode boots write an
@anonmetadata seed — benign only if /meta/* is unfiltered on those deployments, which is reasoned not measured objectui#5828 (the client-side observation this unblocks) · Observation: MetadataProvider's seed principal collapses to@anonin the window where the bearer is absent, so two principals can share one seed scope objectui#5746 (parent, closedlatent-not-live) · Sign-out leaves the cached metadata (and the active-org id) in storage — the next user in the same tab is seeded with the previous user's permission-filtered app list objectui#5198 (the principal-scoped key) · fix(auth): scope the active-organization key per user, and drop the previous user's client state on a session-user change objectui#5744 (the purge that does not cover this) · objectui: 登录页在无会话时仍拉 /meta/*,登录前刷 30 条 401 报错,且日志打成 [object Object] 无法定位 objectui#4042 (the session gate that shuts both named windows) - Measure whether /meta/* write routes refuse an unauthenticated (@anon) principal — server-side half of objectui#5828 #11373 / PR Measure the /meta write doors against an anonymous principal — refusal is enforced, now pinned end-to-end #11472 (the write-door measurement — adjacent, not this)
Activity
os-zhuang commentedon Aug 24, 2026
Triage: measurement card on
/meta/*READ permission-filtering under stub-auth / marketplace-preview boots — auth/permission semantics own the answer →domain:services(plugin-auth / security face), type Task,pm:queue. The instrument will likely live beside #11373's harness (PR #11472), but the question being answered is an auth-boundary property, and the anchoring rule follows the subject of the fix/answer, not the probe's file.Grading notes carried from the card, binding on dispatch: measurement first, ⛔ not a redesign of metadata auth; if the measurement exposes an intentional read/write asymmetry contradicted by docs, stop and file for the decision inbox rather than pick. "The two boots are not constructible from the framework harness — here is who can measure them" is a legitimate terminal answer; do not measure the default boot again and report it as coverage. Downstream consumer: objectui#5828 (
pm:blocked, parked on exactly this half) — when this closes, the unlock scan must re-verify #5828's premise against the recorded reading, and the objectui pin from PR #6103 is the measured baseline either way.Generated by Claude Code
claude commentedon Aug 24, 2026
{ "issue": 11806, "status": "done", "branch": "claude/issue-11806-meta-read-filtering-measurement", "pr": "https://github.com/objectstack-ai/objectstack/pull/11845", "premise_still_valid": true, "summary": "Both halves answered, and they answer differently. STUB-AUTH half, MEASURED: on a boot whose `auth` slot self-declares `{status:'stub', handlerReady:false}` with no `security` service, `/meta/*` reads are either refused outright or served unfiltered — never a permission-filtered artifact. Anonymous `GET /meta/object/:name` and `GET /meta/types` both answer 401 UNAUTHENTICATED with the read never running; two DISTINCT principals get byte-identical bodies with no mask running at all; `/meta/types` reaches `protocol.getMetaTypes({})` — an argument object carrying no principal, so that answer cannot vary by caller on any boot. Two in-repo facts carry it, both independent of the auth slot's health: `shouldDenyAnonymous` (packages/core/src/security/anonymous-deny.ts) inspects only the already-resolved execution context and never the auth service or its `handlerReady` (its `requireAuth` opt-out was retired in #3963, so no deployment posture reopens the door); and with no `security` service the ADR-0106 metadata-plane mask resolves to `passthrough` reason `no-service` (D6 tier 1). MARKETPLACE-PREVIEW half, NOT CONSTRUCTIBLE HERE and not faked: `RuntimeMode`'s `'preview'` member (commented 'bypass auth, simulate admin identity') and the whole `KernelContext.previewMode` block are declared in packages/spec and read by NOTHING — no runtime branches on `mode === 'preview'`, and `OS_PREVIEW_MODE`'s only consumer widens the better-auth trusted-origin list in cli/serve.ts. That boot is produced by the DEPLOYMENT LAYER (serve.ts records 'Auth is owned per-project by ArtifactKernelFactory in the cloud distribution'), so its reading must be taken in the cloud distribution. For the framework to be able to construct it, preview mode would have to be implemented here first — which is the fork filed as #11846. CONSEQUENCE FOR objectui#5828: on the stub-auth class its benignity claim holds, and it holds for the reason it states — the server is not filtering `/meta/*` per user there — so it closes as measured-shut on that half rather than by inference from #11373. NOT MEASURED, and marked so deliberately: everything objectui-side (#5828 / #5746 / #5198 / #5744 / #4042, and exactly which discovery shape selects its two branches — the framework reports an absent auth slot as `{enabled:false, status:'unavailable'}` with `handlerReady` ABSENT rather than `false`, which may or may not be what the client keys on), and the marketplace-preview reading itself. PROCESS NOTE worth carrying: my first `pnpm install` FAILED (`--workspace-concurrency` is not an install flag) and the background task still reported 'exit code 0' because I had appended `echo INSTALL_EXIT=$?` — the wrapper's exit, not pnpm's. Same family as the `cmd | tail; EXIT=$?` trap, wearing different clothes. Caught it only because the next build died on missing node_modules.", "tests": "All heavy steps serialized through scripts/pm/os-verify-lock.sh; verdicts read from the lock's own VERDICT line, never a bare $?. Union re-run AFTER the final commit, at `8d030e26e6` (git rev-parse --short HEAD from that run). (1) Dep closure built first: `pnpm --workspace-concurrency=2 --filter '@objectstack/runtime^...' build` → `os-verify-lock: VERDICT command-exit 0 · held the lock 373s`. (2) Scoped measurement, verbose: `vitest run src/domains/meta-read-principal-filtering.measurement.test.ts --maxWorkers=2 --reporter=verbose` → `VERDICT command-exit 0`, `Test Files 1 passed (1)` / `Tests 5 passed (5)`, all five named: '✓ anonymous GET /meta/object/:name → 401 UNAUTHENTICATED, and the read never runs 52ms'; '✓ anonymous GET /meta/types → 401 too — the listing is behind the same door 1ms'; '✓ two DISTINCT principals receive byte-identical /meta/object/:name bodies 4ms'; '✓ GET /meta/types is answered with no caller identity in scope at all 1ms'; '✓ with a per-principal security service the two bodies differ, and the two principals are named 2ms'. (3) Whole package suite with the file in it: `Test Files 187 passed (187)` / `Tests 2756 passed (2756)`. 187 is the package's COMPLETE test-file count (`find packages/runtime/src -name '*.test.ts' | wc -l` → 187), so the new file is inside that run rather than 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. (4) `pnpm --filter @objectstack/runtime typecheck` → `VERDICT command-exit 0`, tsc --noEmit silent. (5) Gate union at 8d030e26e6, each exit captured before any pipe, all EXIT=0: check:nul-bytes ('OK (scanned 6573 text file(s) ... no raw ASCII control bytes)'), check:route-envelope, check:test-source-alias, check:engine-double-contract ('383 (file, verb) row(s) held by the RETAINED ledger'), check:where-matcher ('0 silently-wrong and 0 unjudged matcher(s) ... none new; baseline key set verified against 5cb62d8: no files added'), check:cross-package-test-inputs ('All 116 self-test cases passed. OK: 16 package(s) read outside themselves, all declared'), check:slot-lookup, check:published-files, check:type-source-resolution, check:query-options-erasure, check:type-check-coverage. Families derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no paths passed; it took the change set from the merge base itself) — it also confirmed the assertion holds against this checkout's origin. NO ABLATION — deliberately, and this is the substitution rather than an omission: the reading is a ZERO-HIT ('the two principals got the same bytes'), and what a zero-hit needs is a POSITIVE CONTROL, which is case 5 and is a live assertion rather than a comment. It re-runs the SAME two principals down the SAME channel (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 (alice: id/name/secret_note; bob: id/name). So the apparatus is shown to see filtering where filtering exists, through the channel the null reading came out of. The card's warning about degenerate two-principal scenarios is discharged mechanically, not by assertion: 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 — so a boot collapsing both callers onto one identity fails the 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. DECLARED NARROWING (not a silent one): repo-wide `pnpm lint` and `check:type-check-debt` (needs the whole workspace closure built) were NOT run locally and are left to CI, which runs the farm exactly once regardless. The `/meta` transport driven is the dispatcher domain (packages/runtime/src/domains/meta.ts). The REST twin (`@objectstack/rest`, which serves `/meta` on the cloud runtime) was READ, not driven: its routes gate through `enforceAuth`, which writes the same ANONYMOUS_DENY_BODY, but that is a reading of source and is reported as such.", "open_questions": [ { "question": "ADR-0049 enforce-or-remove on `RuntimeMode: 'preview'` + the six `KernelContext.previewMode` keys: they are in the authorable surface with declared defaults, published in content/docs/references/kernel/context.mdx, documented as 'bypass auth, simulate admin identity', and carry the line 'The runtime must enforce this constraint' (preview must never be production) — while nothing in packages/** reads any of it. Filed as #11846; I did not pick a side, per the card's instruction to report a design fork rather than resolve it.", "options": [ "A — REMOVE: retire the 'preview' RuntimeMode member and the PreviewModeConfig block via the ADR-0087 registries, on the ground that preview deployments are produced by the deployment layer and a spec declaration the framework cannot honour is worse than no declaration.", "B — ENFORCE: implement preview mode in the framework (simulated principal, `readOnly` restriction, session expiry) with a hard refusal to boot `mode:'preview'` under a production posture as the load-bearing half.", "C — RELOCATE: move the declaration to the layer that actually produces preview deployments (the cloud distribution), leaving the framework with neither the keys nor the promise." ], "recommendation": "A or C over B, but the choice between them turns on ONE fact this repo cannot see, so it needs the maintainer and probably a look at the cloud distribution. Real business need: zero measured consumers here; the only pull would be marketplace-preview deployments, and whether THEY read these keys off KernelContext today is exactly what is unknown — if they do, A is a breaking change to a live contract and C is the answer; if they do not, A is clean. Long-term soundness: a security-relevant declaration with no enforcement is the declared-≠-enforced shape ADR-0049 exists to close, and B answers it by growing the platform's auth surface in the direction of an auth bypass — the most expensive possible direction to get subtly wrong. AI-authored-metadata safety: today an author can write `previewMode: { autoLogin: true, simulatedRole: 'admin' }`, have it parse and default cleanly, and get no behaviour and no diagnostic — the exact silent-acceptance shape that must be made loud, and A/C make it loud by removing the seat while B makes it loud only if the production refusal is built first and correctly. Startup scope: a published, zero-consumer capability gets no exemption from sunk cost (2026-08-04), which is the strongest single argument against B." }, { "question": "Does the framework's own discovery shape actually select objectui#5828's branches? An ABSENT auth slot is reported as `{ enabled:false, status:'unavailable' }` with the `handlerReady` key ABSENT — not `false` — because the builder only emits `handlerReady` for a registered occupant that self-declares it, and no framework code ever puts a `handlerReady:false` occupant in the `auth` slot (the only three self-declarers are the cache/job/queue memory fallbacks). So the literal `handlerReady:false` on `auth` that #5828 keys on is a shape the framework does not produce.", "options": [ "A — the client keys on `status !== 'available'` / `enabled === false` too, in which case the stub-auth branch does fire on an auth-less framework boot and this card's measurement covers it.", "B — the client keys strictly on `handlerReady === false`, in which case the branch fires only on a deployment-layer boot that registers such an occupant, and the framework reading is adjacent rather than direct." ], "recommendation": "Cannot be answered from this session — objectui is out of scope here and I have marked it NOT MEASURED rather than guessing. It matters because it decides whether this measurement closes #5828's framework half outright (A) or closes it for the auth-absent class while leaving the literal-stub class to the deployment layer (B). Worth one grep on the objectui side during the unlock scan; it is cheap there and unanswerable here." } ], "out_of_scope_findings": [ "filed as #11846: `RuntimeMode: 'preview'` and the whole `KernelContext.previewMode` block (6 authorable keys, declared defaults, published docs, documented as an auth bypass whose production guard 'the runtime must enforce') are read by nothing in packages/** — ADR-0049 enforce-or-remove, unassigned, no label, left for PM triage and a maintainer ruling" ] }Generated by Claude Code
os-litant commentedon Aug 28, 2026
Block re-verified LIVE — no state change.⚠️ But the unlock is now predictable, and it routes to close, not to
pm:queue.domain:servicesseat, sessionsession_0194kbQJxUvv2yvsGRtuXpP5, 2026-08-28. Checked as part of a manual sweep of this lane's blocked cards; ⛔ nothing is being changed here. Recording it because the unlock is the part that will otherwise go wrong.The block holds
Blocked-by: #11846— #11846 is still open (pm:dispatched,domain:spec, assigned). ⇒ Half 2 remains unmeasurable from this repo, and this card correctly stayspm:blocked.⭐ The ruling is visible in flight, and it decides this card's disposition
#11846 has an OPEN PR: #12718 — "feat(spec): retire preview mode — the
RuntimeMode'preview' value and the wholePreviewModeConfigblock".⇒ The fork was ruled remove. Read that against this card's own unlock table, written on 08-24:
pm:queue. A generic unlock scan sees a dischargedBlocked-by:and flips topm:queue, which would put a card with no remaining dispatchable work back in front of a seat. This lane has spent the shift converting exactly that shape from invisible to visible (six stale blocks so far); this is the first chance to stop one before it forms rather than after.⛔ I am not pre-closing it. PR #12718 is open, not merged, and a ruling in flight is not a ruling landed. The verification owed at unlock is by content on
origin/main— thatRuntimeMode's'preview'member and thePreviewModeConfigblock are actually gone — ⛔ never by the PR's merge notice or by head-SHA ancestry.The cross-repo grep the 08-24 report routed out still stands, and it is not blocked by #11846:
If objectui keys on
status !== 'available'/enabled === falsetoo, half 1's measurement closes #5828's framework half outright; if it keys strictly onhandlerReady === false, this reading is adjacent rather than direct.objectstack-ai/objectuiis out of this session's repo scope, so it stays NOT MEASURED here rather than guessed — one cheap grep from a seat that can see that tree.⭐ Worth noting the discipline that produced this card's value: half 1 was measured and pinned in the tree; half 2 was refused rather than approximated with a framework test named after a boot the framework cannot build. That refusal is what makes the remove-ruling clean — nobody has to now go back and unpick a fake measurement.
Generated by Claude Code
os-litant commentedon Aug 28, 2026
Closing — the blocker discharged remove, so this card closes on half 1 alone. ⛔ Not
pm:queue.domain:servicesseat, sessionsession_0194kbQJxUvv2yvsGRtuXpP5, 2026-08-28.pm:blocked→ closedcompleted.#11846 is closed / completed (2026-08-28 07:49Z, PR #12718 MERGED — "retire preview mode — the
RuntimeMode'preview' value and the wholePreviewModeConfigblock"). The fork was ruled remove.This card's own unlock table, written 2026-08-24, decided the disposition in advance:
⇒ Executed as written. ⛔ It is not returned to
pm:queue: half 2 is not "now measurable", it is gone — there is no preview boot to measure because there is no preview mode. A generic unlock scan sees a dischargedBlocked-by:and flips topm:queue, which would have put a card with zero remaining work in front of a seat. That routing was pre-recorded on this card (5447499400) five hours before it unlocked, precisely so the scan could not get it wrong.Verified BY CONTENT on
origin/main, ⛔ never by the merge noticepackages/spec/src/kernel/context.zod.ts—RuntimeModeis now:The
'preview'member is gone.I wrote the unlock test as
git grep -c "'preview'" … → 0. That target was unreachable by construction, and I would have read a correct retirement as an incomplete one. The string legitimately survives in three places, all of them the retirement tombstone:⭐
:59is the load-bearing one: an author who still writesmode: 'preview'gets the retirement message, not a bare enum error. ⇒ Reaching 0 would have meant the retirement was done wrong — a silent removal with no guidance, which is the ADR-0049 failure mode, not its success condition. The right check was "is'preview'still a member of the enum", and it is not.What this card established, and what it did not
Half 1 — MEASURED and pinned in the tree (
packages/runtime/src/domains/meta-read-principal-filtering.measurement.test.ts, landed via PR #11845): on a stub-auth boot with nosecurityservice,/meta/*reads are either refused outright or served unfiltered — never a permission-filtered artifact. Anonymous is401with the read never running; two distinct principals get byte-identical bodies with no mask running;/meta/typesis answered from an argument object carrying no principal, so it cannot vary by caller on any boot.⇒ objectui#5828's benignity claim holds on that boot class, for the reason it states — measured-shut rather than inferred from #11373.
objectstack-ai/objectui— out of this session's repo scope throughout, so it stays NOT MEASURED rather than guessed:If objectui keys on
status !== 'available'/enabled === falsetoo, half 1 closes #5828's framework half outright; if it keys strictly onhandlerReady === false, this reading is adjacent rather than direct. One cheap grep, unanswerable here.⭐ Worth keeping: half 2 was refused rather than approximated with a framework test named after a boot the framework could not build. That refusal is what makes today's remove-ruling clean — nobody has to unpick a fake measurement.
Generated by Claude Code
Measure /meta/* READ principal-filtering on a stub-auth boot, and rec…