refactor(runtime,mcp): the last two admission doors classify the tenancy rejection through the shared classifier - #17683
Conversation
…pies onto the shared classifier WIP checkpoint before the verification lap. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 32 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ea00bbe0223cc3958d74bfd3b8c6f68a91744066 && git checkout ea00bbe0223cc3958d74bfd3b8c6f68a91744066
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1a25f4a8d7600ed0f9cea9dbe0a94135452d66ef 056cc4923b710ec6a4dbeed19b9593c56139ba27 && git checkout -B drift-repro 1a25f4a8d7600ed0f9cea9dbe0a94135452d66ef && git merge --no-ff 056cc4923b710ec6a4dbeed19b9593c56139ba27
node scripts/docs-audit/affected-docs.mjs --json 1a25f4a8d7600ed0f9cea9dbe0a94135452d66ef
|
|
ACCEPT — Arming waits on every check, not only the required ones. The file surface was honoured, not merely untrippedDeclared IN: Measured on the diff, ⛔ not taken on the PR body's word: 5 files ( ⭐ The fence was not just obeyed — it was made load-bearing and then proved soThe dispatch's sharpest ⛔ was that only the async kernel leg is in scope, because
And the #16013 binding constraint carried through verbatim in the docblock:
⭐ Then it proved the fence matters by violating it. Two ablations rather than one, each with on-disk mutation proof taken before the result was read:
A2 is the one worth naming: a fence is usually an instruction nobody tests. This one now has a test that fires only when it is crossed, which is the difference between a comment and a guard. Both restores proved by blob-hash equality to the HEAD blob plus an empty The census instruction, fifth reading — and no correction owedThe card's own load-bearing line was "Re-measure the population before starting", and this seat ran it a fourth time at 10:07Z. The dev ran a fifth, independently, from the symbols rather than any line number, and reports still exactly two foldable sites. ⇒ the fourth reading stands and ⛔ there is nothing to correct — which is the outcome that census was demanded for after moving three times historically. ⭐ It also declined to fold a fifth site nobody had named: Other evidence worth recording: the test layer was proved reached rather than assumed (
The docs-drift row — checked, and it is corroboration rather than driftThe advisory named one page precisely:
⇒ that is the same two-arm rule the shared classifier implements, described as behaviour rather than as internals. A behaviour-preserving fold cannot falsify it. ⭐ Not drift — the page and the new code agree. ⛔ No cards owed from this report — stated because the last two produced severalAll three of the dev's "noted, not filed" items dispose correctly on their own terms and ⛔ none needs a card:
Landing verification owed after the queue merges:
Generated by Claude Code |
Fixes #17114
@objectstack/core'sclassifyAdmissionTenancyPostureis the ONE place the #13906 decision 1 option A classification lives. Two admission doors were still hand-writing it, out of the declared scope of the fold that extracted it. They now call it.The census, re-measured — still exactly two
The card's own load-bearing instruction, run a fifth time, on this branch's base
e1fdaf8b0(origin/mainat worktree creation):packages/runtime/src/security/resolve-execution-context.ts, inresolveExecutionContextpackages/mcp/src/plugin.ts,resolveStdioTenancyPosture— the async kernel legMeasured from the symbols, not from any line number —
git grepforeffectiveTenancyPosturecrossed withisServiceNotRegisteredError, both overpackages. Population: 2. That agrees with the PM seat's fourth reading, so there is no correction to report.Everything else the two greps turn up, and why none of it is a third copy:
packages/mcp/src/plugin.tssync leg — the deliberate sync fallback. ⛔ NOT folded; see the fence section below.packages/runtime/src/domains/keys.tsandpackages/runtime/src/domains/activation-gate.ts— reach the service throughdeps.resolveServiceOrLoud(...)and carry no not-registered arm at all. A different, deliberate shape;keys.tsrecords the reason verbatim.packages/rest/src/rest-api-plugin.ts(two sites) — the quiet-return-undefinedshape, disposition (b) in the parent card's amendment.packages/runtime/src/http-dispatcher.ts—classifyServiceanswering{outcome: 'never-registered'}; that is the upstream classifier the three sites above rely on, not a copy of this one.plugin-auth/plugin-sharing— privatethis.effectiveTenancyPosture()methods over an already-held service. No registry rejection in sight.What was extracted, and what deliberately was not
The extractable part is the classification, not the resolution. Each seam keeps its own guard and hands its own former accessor expression in as the thunk:
The helper never learns how a seam reaches the service — one that did would be wrong for one seam or grow a flag per seam, which is the copies with extra steps.
⛔ The fence, and it is now a measured arm rather than a comment
The stdio door's sync fallback is taken only on a
KernelBase-shaped host that has nogetServiceAsyncat all.ObjectKernelBase's sync accessor throws a plain[Kernel] Service 'tenancy' not found— unbranded, because it never reachesPluginLoader.getService. Routing it through the shared classification would mint a 503 outage out of a supported composition, so its barecatchremains that seam's recorded decision.packages/runtime/src/domains/{keys,activation-gate}.tsandpackages/rest/src/rest-api-plugin.tsare untouched for the reasons in the census table.Tests — the discriminating behaviour at each folded seam
Both seams already carried the never-registered-versus-registered-and-failing pins; both suites are green unchanged. What is added is the pin the wrong fold fails, one per seam, plus the fence arm:
packages/runtime/src/security/resolve-execution-context.test.ts— a lookup that throws synchronously (withinKernelServiceLookup's declaredPromise of any, or any) must classify exactly as its rejecting twin: branded ⇒ quiet and admitted; unbranded ⇒ 503SERVICE_UNAVAILABLEwithobject: 'tenancy'. Both error values are read out of a realObjectKerneland then re-raised, so neither arm is a hand-built brand at the seam under measurement.packages/mcp/src/stdio-tenancy-posture-api-key-matrix.test.ts— the same two arms through the stdio door's boot, plus⛔ FENCE: a host with NO getServiceAsync takes the SYNC leg and stays quiet on its UNBRANDED miss.Ablations, each with on-disk mutation proof taken BEFORE the result was read
A1 — the wrong fold at the runtime seam. Resolve the service first, hand the helper a settled value (
() => ablationSettled). Marker occurrences on disk 0 to 2, the folded shape 1 to 0, blob21dbf185tob1cbc2b9.A2 — the over-scope fold at the fenced sync leg. Route the mcp sync leg through the helper. Marker 0 to 1, sync-leg anchor 1 to 0, blob
673907e8to0e1da1ec.Exactly the fence arm, and nothing else. Both restore legs are proved by blob hash equal to the
HEADblob and an emptygit diff HEAD, not by an exit code.Verification
Every heavy run went through
scripts/pm/os-verify-lock.sh; each verdict below is the wrapper's ownVERDICT command-exitline, never a bare exit status.pnpm --filter '@objectstack/runtime^...' --filter '@objectstack/mcp^...' build --concurrency=2VERDICT command-exit 0pnpm --filter @objectstack/runtime --filter @objectstack/mcp typecheckVERDICT command-exit 0pnpm --filter @objectstack/mcp testVERDICT command-exit 0— 31 files, 333 tests passedpnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2VERDICT command-exit 0— 256 files, 3590 tests passedpnpm --filter @objectstack/runtime --filter @objectstack/mcp buildVERDICT command-exit 0Gate roster derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no hand-written path list), re-derived after the changeset existed: 59 commands, every one run with its output redirected and$?captured before any pipe.Reconciled with
--ranafter a freshgit fetch origin main(so the derivation is against the newest localorigin/main, and the change set is still the same 5 paths off merge basee1fdaf8b0, three-dot):The two NOT MEASURED, with their reasons —
exit 3isPREREQUISITE NOT MET, which is neither a pass nor a failure, and both want a whole-tree build that is CI's run, not this card's:pnpm check:dual-build-cjs-loads— "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured." 36 packages unbuilt in this worktree.pnpm check:type-check-debt— "--re-measure cannot run: 1 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk", and its own text forbids reading anything into it.pnpm lint— the FULL union, ⛔ never narrowed to these paths.scripts/pm/dispatch-gates.mjsdoes not name it, so this lane adds it by hand:Zero problems over the whole repository. Positive control beside the zero, because a silent linter and a clean tree read alike — the same binary, the same
--no-inline-config, scoped topackages/mcp/src/plugin.ts:The test layer is proved reached, not assumed.
tsc -p tsconfig.test.json --listFilesnames both edited test files in the compiled program (stdio-tenancy-posture-api-key-matrix.test.tsx1,security/resolve-execution-context.test.tsx9), andpnpm --filter @objectstack/{runtime,mcp} check:test-typecheckisOKon both — the shrink-only, identity-pinned ledgers held (27 files / 191 errors for runtime, 6 / 53 for mcp; nothing added).Changeset — owed, and here is the measurement
patchon@objectstack/runtimeand@objectstack/mcp, ⛔ notskip-changeset. Both publishfiles[]: ["dist"], and after a real build the new call is in the shippeddistof each — 2 files each, with a known-absent symbol scoring 0 as the negative control andisServiceNotRegisteredErrorscoring 4 inruntime/distas the lit positive control.@objectstack/mcp'sdistno longer mentionsisServiceNotRegisteredErrorat all. Something published moves, so a changeset is owed even though no behaviour and no export does.Clause-②: no— confirmed by measurement, not assumed:classifyAdmissionTenancyPostureis already exported frompackages/core/src/security/index.tsand already imported from 8 files outsidepackages/core. This adds a 9th and 10th consumer, not a surface. No accepted input, no new export, no authorable key.Acceptance notes
Noted, not filed — neither is a reproducible defect, a declared-contract violation, or an authoring trap, and both are cosmetic drift inside files a future reader of this seam will open anyway:
classifyAdmissionTenancyPosture's export atpackages/core/src/security/index.ts:119; it reads:117today, and the mcp site reads:90-:101against the card's:92-:99. Ordinary line drift on a read-only file, already flagged by the dispatch itself — recorded so the next reader anchors on symbols. Carrier: any later card folding onto this helper.packages/runtime/src/security/resolve-execution-context.ts's module docblock still narrates the classification inline ("throwsAuthzStoreUnavailableError(503) for the one class of fault ..."). Still accurate, now one indirection away from the code. Left as is rather than rewritten: the prose is right, and rewriting it would enlarge an S-sized diff. Carrier: the next change to that door's tenancy read.Authored in Claude Code session
session_01TSf4DV7ziu4V5j73e46b7c. The attribution block below is the platform's append on the edit channel, not a second footer of mine — the session-URL form this write sent was appended over rather than stored, so the durable reference is this sentence.Generated by Claude Code