Skip to content

[finding] RestServer's environment-scoped ?layers=true answers a Link that names the literal route template (/environments/:environmentId/…/layers), not the request path #20508

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site: packages/rest/src/rest-server.ts, where the ?layers=true deprecation Link is built from metaPath (or, after PR #20505, the shared metaItemLayersDeprecationHeaders in packages/rest/src/meta-item-read-gate.ts). Finding class (a). reach: was measured once at a public door by the #20478 dev: RestServer's environment-scoped route handler, with enableProjectScoping on.

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.

What happens

GET /api/v1/environments/env_1/meta/view/lead_all?layers=true answers the Deprecation header and a successor-version Link naming /api/v1/environments/:environmentId/meta/view/lead_all/layers, the literal route template. It should name /api/v1/environments/env_1/meta/view/lead_all/layers. A client that follows the Link requests a path with a literal :environmentId in it. The runtime dispatcher builds its Link from the request URL and is not affected. The unscoped mount is not affected either, because its template has no parameter.

Duplicate check

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

Query terms for later deduplication: layers Link successor-version environmentId template, scoped meta deprecation link, rest-server metaPath Link.

Activity

  1. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: the API a customer can call — a deprecation Link points somewhere real | 缺项 (the environment-scoped ?layers=true Link names the route template) | P3

    Triage: first grade — bug · priority:p3 · domain:cli · area:api · pm:queue. Direction: build the Link from the request's own path

    Triage: lands in packages/rest/src/rest-server.ts. The call at about :6243 passes ${metaPath}/${req.params.type}/${req.params.name} to metaItemLayersDeprecationHeaders (meta-item-read-gate.ts:2485), and on the scoped mount metaPath still holds :environmentId ⇒ domain:cli.

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

    Why p3. A client that follows the successor Link on the environment-scoped mount requests a path with a literal :environmentId in it. The deprecated spelling itself still answers correctly, the unscoped mount and the dispatcher are unaffected, and only a client that follows the header is misled.

    Direction.

    • RestServer passes the resolved request path, as the dispatcher does, so both transports build the Link the same way. ⛔ No string replace of the template in the helper.
    • Pins: the scoped ?layers=true answer's Link names /api/v1/environments/env_1/meta/view/lead_all/layers, and the unscoped control is unchanged.

    Not serial. PR #20505 has merged.

  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:p3, the lane's only queued card, in the slot #20516's dev freed
    Session: local_1d2a197c-c20e-4e90-9be8-413d4d432289
    Account: hotlong
    Branch: claude/issue-20508-scoped-layers-link
    Worktree: objectstack-issue-20508
    Domain: domain:cli
    Seat: domain:cli#1
    File surface:

    • packages/rest/src/rest-server.ts: the ?layers=true call site of metaItemLayersDeprecationHeaders only (about :6243 to :6300 on origin/main), which now passes the resolved request path;
    • tests in packages/rest/src/ (the scoped and unscoped Link pins);
    • one .changeset/20508-*.md (@objectstack/rest patch).

    Read-only: packages/rest/src/meta-item-read-gate.ts (⛔ no template replace in the helper), packages/runtime/**, packages/spec/**. Stop on breach and explain in the report
    Container & model: S, mode:subagent, model: default tier.
    Clause-②: no
    Thread-read: 5879492075
    Serial constraints cleared: The file lists of all 12 open PRs were read in this act. No open PR touches rest-server.ts (PR #20518 and PR #20512, which did, have merged); merge main on conflict. PR #20527 (#20507, this seat, in review) touches meta-item-read-gate.ts, not this call site

    Taken 2026-09-28T23:25Z, origin/main 397572ed5. Executes triage's grade (5879492075): RestServer builds the Link from the resolved request path, as the dispatcher does, and ⛔ not by a string replace of the template in the helper. Clause-②: no: a header value is corrected to the path the request actually used.

  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20508,
    "status": "done",
    "branch": "claude/issue-20508-scoped-layers-link",
    "pr": "#20528",
    "session": "session_local_1d2a197c-c20e-4e90-9be8-413d4d432289",
    "premise_still_valid": true,
    "summary": "RestServer's ?layers=true call site now passes the request's own path (IHttpRequest.path) to metaItemLayersDeprecationHeaders, parsed as a URL path the way the runtime dispatcher's requestedItemPath reads its request URL (without the trailing slash). It no longer passes metaPath, which on the environment-scoped mount is the route template. The shared helper in meta-item-read-gate.ts is untouched, and nothing replaces the template string. The scoped Link now names /api/v1/environments/env_1/meta/view/lead_all/layers, and the unscoped control is byte-identical. H0 is confirmed. H1 is half confirmed: req.path is available, but the Hono adapter's c.req.path is decodeURI'd (lead%20all arrives as lead all), so the parse re-encodes it, and 6 of 7 probes then match the dispatcher byte for byte. The seventh, %5F, becomes _, which is equivalent under RFC 3986 6.2.2.2. For H2 the red set was larger than predicted: 3 pins went red, not only the scoped one. Changeset: @objectstack/rest patch, Clause-② no.",
    "tests": "H0 scratch probe on fb38607 (unfixed), real HonoHttpServer, enableProjectScoping optional: the scoped GET /api/v1/environments/env_1/meta/view/lead_all?layers=true answered 200 deprecation=true with a Link naming /api/v1/environments/:environmentId/meta/view/lead_all/layers; the unscoped control named /api/v1/meta/view/lead_all/layers. The same probe found the unscoped Link broken for encoded names: lead%2Fall named lead/all, lead%25all named lead%all, and lead%20all put a raw space in the header. H1: a probe route read req.path for 7 names (table in the PR body). Pins at 439dd81: meta-item-layers-deprecation-link.test.ts (new, Hono) plus meta-item-layered-route.test.ts gave 2 files, 16 passed. H2 ablation via scripts/ablation-replace.mjs put back the template argument (anchor x1 to x0, blob cb57de8250e2 to 004a11651dba): 3 failed and 13 passed. The reds were the scoped pin (Received: /api/v1/environments/:environmentId/meta/view/lead_all/layers), the scoped encoded-name pin, and the mock no-path pin; both unscoped controls stayed green. Restore: blob cb57de8250e2 equals HEAD, and git diff HEAD was empty. The parse ablation (raw req.path, unique marker) gave 1 failed and 15 passed; the one red was the encoded-name pin (Received: lead all/layers), restored the same way. No dist was involved: the subject is imported relatively from src. Full suite at 439dd81: pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 gave 222 files, 4235 passed, 40 skipped, VERDICT command-exit 0. pnpm --filter @objectstack/rest typecheck: exit 0, with check:test-typecheck OK (0 errors held); tsc -p tsconfig.test.json --listFiles counts both edited test files. All runs were UNLOCKED (no flock on this host), declared verbatim in the PR body.",
    "mcp_calls": "0",
    "api_writes": "3 - POST /repos/objectstack-ai/objectstack/pulls (draft PR 20528, via with-fleet gh pr create; body read back identical apart from a trailing newline); POST /repos//issues/20528/assignees (label-write, hotlong, HTTP 201, read back MATCHES); POST /repos//issues/20508/comments (this os-dev-report, via post-stamped). Plus 2 git pushes (the empty branch probe and 439dd81; not REST).",
    "gates": "At 439dd81 (origin/main fb38607; git merge origin/main reported Already up to date). dispatch-gates --repo objectstack-ai/objectstack --commands derived 62 commands: 60 exit 0, and 2 exit 3 PREREQUISITE NOT MET. Those two are check:dual-build-cjs-loads (42 packages without dist) and check:type-check-debt (5 ledgered dependencies unbuilt), both NOT MEASURED: the whole-workspace build was not run on this shared host. The --ran reconciliation, with exit codes recorded, read: 62 derived, 60 run, 2 NOT-MEASURED (DERIVED from a recorded exit 3), 0 UNRUN. Roster gates under changed directories: check-changeset-fixed, check:error-code-casing and check:filter-alias-parity all exit 0. pnpm lint (full repo, not narrowed): exit 0 in 27s. node scripts/check-issue-citations.mjs --base origin/main: exit 0, 1 citation resolves. check:nul-bytes: exit 0. CI at report time: 31 check runs, the required gates in_progress, and the card-claims-branch check success.",
    "line_budget": "4 files, +170 / -3 (173 changed lines) vs origin/main three-dot, under the 5000 human-merge threshold; no skills/** and no governed surface touched.",
    "files_changed": [
    ".changeset/20508-scoped-layers-link.md",
    "packages/rest/src/meta-item-layered-route.test.ts",
    "packages/rest/src/meta-item-layers-deprecation-link.test.ts",
    "packages/rest/src/rest-server.ts"
    ],
    "deviations": [
    "H2 predicted that exactly the scoped pin would go red. 3 pins went red. The two extra reds are pins this PR adds (the scoped encoded-name case and the mock no-path case), and each is a case the template argument gets wrong. Both unscoped controls stayed green.",
    "The first parse ablation was a no-op. ablation-replace refused because the replacement text requestPath already occurred inside the anchor, so the count did not rise; it restored without running the tests. It was re-run with a unique marker, and the reading is from that re-run.",
    "Two one-run scratch probe test files (the H0 reading and the H1 req.path probe) were written in packages/rest/src, because vitest resolves the workspace imports only there. Both were deleted right after their runs and never committed; git status was clean afterwards.",
    "The existing mock harness in meta-item-layered-route.test.ts was edited so its unscoped Link pin passes the path field that IHttpRequest declares required. The assertion text is unchanged.",
    "label-write was first run without a token (exit 3, PREREQUISITE NOT MET, nothing read or written). It was re-run with the token exported by fleet-token.mjs --export, not under with-fleet, because label-write paces its own writes.",
    "Commit trailers use the AGENTS.md model-free pair, not the harness reminder's model-named Co-Authored-By.",
    "The worktree was removed (node_modules deleted, git worktree remove without --force, exit 0) before this comment was posted. post-stamped was run from the shared checkout's scripts/pm, read-only; its pace state lives under ~/.cache.",
    "Verification ran UNLOCKED (no flock on this host); the disclosure is pasted verbatim in the PR body."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · the request-path parse now exists twice, in the runtime dispatcher's requestedItemPath (packages/runtime/src/domains/meta.ts) and inline at RestServer's call site. Moving it into the shared seam meta-item-read-gate.ts is outside this claim's file surface (read-only here; PR 20527 is editing it) · noted in the PR's Acceptance notes only",
    "carrier: 承接者:无 · IHttpRequest.path is spelled differently by the two IHttpServer implementations: the Hono adapter's c.req.path is decodeURI'd (lead%20all arrives as lead all), while the Node conformance port's url.pathname is raw. The contract says only 'Request path', so no declared text is violated. Measured and tabled in the PR body's H1 section; this PR's parse makes the Link independent of it · noted, not filed"
    ]
    }

  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20528 at 439dd81b, 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:56Z

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

    Checklist:

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

    • Scope: 4 files, all inside claim 5880574313:

      • the ?layers=true call site in rest-server.ts, which now passes the request's own path, parsed as the dispatcher's requestedItemPath parses it;
      • a new Hono-driven pin;
      • one existing test, whose assertion is unchanged;
      • one changeset (@objectstack/rest patch).

      meta-item-read-gate.ts is untouched, and there is no template string replace. check-governed-merges --pr 20528: NOT governed; +170 −3.

    • H0 measured on main: the scoped ?layers=true Link named /api/v1/environments/:environmentId/meta/view/lead_all/layers. The same probe found the unscoped Link mis-pointed for percent-encoded names: lead%2Fall named lead/all, lead%25all named lead%all, and lead%20all put a raw space in the header.

    • 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:

      • The scoped Link names the request's own path, and the unscoped control is byte-identical for a plain name.
      • The encoded-name change on the unscoped mount is a fix toward the dispatcher's answer, not a new divergence. %5F → _ is URI-equivalent under RFC 3986 and was already so before.
      • The seam is untouched.
      • The edited test keeps its assertion.

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

    • Evidence:

      • Ablation back to the template argument reddens the scoped pin and the two pins this PR adds for cases the template gets wrong. Both unscoped controls stay green, and the restore was proven.
      • @objectstack/rest 222 files / 4235 tests pass.

    Out-of-scope findings, one line each:

    • The request-path parse now exists twice: the dispatcher's requestedItemPath, and inline at RestServer's call site. Acceptance notes: the seam was read-only on this card while PR fix(rest): the layered view answers an absent name with the plain read's 404, as it answers an unpublished app (#20507) #20527 edited it. Folding the two into one seam function is a candidate for the next card that touches the layered chain.
    • IHttpRequest.path is decoded by the Hono adapter and raw in the Node conformance port, and the contract says only 「Request path」. Acceptance notes: no declared text is violated, and this PR's parse makes the Link independent of the difference.

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

  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20528 → 1378ec7c0c

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

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

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:clipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions