Repository navigation
rest: the API docs viewer page loads a third-party script by an unpinned URL with no integrity check #25104
Description
Filing gate ①, security exception ("may leak data": measure reach first). Filed by domain:cli seat 2 (seat post #22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6, on the maintainer's word in that session's chat, verbatim 「1 2 同意」. Item 2 asked whether to file this reading as its own card, held to the disclosure ruling and placed in packages/rest, which is this lane. ⛔ Not a claim.
Disclosure: the maintainer's ruling on #9387 (5323310017) applies. The card records existence, location and governing text, and reach as an abstract statement only.
Reader, and what they do: domain:cli seat 2 dispatches it after PR #25093 (#25077) lands, because both touch the same file. The dev measures first.
Where it lives (origin/main 8f59304ddc)
- The location:
packages/rest/src/discovery-routes.ts, theGET {base}/docsviewer page. It is the only place inpackages/**source that loads a script from a public CDN. - The reading: the page loads the viewer's script from a public CDN URL that names no version, and the script element carries no
integrityattribute. Every page view runs whatever release the CDN serves for that package at that moment, in the API's origin. This is a source reading. Reach is NOT measured. - Read while reviewing PR fix(rest): escape the API docs viewer URL value for its HTML attribute context (ruling 3C) #25093, and held out of that PR's scope.
Governing text
docs/HARDENING.md § Security response headers: every dispatcher response carries Content-Security-Policy: default-src 'none'; frame-ancestors 'none'. Verbatim: "These defaults assume the dispatcher is serving APIs, not HTML. If the same host also serves a SPA, configure a less restrictive CSP for the HTML route (or — better — serve the SPA from a different origin and let the API stay locked down)." The docs page is an HTML route on the API host. That section does not name it.
Measure first
- Does the page work as served? Does the served page carry the dispatcher's CSP? If it does, the external script is blocked and the page renders nothing. If it does not, the script runs in the API origin. Measure both route shapes through each mounted host.
- Who views it. rest: one server-rendered response may not escape a value by the context it lands in (reach not measured; detail withheld under the disclosure ruling) #25077's measurement found the page needs an authenticated request. Confirm that.
- Who needs it. Name any consumer of
GET {base}/docsin this repo, objectui, the docs site or the examples. Under the four-axis frame, a page nobody uses is a retirement candidate, not a hardening one.
Options the dev reports on, with measurements (the maintainer chooses if they differ in cost)
- pin an exact version and add
integrity+crossorigin; - serve the viewer's script from this package or the host instead of a public CDN;
- retire the page if step 3 finds no consumer.
Dedupe
Two MCP search_issues queries returned 0 and 0 hits. A positive control on the same route returned 3 closed cards, none of them this. New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 21, 2026
Claim: PM loop round 22 (serial) ·
domain:cliseat 2 · p2 ·security· measure first, filed by this seat on the maintainer's 「1 2 同意」 · 2026-10-21T12:00ZSession:
session_01LmjU2x1faLZ39DVTFyVca6Account:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-25104-docs-viewer-scriptWorktree:
objectstack-issue-25104Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)Disclosure: the maintainer's ruling on #9387 (
5323310017) applies. This claim, the PR and every comment state existence, location, governing text, and reach as an abstract statement only.File surface (read on
origin/mainba244f1b48; line numbers move; stop on a breach and explain it in the report):GET {base}/docspage carry the dispatcher's CSP, and does it work through each mounted host, on both route shapes;packages/rest/src/discovery-routes.ts(about:681–:712). Pin an exact version of the viewer's script and addintegrity+crossorigin. Pin the served element's attributes, and add apatchchangeset for@objectstack/rest.docs/HARDENING.md§ Security response headers and their implementation); the escaping landed in PR fix(rest): escape the API docs viewer URL value for its HTML attribute context (ruling 3C) #25093 and its outlet-list pin, which stay as landed;packages/spec/**.Container & model:
S,mode:subagent,model: default tier(dispatch-gates.mjs --tieratba244f1b48: "no path-derived mandate")Clause-②: no
Basis for the Clause-② line: pinning a script's version and its integrity changes no published schema, REST data answer or accept set. The page's markup gains two attributes. A retire or self-serve outcome is not built in this round. If chosen later, it is judged on its own claim.
Responsibility: this lane's
packages/rest. No platform path covers it. Reach is unmeasured.Governing text:
docs/HARDENING.md§ Security response headers. These defaults assume the dispatcher serves APIs, not HTML, and an HTML route on the API host needs its own CSP.Thread-read: none
Serial constraints cleared:
packages/rest/src/discovery-routes.tsorhtml-*. All 10 open PRs were read in this act. PR fix(rest): escape the API docs viewer URL value for its HTML attribute context (ruling 3C) #25093 (rest: one server-rendered response may not escape a value by the context it lands in (reach not measured; detail withheld under the disclosure ruling) #25077) landed as4f97aeb5c.objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{
"issue": 25104,
"status": "needs_decision",
"branch": "claude/issue-25104-docs-viewer-script",
"pr": null,
"session": "session_01LmjU2x1faLZ39DVTFyVca6",
"premise_still_valid": true,
"summary": "Measured first and built nothing, following the dispatch's otherwise-branch: the page works as served, but no consumer exists. On the shipped host (plugin-hono-server, composition mirroring serve.ts) and on the node IHttpServer, GET {base}/docs answers 401 to an anonymous caller and 200 text/html to a signed-in caller on both the unscoped and the environment-scoped route. It carries no Content-Security-Policy and none of the dispatcher's other default security headers, while the dispatcher's own /health on the same servers carries all six. In Chromium the page renders the API reference (the CDN script was supplied from the registry-verified 1.73.0 file because the CDN host is a policy denial here), so nothing blocks the script. Through @objectstack/hono's createHonoApp the route is 404. No consumer exists in this repo (docs site, examples, apps, skills, CLI, client) or in objectui origin/main 7e9708e63. Every mention is a route ledger, the route's own tests, a QA checklist row, changelog prose or spec describe text. The card's premise holds: the script element still names no version and carries no integrity at ba244f1, and the file is byte-identical at origin/main 03c12a5. The three options go to the maintainer below, with measurements under the four axes.",
"tests": "All measurement runs used os-verify-lock.sh; each exit is the VERDICT line. (1) Build: turbo run build --filter=@objectstack/runtime... --concurrency=2, VERDICT command-exit 0, 33/33 tasks. (2) Headers: pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 on a scratch file (deleted, never committed), VERDICT command-exit 0, 4/4. Rows on plugin-hono-server (real better-auth, signed in as the dev-seeded admin; REST and dispatcher with api scoping enableProjectScoping true / projectResolution auto, as serve.ts forwards it; dispatcher securityHeaders at its default): /api/v1/docs anonymous 401, signed-in 200 text/html. /api/v1/environments/env_measure/docs anonymous 401, signed-in 200 text/html. The header set on both 200s is access-control-allow-credentials, access-control-expose-headers, connection, content-length, content-type, date, keep-alive, vary. Absent: content-security-policy, x-frame-options, x-content-type-options, referrer-policy, cross-origin-resource-policy, permissions-policy. /api/v1/openapi.json signed-in 200, no CSP. CONTROL /api/v1/health 200 with content-security-policy default-src 'none'; frame-ancestors 'none' plus the other five. Node IHttpServer (private @objectstack/http-conformance, stub auth): the same pattern. Docs 401/200 text/html on both routes; header set connection, content-length, content-type, date, keep-alive; the /health control carries the CSP. @objectstack/hono createHonoApp over the same kernel: docs on both routes and openapi.json answer 404 ROUTE_NOT_FOUND; its /health carried no CSP. Browser (Chromium 1194, session cookie set; the CDN request was fulfilled from the 1.73.0 dist/browser/standalone.js, every other external request recorded and aborted): status 200, title 'ObjectStack API Docs', heading 'ObjectStack REST API', 105 viewer elements, 3324 chars of text. External requests on load: the CDN script, plus two font files from a second vendor origin (aborted). (3) Retirement cost: node scripts/ablation-replace.mjs --delete of the 51-line /docs registration block in packages/rest/src/discovery-routes.ts (anchor 1 to 0, blob 2b1c7eb3fb2a to 55d7d73908fc), then the whole @objectstack/rest local project (6511 tests). Result: 18 failed in 6 files, VERDICT command-exit 1, the expected red. The tool's restore proved blob == HEAD 2b1c7eb3fb2a with git diff HEAD empty, re-checked independently. Control leg: the same 6 files on the restored tree pass 6/6 files and 89/89 tests, VERDICT command-exit 0. (4) Census, objectstack ba244f1: git grep for api/vN/docs, a base variable followed by /docs, and the viewer package spellings over content/docs, examples, apps, skills and packages (exit 0) finds no consumer. Positive control: /api/v1/meta hits content/docs (exit 0). objectui origin/main 7e9708e63 (shallow clone; tip-tree grep): 14 hits on the exact spellings, none a consumer (console documentation-portal links and the spec package's openapi.json export subpath). Positive control: api/v1/meta hits apps/console (exit 0). (5) npm registry: @scalar/api-reference dist-tags.latest is 1.75.1, published 2026-10-20, with 12 stable releases in the last 30 days. The 1.73.0 tarball's sha512 matches the registry integrity. (6) Churn: tree diff ba244f1..origin/main 03c12a5 over the measured files is empty (exit 0); control: the whole-tree diff over the same range shows 6 files changed.",
"gates": "None run: no code change, and the dispatch scopes the gate list to a code change. There is no PR, so there are no check-runs to cite. The verify-lock verdicts of the measurement runs are under tests.",
"mcp_calls": "0",
"api_writes": "1: POST /repos//issues/25104/comments (this report, through scripts/pm/post-stamped.mjs). Not REST: one git push of the empty branch (the write probe).",
"open_questions": [
{
"question": "What should happen to GET {base}/docs, the Scalar viewer page in packages/rest/src/discovery-routes.ts, given that it works as served, is authenticated-only, and has zero measured consumers?",
"options": [
"A: Give the docs route its own CSP (the card's 'adjust the route's CSP'), paired with the card's pin plus SRI. Measured: there is no CSP on the route to adjust. The dispatcher's default never reaches RestServer responses because it is applied only inside the dispatcher's own handlers, so this option ADDS a route-specific policy in packages/rest; the dispatcher's defaults and their implementation stay untouched. To constrain the script it needs an exact version in the URL, integrity sha384 of the exact served file and crossorigin. The page also fetches fonts from a second vendor origin on load, so font-src must admit that origin or the fonts fall back. Cost: small (one header, two attributes, one served-element pin, a patch changeset). The hash must come from the bytes the CDN serves, and this container cannot fetch them (cdn.jsdelivr.net is a CONNECT 403 policy denial; not retried). The npm tarball is reachable and its sha512 verified, so the hash needs either a seat with CDN reach or a maintainer-accepted equivalence between the tarball file and the CDN file. Axes. Real business need: serves zero measured consumers. Long-term soundness: HARDENING.md names this only as the fallback ('configure a less restrictive CSP for the HTML route') behind its stated better path ('serve the SPA from a different origin and let the API stay locked down'), and it keeps a frontend asset hard-coded in a backend package. Guarding against AI authoring mistakes: version and hash form a two-field invariant; bumping one without the other makes SRI refuse the script, and the page then renders nothing with no server-side signal. A pin test can hold the shape, not the hash's truth. Startup focus: small, but it spends effort hardening an already-published zero-consumer surface, which the frame says sunk cost does not exempt.",
"B: Serve the viewer's script from this package or the host. Measured: the browser bundle (1.73.0 dist/browser/standalone.js) is 4,418,213 bytes (1,278,288 gzipped); the npm package is 46,750,038 bytes unpacked, 1,294 files, 27 runtime dependencies. Vendoring the one file adds about 4.4 MB to @objectstack/rest's published tarball. Depending on the package adds the whole tree to every @objectstack/rest install. It needs a new static-asset route on the API (one more route, ledger row, anonymous-deny decision and closed query set), and still a route CSP (script-src 'self' plus the font origin) to mean anything. Upgrades become manual. Axes. Real business need: zero measured consumers. Long-term soundness: it puts a frontend bundle into a backend package (this repo ships backend only; UI lives in objectui), the opposite of HARDENING.md's 'serve the SPA from a different origin'. Guarding against AI authoring mistakes: it removes the runtime third-party fetch but adds a large vendored artefact that drifts silently. Startup focus: capability expansion (a new route and asset) with no measured pull, so it argues against.",
"C: Retire GET {base}/docs on both bases and keep /openapi.json. Measured by an ablation of the registration block over the whole @objectstack/rest local suite (6511 tests): 18 tests in 6 files fail. Every one is a pin of this route or a count that includes it: html-outlets.pin 2; rest-api-description-anonymous-deny 9 (the viewer half of those pins, including 'the viewer is described in the document'); rest-config-mount-table.pin 2 (enableOpenApi gates 2 routes, 1 after); rest-api-config-dead-keys-refused 1; rest-route-ledger.conformance 1 (the ledger row); execctx-consumer-census 3 (62 sites, 61 after). The same 6 files pass 89/89 on the restored tree. Beyond the suite, by grep: the rest-route-ledger row; packages/spec rest-server.zod.ts, where the enableOpenApi describe and the documentation.enabled tombstone prescription both name the viewer (domain:spec seat; gen:schema plus gen:docs); the ADR anchor invariant text for discovery-routes.ts; the QA checklist row in access-security.json. html-escape.ts loses its only caller. The served OpenAPI document loses one path. The outlet-list pin from PR #25093 needs rework because no outlet is left. This dispatch's hold-offs name that pin and packages/spec, so a retire ruling has to lift them. Removing a published route is judged on its own Clause-② claim and changeset. Consumers broken: none found (census under tests). Axes. Real business need: zero consumers measured in this repo, its docs, examples and objectui; the machine-readable /openapi.json, the documented surface, stays. Long-term soundness: it matches HARDENING.md's stated better path (the API origin stays locked down; human-facing docs already live on the docs site and in the objectui console), and it removes the only public-CDN script load in packages/**. Guarding against AI authoring mistakes: one fewer hard-coded third-party dependency, and one fewer surface an AI could extend or harden wrongly; the spec describe text that advertises the viewer goes with it. Startup focus: '已发布零消费能力不因沉没成本豁免' and '能力退役默认立即退休' point here."
],
"recommendation": "Not chosen here: the dispatch reserves the choice to the maintainer (deviations note the conflict with the definition's recommendation slot). The four-axis reading leans C on all four axes. A is the narrowest change if the page is to be kept, and it needs CDN reach to compute the hash."
}
],
"out_of_scope_findings": [
"observation (not one of the three classes): RestServer-served responses (openapi.json and the docs page measured; every RestServer route registers the same way) carry none of the dispatcher's default security headers on any measured host. The governing texts scope the defaults to dispatcher-routed responses (docs/HARDENING.md § Security response headers, 'Every response routed through the dispatcher plugin'; content/docs/deployment/production-readiness.mdx, 'every route the dispatcher mounts'), so no declared text is contradicted. This is this card's hold-off area. carrier: none (承接者:无) · noted, not filed",
"observation (dormant, zero pull, no reach): packages/spec/src/api/documentation.zod.ts declares ApiTestingUiConfigSchema (viewer type including 'scalar', path default '/api-docs') as ApiDocumentationConfigSchema.ui. No authorable surface or runtime reads it: git grep finds only the api-surface and declaration-map listings and one comment in http-dispatcher.ts. It is relevant to option C's sweep. carrier: none (承接者:无) · noted, not filed"
],
"deviations": [
"Composition A is an in-process mirror of serve.ts, not a spawned CLI binary: the plugin order of in-process-session-renewal.pin, serve.ts's api scoping forwarded to REST and the dispatcher, and the dispatcher's securityHeaders left at its default as serve.ts leaves it.",
"The node host (private @objectstack/http-conformance) used a stub auth service, because better-auth mounts through getRawApp, which that adapter lacks. Its first run answered 503 on the signed-in leg until the conformance harness's authz-resolver objects were registered. Every node row quoted comes from the second run.",
"Browser leg: the CDN request was fulfilled from @scalar/api-reference 1.73.0 (registry sha512 verified) because cdn.jsdelivr.net is a policy denial from this container; it was not retried or routed around. NOT MEASURED: the bytes the CDN serves today (latest is 1.75.1). NOT MEASURED: any 'Try it' interaction.",
"The retirement measurement mutated packages/rest/src/discovery-routes.ts, a hold-off file, in the worktree only, under ablation-replace's trap. Nothing was committed, and the restore is proven (blob == HEAD 2b1c7eb3fb2a, git diff HEAD empty).",
"Conflict noted rather than settled silently: the dispatch says 'Do not choose', while the os-dev definition and the four-axis frame both require a recommendation per question. The recommendation slot carries the axes' lean, labelled as not a choice.",
"A scratch measurement test was written in packages/runtime/src, run three times and deleted; it was never committed. A turbo build of the runtime closure (34 packages in scope) was run so its dist imports resolve."
],
"files_changed": [],
"line_budget": "n/a: no file changed (branch head = ba244f1, pushed empty)"
}
objectstack-fleet commentedon Oct 21, 2026
Maintainer's choice ·
domain:cliseat 2 ·os-justin· sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-21T13:45Z. ⛔ Not a new claim; the claim6315391372stands.6315947498. The chosen option is labelled 「C 下线页面」: retireGET {base}/docson both route shapes, and keepGET {base}/openapi.json.Clause-②: no (narrowing)) and owes oneCONTRACT_REVIEW_TIERreview of record.6315947498: 18 tests in 6 files of@objectstack/rest, the route ledger row, the QA checklist row, and the outlet-list pin from PR fix(rest): escape the API docs viewer URL value for its HTML attribute context (ruling 3C) #25093, which loses its only outlet.packages/specdescribe text that names the viewer isdomain:spec's. It is filed to that lane when the build round opens.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Claim amendment: PM loop round 24 (serial) ·
domain:cliseat 2 · p2 ·security· it amends6315391372on the maintainer's choice6317642533· 2026-10-21T14:02ZSession:
session_01LmjU2x1faLZ39DVTFyVca6Account:
os-justinBranch:
claude/issue-25104-docs-viewer-script(unchanged; it has no commits ahead ofmain, so the build is on currentmain)Worktree:
objectstack-issue-25104Scope of this round: retire
GET {base}/docson both route shapes, and keepGET {base}/openapi.json(the maintainer's 「C 下线页面」).File surface (read on
origin/mainf2b6cf230b; line numbers move; stop on a breach and explain it in the report):packages/rest/src/discovery-routes.ts: the/docsregistration (about:676–:725) and its comments.packages/rest/src/html-escape.ts, which loses its only caller (retire it, or say why it stays).packages/rest/src/html-outlets.pin.test.ts: the outlet list becomes empty. The census stays, so a new HTML outlet still fails until classified, and its positive control is rebuilt on a fixture.packages/rest/src/rest-route-ledger.ts, about:183).6315947498measured as naming the route:rest-api-description-anonymous-deny,rest-config-mount-table.pin,rest-api-config-dead-keys-refused,rest-route-ledger.conformance,execctx-consumer-census. Update them to the retired state; never loosen them.docs/qa/platform-checklist/areas/access-security.json.@objectstack/rest.packages/spec/**, whose text naming the viewer is filed todomain:specin this act;/openapi.jsonand its gate; the dispatcher's security-header defaults.Container & model:
M,mode:subagent,model: default tier(dispatch-gates.mjs --tieratf2b6cf230b: "no path-derived mandate")Clause-②: no (narrowing)
Basis for the Clause-② line: a published route stops being served. Nothing widens:
/openapi.json, its gate and its answer stay, except the one path the document described for the viewer. It owes oneCONTRACT_REVIEW_TIERreview of record before the queue.Serial constraints cleared:
Mode: serial. This is the seat's only dev in flight.
Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{
"issue": 25104,
"status": "done",
"branch": "claude/issue-25104-docs-viewer-script",
"pr": "#25124",
"session": "session_01LmjU2x1faLZ39DVTFyVca6",
"premise_still_valid": true,
"summary": "Retired GET {base}/docs, the OpenAPI document's HTML viewer page, on both route shapes. The reason, stated in the PR, commits and changeset: no consumer, and the API origin serves no HTML viewer. GET {base}/openapi.json, its api.enableOpenApi gate and its answer are unchanged. The retired path now gets the host's ordinary not-found answer, measured on this branch's build: 404 ENDPOINT_NOT_FOUND on plugin-hono-server and on the node IHttpServer, for both route shapes and for anonymous and signed-in callers; byte-identical to a never-served path's 404 on a booted showcase; and 404 ROUTE_NOT_FOUND on @objectstack/hono createHonoApp, unchanged, because that adapter never mounted RestServer routes. Also in the change: html-escape.ts retired with its only caller; the outlet census kept, its positive control rebuilt on a fixture; the ledger row removed; the five named pins moved to the retired state; one new not-served pin; the ADR anchor text and QA checklist row (revision 3) updated; a minor changeset for @objectstack/rest with Clause-②: no (narrowing). The fallout reached past the measured starting map into three packages/qa/dogfood files, which main's full battery would have turned red (see deviations).",
"tests": "Every run went through os-verify-lock; each result is its VERDICT line. (1) New pin, red on main: packages/rest/src/rest-docs-viewer-retired.pin.test.ts on the base tree f2b6cf2 with only the pin added (commit 5fb3bdb),
pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/rest-docs-viewer-retired.pin.test.ts: VERDICT command-exit 1, 'Tests 4 failed | 2 passed (6)'. The not-served cases failed with 'expected [ { method: GET, …(5) } ] to deeply equal []' on /api/v1/docs and on /api/v1/environments/:environmentId/docs. The not-described cases failed with "expected [ '/api/v1/docs' ] to deeply equal []". The two kept-document cases passed. (2) Green after: the seven affected rest files (the new pin, html-outlets.pin, rest-api-description-anonymous-deny, rest-config-mount-table.pin, rest-api-config-dead-keys-refused, rest-route-ledger.conformance, execctx-consumer-census) passed with VERDICT command-exit 0, 'Test Files 7 passed (7), Tests 89 passed (89)'. (3) Whole @objectstack/rest local project at b55b8f7,vitest run --project local --maxWorkers=2: VERDICT command-exit 0, 'Test Files 312 passed (312), Tests 6185 passed | 326 skipped (6511)'. (4)pnpm --filter @objectstack/rest typecheckexit 0 (tsc --noEmit plus check:test-typecheck, 0 debt).pnpm --filter @objectstack/dogfood typecheckexit 0. --listFiles shows both programs include the edited test files. (5) dogfood on the final head 1379d30, afterturbo run build --filter='@objectstack/dogfood^...'exit 0. authz-probe-blind-spot.test.ts plus authz-conformance.test.ts (--project isolated): 108/108, VERDICT 0. Before the census and matrix edits the same run was red: 'expected 80 to be 81', 'expected 69 to be 70', 'docblock says 81 rows, the table holds 80'. showcase-anonymous-deny-surfaces.dogfood.test.ts (--project shared-showcase): 74/74, VERDICT 0. A verbose filtered re-run lists the new case passing: '[#25104] the retired viewer page is routed NOWHERE — the host's ordinary 404 for everyone, not a 401'. (6) Host measurement: a scratch test in packages/qa/http-conformance/src booted node and plugin-hono-server with REST and dispatcher scoping on, plus createHonoApp over the hono kernel. It was never committed and was deleted; rest dist was checked first with 0 viewer markers and 1 openapi.json registration. Results: /api/v1/docs and /api/v1/environments/env_measure/docs, anonymous and signed-in, gave 404 {success:false,error:{code:ENDPOINT_NOT_FOUND,message:Not found}} on node and on hono-server, the same body as /api/v1/this-route-does-not-exist. /openapi.json on both shapes answered 200 OpenAPI 3.1 when signed in and 401 UNAUTHENTICATED when anonymous. createHonoApp answered 404 ROUTE_NOT_FOUND for /docs, /openapi.json and the control alike. (7) One measurement probe, not a behaviour ablation:node scripts/ablation-replace.mjsswapped the §3 floor line of execctx-consumer-census.test.ts for a printing assertion, 'expected 51 to be -1'. Mutation landed: anchor 1 to 0, blob 56c14e0312 to 98fa59dc3a. Restore proven: blob 56c14e0312 again and git diff HEAD empty. The floor stays at 46. (8) After merging origin/main edcc8ce, which carried one packages/spec describe commit:turbo run build --filter=@objectstack/specpluscheck:generatedexit 0, 'All 14 generated artifacts are up to date'.","gates": "Final commit 1379d30.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 86 commands (exit 0). Run with them: the 10 'Declared WIDE population' families (check:init-service-contract, live-db-isolation, meta-type-normalized, optional-error-sink, resume-authority-declared, runner-env-posture, settings-bind-window, startup-registry-verdict, verify-stand-in, wildcard-fallthrough). That is 96 commands, each exit captured before any pipe. 95 exited 0 on the first pass. check:dual-build-cjs-loads exited 3 with PREREQUISITE NOT MET: 8 packages unrelated to this diff had no dist. They were built (turbo, 59/59 cache hits) and the gate re-run exited 0 ('112 published require entry point(s) across 69 package(s) load'). Reconciliation:dispatch-gates.mjs --ranover the 96 'cmd :: exit N' lines: '✓ dispatch-gates --ran: 86 derived famil(ies) accounted for — 86 run, 0 NOT-MEASURED (a DERIVED zero — all 86 recorded an exit code and none of them is 3).' The 10 WIDE families are recorded as outside the derivation.pnpm lint(eslint . --no-inline-config, whole tree) on 1379d30: exit 0. CI check-runs on PR #25124 head 1379d30 when this report was written: 25 runs. No other open PR may claim the same issue: success. Packed-tarball smoke and 18.x upgrade harness: skipped (opt-in). The other 22 were in_progress and are not cited as a verdict: Lint · ESLint, Lint · repo & tooling gates, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · scoped ×3, Type Check · workspace / source gates / debt ledger / consumer gates, Spec property liveness, Governed Surface Queue Guard, Check Changeset, Check PR Size, Check Documentation Links, Flag docs affected by code changes, the card-claims-branch, Part-of and single-writer-path checks, filter. NOT MEASURED locally, CI-only: Test Core, Build Core, test:repo. NOT MEASURED, main battery only: Dogfood Regression Gate, Dogfood Verify CLI, Temporal Conformance.","mcp_calls": "0",
"api_writes": "5 REST writes. Session-side: 2 × POST /repos/objectstack-ai/objectstack/dispatches (the fleet-write relay triggers, one for pr_create and one for this comment). Executed by the relay as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/pulls (draft #25124); POST /repos//issues/25124/assignees (os-justin), read back as stored; POST /repos//issues/25104/comments (this report). Not REST: 7 fast-forward git pushes of the branch, from ba244f1 to 1379d30, with no force. All GitHub reads went through gh api as the session.",
"open_questions": [],
"out_of_scope_findings": [
"observation, not one of the three classes: more packages/spec text names the retired viewer, beyond #25121's describe text. The sites at origin/main edcc8ce: liveness/rest_api.json:73 (evidence 'mounts both /openapi.json and the /docs viewer'); scripts/lib/openapi-self-consistency.ts:34 (a comment) and :156 (a refusal message naming 'the Scalar viewer at GET /api/v1/docs'); migrations/entries/semantic/18.rest-api-config-dead-keys-retired.ts:20 and :46 (prose). Same family as #25121, and folded into it. carrier: #25121 (domain:spec) · noted, not filed · dedupe words: docs viewer, enableOpenApi describe, openapi-self-consistency, rest_api liveness",
"observation, dormant with zero pull: packages/spec/src/api/plugin-rest-api.zod.ts declares uiPath, default '/api/docs', described 'URL path to serve documentation UI', and packages/spec/REST_API_PLUGIN.md echoes it. git grep uiPath at origin/main edcc8ce finds no reader outside packages/spec; the one hit, in rest/src/ui-routes.ts, is an unrelated local of the same name. Relevant to #25121's sweep. carrier: #25121 (domain:spec) · noted, not filed · dedupe words: uiPath, documentation UI, plugin-rest-api"
],
"line_budget": "n/a: no skills/** file changed. The PR changes 687 lines (+371 / -316) in 17 files, under the 3000 human-merge threshold (dispatch-gates count).",
"deviations": [
"The file surface was breached, deliberately, and is named here for the PM to amend the claim. The claim's surface lists packages/rest, the QA row, docs and the changeset. Three test files under packages/qa/dogfood also had to move to the retired state: authz-probe-blind-spot.census.ts (ledger and registration counts), authz-conformance.matrix.ts (the API-description row and the docblock row count its pin reads), and showcase-anonymous-deny-surfaces.dogfood.test.ts (the viewer row and its 200 control). Round 22 measured fallout only in @objectstack/rest's suite. These files run in main's full battery, not on PRs, so leaving them unchanged would turn main red after the merge. They are test pins of the same class as the five named, with nothing extra: no producer or consumer changed outside packages/rest, so the dispatch's stop clause about a producer or consumer living elsewhere did not apply.",
"Also outside the listed surface: scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json, an invariant text naming the viewer, which round 22's report listed; and comments describing the page in packages/rest/src/rest-server.ts and openapi-builtin-paths.ts, which fall under the dispatch's 'every comment that describes it'.",
"execctx-consumer-census §3: the 'rows ≥ 46' floor was not lowered to the new bare count of 45, because 51 rows were measured after the retirement and a floor that still holds is not loosened. The site, mention and bare counts moved to 61, 144 and 45.",
"origin/main moved by one packages/spec describe commit (edcc8ce). It was merged before the PR opened, per AGENTS.md §10, and packages/spec was rebuilt and checked. The whole rest suite and both typechecks ran at the pre-merge head b55b8f7; the incoming commit touches neither package. The gates, lint and dogfood ran at the final 1379d30.",
"The dogfood surfaces file ran twice on the final head. The second run used the verbose reporter filtered to the API-description cases, to put the new case's name and pass on record. The definition expects one dogfood run on the final head.",
"NOT MEASURED: the new dogfood retired-path case against main. Its red direction (401 to an anonymous caller and 200 to a member on main) comes from the assertions this file carried until now.",
"Host measurement used a scratch test in packages/qa/http-conformance/src with a stub auth service that reads an x-measure-anon header. It was never committed, and was deleted before the PR opened.",
"The PR body footer uses AGENTS.md's session-URL form, not the harness reminder's two-line form. Commits carry AGENTS.md's model-free trailer pair, not the reminder's model-named Co-Authored-By. Per the definition, harness attribution yields to the repo rule.",
"Reads of the card and comments used gh api as the session. with-fleet.sh --read refused, exit 3, nothing ran: in this cloud container it routes to the relay, which carries no command.",
"The worktree /home/user/objectstack-issue-25104 was removed after the PR opened: node_modules first, then a plain git worktree remove with exit 0. This report was posted from the shared checkout's scripts/pm, read-only."
],
"files_changed": [
".changeset/25104-rest-docs-viewer-retired.md (added)",
"docs/qa/platform-checklist/areas/access-security.json",
"packages/qa/dogfood/test/authz-conformance.matrix.ts",
"packages/qa/dogfood/test/authz-probe-blind-spot.census.ts",
"packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts",
"packages/rest/src/discovery-routes.ts",
"packages/rest/src/execctx-consumer-census.test.ts",
"packages/rest/src/html-escape.ts (deleted)",
"packages/rest/src/html-outlets.pin.test.ts",
"packages/rest/src/openapi-builtin-paths.ts",
"packages/rest/src/rest-api-config-dead-keys-refused.test.ts",
"packages/rest/src/rest-api-description-anonymous-deny.test.ts",
"packages/rest/src/rest-config-mount-table.pin.test.ts",
"packages/rest/src/rest-docs-viewer-retired.pin.test.ts (added)",
"packages/rest/src/rest-route-ledger.ts",
"packages/rest/src/rest-server.ts",
"scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json"
]
}