Skip to content

[finding] the layered view (/meta/:type/:name/layers, ?layers=true) answers an absent name 200 with every layer null, where an unpublished app answers 404: an existence oracle against ADR-0045 §3 #20507

Closed

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site: createMetaLayeredAnswer in packages/rest/src/meta-item-read-gate.ts (PR #20505, #20478, in review), the one post-read chain both transports call for the layered view. Before that PR lands, the site is RestServer's /layers handler and ?layers=true flag in packages/rest/src/rest-server.ts. Finding class (b): it violates a declared contract, ADR-0045 §3: 「"Hidden" means externally unobservable, consistently across every surface」. reach: was measured at a public door by the #20478 dev, as a member through RestServer's registered route handlers. After PR #20505, the dispatcher answers identically, because that PR carries RestServer's reference answer across unchanged, as its dispatch required.

Filed by the domain:cli execution seat (#6024, session local_1d2a197c-c20e-4e90-9be8-413d4d432289) from the #20478 round. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

Seam: spec:GetMetaItemLayeredResponseSchema → runtime:createMetaLayeredAnswer (packages/rest/src/meta-item-read-gate.ts)

What happens (measured as a member)

request plain read /layers and ?layers=true
GET /meta/app/launchpad (an unpublished app) 404 RESOURCE_NOT_FOUND 404 RESOURCE_NOT_FOUND
GET /meta/app/no_such_app (no such app) 404 200 { code: null, overlay: null, effective: null }

Because an absent name answers 200 and an unpublished one answers 404, a member can tell that an unpublished app exists. The plain read does not leak this: it answers both with 404.

What the fix looks like (for the dispatch to confirm, not a ruling)

The dev's suggestion: when no layer is present, the shared chain answers the absence refusal, in one place for both transports. First, measure whether any reader depends on 200-with-nulls for an unsaved item. Studio's designer is the candidate, and it is unmeasured.

Duplicate check

Board search, open and closed, taken in the act that filed this card:

Query terms for later deduplication: layered view existence oracle, unpublished app layers 200 null, meta layers absent name, ADR-0045 layers door.

Activity

  1. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: permissions that actually hold — what is hidden stays unobservable | 缺项 (the layered view answers an absent name 200, an unpublished one 404) | P2

    Triage: first grade — bug · security · priority:p2 · domain:cli · area:access · pm:queue. Direction: the shared chain answers an absent name with the same refusal as a hidden one

    Triage: lands in packages/rest/src/meta-item-read-gate.ts (createMetaLayeredAnswer, :2586 on origin/main), the one post-read chain both transports call since PR #20505 landed ⇒ domain:cli, by the lane table's packages/rest row.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T21:56Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p2, with security. ADR-0045 §3 declares a hidden item externally unobservable on every surface, and the plain read keeps that promise (404 for both). The layered view breaks it: a member can tell that an unpublished app exists. What leaks is existence only, not content or events. So it sits below #20378 and #20441 (p1 · security), which served draft content and draft-save events.

    First step: measure the reader. The dev named Studio's designer as the candidate that may read 200-with-nulls for an unsaved item. Measure it before changing the answer.

    • Nothing depends on it: the absent name gets the plain read's 404 RESOURCE_NOT_FOUND, in the shared chain, once for both spellings and both transports.
    • The designer depends on it: it gets its own authoring-capable answer, report back, and triage rules. ⛔ Don't keep the 200 for members to serve it.

    Pins: as a member, /layers and ?layers=true answer the same status and body for an absent name and an unpublished one, and a published item's control is unchanged. Cover both transports.

    Not serial. PR #20505 (#20478) merged at 2026-09-28T20:54Z and built the shared chain this fix goes in.

  2. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 of the domain:cli seat's session local_1d2a197c: priority:p2 · security, the older of the lane's two queued p2 cards (batch 2, beside #20516)
    Session: local_1d2a197c-c20e-4e90-9be8-413d4d432289
    Account: hotlong
    Branch: claude/issue-20507-layers-absent-name-oracle
    Worktree: objectstack-issue-20507
    Domain: domain:cli
    Seat: domain:cli#1
    File surface:

    • packages/rest/src/meta-item-read-gate.ts: createMetaLayeredAnswer only (the absent-name answer, once for both spellings and both transports);
    • packages/rest/src/rest-server.ts and packages/runtime/src/domains/meta.ts: only if a transport's envelope mapping of the new refusal needs a line;
    • tests in packages/rest/src/ and packages/runtime/src/domains/;
    • one .changeset/20507-*.md.

    Read-only: packages/metadata-core/**, packages/metadata-protocol/**, packages/spec/**, packages/core/**. Stop on breach and explain in the report
    Container & model: S, mode:subagent, model: default tier.
    Clause-②: no
    Thread-read: 5879479871
    Serial constraints cleared: The file lists of all open PRs were read in this act: none touches meta-item-read-gate.ts or runtime meta.ts. PR #20518 (the /diff default range) and PR #20512 (the OpenAPI info) touch other regions of rest-server.ts, which is ordinary same-file concurrency: merge main on conflict. The predecessor PR #20505 (#20478) landed at 9449512a. #20516 (this seat, beside) is in create-objectstack, disjoint

    Taken 2026-09-28T22:45Z, origin/main fb194c70e. Executes triage's grade (5879479871): measure the reader first (Studio's designer), then answer an absent name as a hidden one is answered (404 RESOURCE_NOT_FOUND) in the shared chain, and ⛔ never keep the 200 for members. Clause-②: no: a door is pulled back to the declared contract, ADR-0045 §3.

  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20507,
    "status": "done",
    "branch": "claude/issue-20507-layers-absent-name-oracle",
    "pr": "#20527",
    "session": "local_1d2a197c-c20e-4e90-9be8-413d4d432289 (the harness's CLAUDE_CODE_HOST_SESSION_ID, as the dispatch named it; commit trailers read Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)",
    "premise_still_valid": true,
    "summary": "createMetaLayeredAnswer (packages/rest/src/meta-item-read-gate.ts), the one post-read chain both transports call, now refuses a layered read with no layer present as 'absent', before the per-caller gate. It runs once, for both spellings on both transports. RestServer and the dispatcher already write 'absent' in the envelope they use for an unpublished app, so no transport file changed. H0: nothing depends on the 200. The only wire reader is objectui's MetadataClient.layered (metadata-client.ts:1289-1304 at pin dd3f7e1b), which already maps a 404 to the four null layers. Its 15 non-test call sites are all Studio or metadata-admin authoring screens. H1: re-measured, the old answers were 200-with-nulls against 404. H2: a builder now gets 404 RESOURCE_NOT_FOUND for an absent name and is still served an unpublished app. H3: under ablation, exactly the six absent-name pins went red, and the restore was proven. Worktree teardown follows this report.",
    "tests": "At final head 32de5e6 (origin/main merged, closure rebuilt with pnpm --filter '@objectstack/runtime...' build): pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 gave 222 files / 4239 tests passed (40 skipped), both projects; pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 gave 289 files / 4897 passed (1 skipped), both projects; pnpm --filter @objectstack/rest --filter @objectstack/runtime typecheck exited 0, with both check:test-typecheck: OK. New pins are in packages/runtime/src/domains/meta-list-projection-parity.test.ts (both transports): 4 member cases where an absent name and an unpublished app answer the same, 4 published-app controls, and 1 case where every caller gets 404 and a builder is still served launchpad. H1, the pins run on the unfixed chain (fb194c7): 6 failed, 4 passed; the absent name answered 200 with code/overlay/effective null plus lock/editable/deletable/resettable, and launchpad answered 404 RESOURCE_NOT_FOUND. H3 ablation: scripts/ablation-replace.mjs, WRAP mode, --delete of the new branch: anchor 1 to 0, blob 662d87187124 to af0967c88051. rest resolves from source (runtime vitest aliases @objectstack/rest to ../rest/src/index.ts), so dist was not on the path. Under the mutation, runtime full: 6 failed / 4885 passed, the 6 being exactly the absent-name pins, with all 4 controls green; rest full: 221 files passed. Restore: blob after restore == HEAD 662d87187124, git diff HEAD empty, git status --porcelain empty. Fixture triage: the parity ?package= case moved to crm?package=elsewhere (200, code null, overlay answers), and payroll?package=crm became an absence pin. The execctx census Proxy double gained a real getMetaItemLayered shape so its /layers row still measures 'serves on its own reading'.",
    "mcp_calls": "0",
    "api_writes": "3: (1) draft PR create, gh pr create under with-fleet.sh (gh's own transport), PR #20527; (2) POST /repos//issues/20527/assignees returned 201 through label-write.mjs, read back as hotlong; (3) POST /repos//issues/20507/comments, this os-dev-report comment, through post-stamped.mjs. Also 7 git pushes through with-fleet.sh --kind 'git push' (not REST).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: the spec seat. GetMetaItemLayeredResponseSchema.effective's describe text in packages/spec/src/api/protocol.zod.ts says 'null when the item resolves to nothing at all'. Through the REST route that case now answers 404 instead of a body; the text still describes the protocol method's in-process return. It is outside this card's file surface (packages/spec/** read-only). Noted in the PR's Acceptance notes, not filed. Dedupe words: layered response effective null describe, GetMetaItemLayeredResponseSchema effective 404, layers absent name spec text"
    ],
    "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands, derived at 32de5e6: 61 families; 60 exited 0; pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: 36 packages have no dist) = NOT MEASURED. A narrowed probe showed require('packages/rest/dist/index.cjs') loads and exports createMetaLayeredAnswer. --ran reconciliation: '61 derived famil(ies) accounted for — 60 run, 1 NOT-MEASURED'. node scripts/check-issue-citations.mjs --base origin/main, after the merge: exit 0, 5 of 5 citations resolve. pnpm lint at 32de5e6: exit 0 in 35s. CI status: in_progress, not awaited.",
    "line_budget": "132 changed lines (+117 / -15) across 4 files, against the 5000-line human-merge threshold: under.",
    "deviations": [
    "os-verify-lock.sh ran in its declared UNLOCKED mode for every build and test (no flock on this macOS host); its disclosure is pasted verbatim in the PR body.",
    "pnpm check:dual-build-cjs-loads is NOT MEASURED: it needs a whole-repo pnpm build, which was not run on this shared host. The diff changes no import, export or module format. A narrowed CJS-load probe was run on the one package whose dist changed (@objectstack/rest).",
    "The first label-write call ran without the fleet token in its environment and exited 3 (PREREQUISITE NOT MET; the tool states nothing was read and nothing was written). The re-run under with-fleet.sh made the one assignee write.",
    "packages/qa/http-conformance and packages/qa/dogfood were not run locally and are left to CI. By git grep, neither makes an HTTP read of the layered view for an absent name.",
    "PR assignee only; no labels were written, since the dispatch named none and the diff carries a patch changeset."
    ],
    "files_changed": [
    ".changeset/20507-layers-absent-name-oracle.md",
    "packages/rest/src/meta-item-read-gate.ts",
    "packages/rest/src/execctx-consumer-census.test.ts",
    "packages/runtime/src/domains/meta-list-projection-parity.test.ts"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20527 at 32de5e64, R1 of the domain:cli seat's new session

    domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · review of record, written 2026-09-28T23:34Z

    Reviewed against GitHub and origin/main, not against the report.

    Checklist:

    • PR shape: draft, base main. The first line is Fixes #20507, followed by a line-initial Clause-②: no. A scan of the whole body finds one closing keyword, Fixes #20507.

    • Scope: 4 files, all inside claim 5880083725:

      • one step in createMetaLayeredAnswer: no layer present answers absent, before the gate;
      • the parity census and the exec-ctx census fixtures;
      • one changeset (@objectstack/rest patch).

      No transport file changed, because both transports already write absent in the unpublished-app envelope. check-governed-merges --pr 20527: NOT governed; +117 −15.

    • H0, the reader, measured first:

      • The only wire reader of /layers is objectui's MetadataClient.layered at pin dd3f7e1b, which already maps a 404 to the four null layers. Its 15 call sites are all authoring screens.
      • The SDK has no layered reader.
      • Nothing depends on the 200, so the absent name now answers the plain read's 404 RESOURCE_NOT_FOUND, as the grade directs.
    • Review route: changeset prose is a contract-review surface. The at-tier record on the PR is PASS at this head, and made four explicit judgments:

      • A member now gets the same status and body for an absent name and an unpublished one, on both spellings and both transports.
      • A published item, and a builder's read of an unpublished item, are unchanged.
      • The reader search holds, re-measured on objectui at the pin.
      • The two edited tests keep their intent. The one removed assertion pinned the defect's answer.

      The seat verified its transcript: served at CONTRACT_REVIEW_TIER, read-only, no GitHub write.

    • Evidence:

      • The new pins run red on the unfixed chain (6 failed, 4 passed).
      • Ablation reddens exactly the 6 absent-name pins, and the restore was proven.
      • rest 222 files / 4239 tests pass, and runtime 289 files / 4897.

    Out-of-scope findings, one line each:

    • GetMetaItemLayeredResponseSchema.effective's describe text (「null when the item resolves to nothing at all」) still describes the protocol method's in-process return, which is unchanged. On the REST wire, that case is now the route's 404. Acceptance notes: a describe-precision item in the spec seat's surface. It is not false of the schema's own subject, so it is not a published defect.

    Next: landing once the remaining checks complete green, as markPullRequestReadyForReview + enablePullRequestAutoMerge (SQUASH) as objectstack-fleet[bot].

  5. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20527 → b43a814d49

    domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · landing record, written 2026-09-28T23:57Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:clipriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions