Skip to content

Measure whether /meta/* READS are permission-filtered on stub-auth and marketplace-preview boots — the half objectui#5828 still needs, which #11373 did not answer #11806

Closed

Description

@yinlianghui

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. ⚠️ It is not a duplicate of #11373, and the difference is the whole point of the card.

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. ⚠️ Refusing anonymous writes says nothing about whether reads are permission-filtered, so "benign by construction" does not follow from the one to the other.

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:false deployment and on a marketplace-preview deployment, are /meta/* reads permission-filtered per user?

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.

⚠️ Worth stating for whoever takes it: the two named boots may not be constructible from the framework's own test harness at all. If they are configurations only the deployment layer produces, that answer ("this cannot be measured from here, and here is who can") is a legitimate and useful result — much better than measuring the default boot again and reporting it as though it covered these.

What the client side has already established

Refs

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Aug 24, 2026
  2. added theissue type on Aug 24, 2026
  3. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    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

  4. claude commented on Aug 24, 2026

    @claude
    Contributor
    {
      "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

  5. os-litant commented on Aug 28, 2026

    @os-litant
    Contributor

    Block re-verified LIVE — no state change. ⚠️ But the unlock is now predictable, and it routes to close, not to pm:queue.

    domain:services seat, session session_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 stays pm: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 whole PreviewModeConfig block".

    ⇒ The fork was ruled remove. Read that against this card's own unlock table, written on 08-24:

    #11846 ruled ⇒ this card
    remove "this half is moot and this card closes on half 1 alone"
    enforce preview mode becomes constructible here; half 2 becomes real work
    relocate the reading is taken in the cloud distribution, not here

    ⚠️ So when PR #12718 lands, #11806 should be CLOSED — ⛔ not returned to pm:queue. A generic unlock scan sees a discharged Blocked-by: and flips to pm: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 — that RuntimeMode's 'preview' member and the PreviewModeConfig block are actually gone — ⛔ never by the PR's merge notice or by head-SHA ancestry.

    ⚠️ One item that is NOT discharged by any of this, and is not this repo's

    The cross-repo grep the 08-24 report routed out still stands, and it is not blocked by #11846:

    The framework reports an absent auth slot as { enabled: false, status: 'unavailable' } with handlerReady ABSENT — not false. No framework code ever puts a handlerReady:false occupant in the auth slot. ⇒ The literal handlerReady: false that objectui#5828 keys on is a shape the framework does not produce.

    If objectui keys on status !== 'available' / enabled === false too, half 1's measurement closes #5828's framework half outright; if it keys strictly on handlerReady === false, this reading is adjacent rather than direct. objectstack-ai/objectui is 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

  6. os-litant commented on Aug 28, 2026

    @os-litant
    Contributor

    Closing — the blocker discharged remove, so this card closes on half 1 alone. ⛔ Not pm:queue.

    domain:services seat, session session_0194kbQJxUvv2yvsGRtuXpP5, 2026-08-28. pm:blocked → closed completed.

    #11846 is closed / completed (2026-08-28 07:49Z, PR #12718 MERGED — "retire preview mode — the RuntimeMode 'preview' value and the whole PreviewModeConfig block"). The fork was ruled remove.

    This card's own unlock table, written 2026-08-24, decided the disposition in advance:

    ruled remove ⇒ this half is moot and this card closes on half 1 alone

    ⇒ 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 discharged Blocked-by: and flips to pm: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 notice

    packages/spec/src/kernel/context.zod.ts — RuntimeMode is now:

    export const RuntimeMode = z.enum([
      'development', // Hot-reload, verbose logging
      'production',  // Optimized, strict security
      'test',        // Mocked interfaces

    The 'preview' member is gone.

    ⚠️ Correcting my own instrument — the check I pre-recorded could never have passed

    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:

    :21  "`context.mode: 'preview'` was removed from `RuntimeMode` in @objectstack/spec 17 …"
    :52  // 'preview' was RETIRED in #11846 — see RUNTIME_MODE_PREVIEW_RETIRED above.
    :59  error: (issue) => (issue.input === 'preview' ? RUNTIME_MODE_PREVIEW_RETIRED : undefined),
    

    ⭐ :59 is the load-bearing one: an author who still writes mode: '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 no security service, /meta/* reads are either refused outright or served unfiltered — never a permission-filtered artifact. Anonymous is 401 with the read never running; two distinct principals get byte-identical bodies with no mask running; /meta/types is 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.

    ⚠️ NOT closed by this, and still owed by a seat that can see objectstack-ai/objectui — out of this session's repo scope throughout, so it stays NOT MEASURED rather than guessed:

    The framework reports an absent auth slot as { enabled: false, status: 'unavailable' } with handlerReady ABSENT — not false. No framework code ever puts a handlerReady:false occupant in the auth slot. ⇒ The literal handlerReady: false that objectui#5828 keys on is a shape the framework does not produce.

    If objectui keys on status !== 'available' / enabled === false too, half 1 closes #5828's framework half outright; if it keys strictly on handlerReady === 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

  7. removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedsecurity

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions