test(rest): measure identity resolution at GET /ui/view/:object/:type — seam, downstream producer, and the ratchet blind spot behind it - #13244
Draft
os-trump wants to merge 2 commits into
Conversation
…, at the seam and downstream Measurement only -- nothing is repaired. Drives the route on the real registerRoutes() table under an absent and an entitled execution context, records the resolveExecCtx call count, and continues past the seam into the real getUiView producer, which #13214 marks unmeasured. Every reading stands next to a positive control on the same instrument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
The first draft passed 27 arguments to a 20-parameter constructor. It ran -- JS drops the surplus -- but silently shifted serviceExistsProvider, requestEnvResolver and metadataServiceProvider onto the wrong parameters, so the boot under measurement was not the boot the file describes. tsconfig.test.json covers this file, which is what turned it into a compile error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-trump
pushed a commit
that referenced
this pull request
Aug 29, 2026
Extends PR #13244's measurement with the axis it explicitly did not assert: its harness had no envRegistry and no kernelManager, so the environment resolution chain was read from source and never driven. Measurement only. rest-server.ts is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13214
⛔ MEASUREMENT ONLY. Nothing here repairs, gates or changes access-control behaviour.
Access control is a human floor in this repo. If a reading below is judged a defect, the
repair is a separate card with a human decision on it. What this PR lands is the instrument
and its readings, so nobody has to re-derive them from grep.
packages/rest/src/rest-server.tsis unchanged. The diff is one new test file.Card premise: HOLDS, and is now measured on both halves
#13214 relayed four seam readings from #13160 / PR #13213 and explicitly marked one thing
UNMEASURED: whether
getUiViewapplies authorization of its own downstream. Every relayedreading reproduced on an independent harness (this file builds its own; it cites #13213
for nothing), and the unmeasured half is now driven.
① Does
getUiViewgate downstream? — No. Measured, not inferred.Driven against the real
ObjectStackProtocolImplementationmounted on the realregisterRoutes()table:resolveExecCtxcalls200200The one field the producer drops (
hidden) is dropped by declaration — identically forboth callers.
salaryis served to both. There is no caller-keyed narrowing anywhere on thepath, and there cannot be: §2 records the exact argument the seam hands the producer and it
is exactly
{ object, type }— nocontext,executionContext,ctx,userId,user,tenantId,organizationId,principal,reqorrequest, driven with an entitledcontext in scope so this is not "there was no identity to pass". §4 adds why the producer
could not gate even if it wanted to:
resolveProtocolhands the same instance to twodifferent requests, so it holds no per-caller state, and the shipped
getUiViewdeclaresone parameter — the record §2 measured.
② Route gap, or RATCHET gap? — ⭐ RATCHET gap. This is the larger finding.
packages/qa/dogfood/test/authz-conformance.matrix.tscarries no row for this route, andits header says a new ungated route is UNCLASSIFIED and breaks CI. The two candidate
explanations the card names are not equally true:
discover()'s curated probe tablecannot reach
registerUiEndpointsat all, so no row — named or unnamed — could becovering it. Measured:
PROBEStable is 16 entries over 11 files. Forpackages/rest/src/rest-server.tsit carries exactly one non-tripwire probe:
/private\s+registerMetadataEndpoints\s*\(/.The other two are the authz matrix: the
rest-server.tsrealtime tripwire matches only a/realtimeliteral — it cannot fire for/wsor/api/v1/stream, the two transports the protocol page documents #9084 realtime tripwires, which match nothing in this file today.discover()mints 9 keys repo-wide. The matrix'scoverskeys are the same 9. Noneconcerns
/ui.same file):
registerUi→ 0,/ui→ 0,getUiView→ 0,ui-view→ 0, againstregisterMetadataEndpoints→ 2,anonymous-deny→ 20,enforced→ 78.⭐ The number the card asked for. Driven on the real route table (§6 commits this half):
RestServer.registerRoutes()mountsrest-server.tsregisterMetadataEndpoints)registerUiEndpointsis one of 16 sibling registrars invisible to the ratchet —registerCrudEndpoints,registerApprovalsEndpoints(12 routes),registerDataActionEndpoints(9),
registerReportsEndpoints(8),registerSharingRuleEndpoints(5) and the rest. It isnot special; it is simply the one a census happened to walk past. The ratchet's stated
guarantee — "a new ungated route is UNCLASSIFIED and breaks CI" — is true for the
/metaumbrella and false for 66 of the 85 routes this file mounts.
Asserting the cross-package half inside a
packages/resttest would widen this package'sreal input set past its
CROSS_PACKAGE_TEST_INPUTSdeclaration — the #7802 defect — so theprobe-table readings above are reported here and reproducible with the commands in the
issue-comment report.
③
isAuthGateAllowlisted— confirmed NOT the explanationVerified by driving the predicate, not by reading the array: all four spellings of this
route (
/api/v1/ui/view/:object/:type, the form variant, the environment-scoped mount, andthe query-string form) read
false. Positive control on the same predicate:/api/v1/auth/sign-in→true,undefined→true. So thefalseis a decision, not apredicate that returns
falsefor everything.An instrument that can only produce one answer has measured nothing — and that cuts both
ways here. A
200from a harness that could never have shown a refusal is not evidence of afail-open either.
GET /api/v1/data/:objectwith401 UNAUTHENTICATEDunder an absent context (resolveExecCtxcalls > 0), and serves itunder an entitled one. So "200 with 0 calls" is a reading about this route.
contextkey when one is present, soObject.keys(arg) === ['object','type']is not a recorder silently dropping keys.403 PERMISSION_DENIED. Had the shipped producer gated, this harness would have said so.registry makes it answer non-200.
truefor something.Reverse verification (ablation), on the committed implementation
Mutation leg — inserted
resolveExecCtx+enforceAuthintoregisterUiEndpoints:1(unique); injected-textgrep -c→2; blob moved3eca151d→8d0b18cb. ⛔ Mutation confirmed on disk, never from the editor's exit code.§3 downstream readings flip red. Predicted direction, observed.
the seam before the producer is reached, so the downstream refusal it looks for no longer
happens.
Restore leg —
git checkout HEAD -- "$REPO_ROOT/packages/rest/src/rest-server.ts"(absolutepath,
HEAD-pinned to bypass the index, under atrap ... EXIT INT TERM):3eca151d= HEAD blob3eca151d; injected-text count0;git diff HEADempty;git statusclean.Verification
Gate union re-derived for the actual change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(the script takes thechange set from the merge base itself), then run at
0684c41b— the final commit on thisbranch, so these verdicts are about the tree that is actually here. Verdict lines are quoted
from each gate's own output, and every exit code was captured before any pipe.
pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2— 158 files / 2623 tests passedpnpm --filter @objectstack/rest typecheck— exit 0.check:test-typecheck: OK — @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json.NOT-MEASURED green:
tsc -p tsconfig.test.json --listFileslists this file, and it is whatcaught a 27-argument call to the 20-parameter
RestServerconstructor — a harness that ranfine while silently shifting three providers onto the wrong parameters (second commit).
pnpm check:authz-resolver—✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate.check:cross-package-test-inputs,check:dispatcher-error-vocabulary,check:logger-receiver-detach,check:objectql-double-limit,check:page-declaration-shape,check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check:query-options-erasure,check:engine-double-contract,check:where-matcher,check:type-check-coverage,check-ci-filter-parity,check-comment-mask-adoption,check-cross-package-test-inputs,check-keyed-text-bounds,check-plugin-teardown-shape,check-shard-attestation,check-undeclared-dep-imports,check-nul-bytes,docs-audit/check-affected-docs,docs-audit/check-drift-comment.turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*, 70/70 successful), because the next two refuse outright on anunbuilt tree and that refusal is NOT MEASURED, never a pass:
✓ check:dual-build-cjs-loads — 102 published require entry point(s) across 66 package(s) load; 610 emitted CommonJS file(s) parsecheck-type-check-coverage --re-measure: OK — 30 ledger entr(ies) re-measured in 250.5s, 1560 raw tsc error(s) total, none above its recorded number—surplus: none, so thenew test file adds no type-check debt.
scripts/check-test-completeness.mjsexits 3(
PREREQUISITE NOT MET) with no argument — it grades a savedturbo run testlog andcannot produce one. CI tees the log and passes the path, so CI measures it. ⛔ Recorded as
NOT MEASURED, not as a pass and not as a red.
No changeset: the diff is a single test file, publishes nothing from any package and changes
no user-visible behaviour —
skip-changeset.Measured and written by Claude Code in session
https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k(kept in prose because a body editrewrites the footer link below back to its bare form).
Generated by Claude Code
Generated by Claude Code