Skip to content

fix(runtime): the /keys mint and install-wide activation gates classify a tenancy resolution failure instead of reading it as "no wall" - #16385

Merged
os-litant merged 9 commits into
mainfrom
claude/issue-15900-tenancy-posture-loud-resolve
Sep 6, 2026
Merged

fix(runtime): the /keys mint and install-wide activation gates classify a tenancy resolution failure instead of reading it as "no wall"#16385
os-litant merged 9 commits into
mainfrom
claude/issue-15900-tenancy-posture-loud-resolve

Conversation

@os-litant

@os-litant os-litant commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15900

Clause-②: yes

Two dispatcher domain gates derived the effective tenancy posture through DomainHandlerDeps.resolveService, which is the dispatcher's capability probe: every step of its fallback chain absorbs every rejection and answers undefined. So a tenancy service that was registered and failed to build arrived at both gates as the same value a deployment that never registered one produces, and both read that as "there is no wall".

Round 2 — the contract review found a regression this branch had introduced, and it is repaired here

The at-tier review drove a healthy tenancy registered ServiceLifecycle.SCOPED and found that the new classified read at the activation gate dropped the scope id — deps.resolveServiceOrLoud(context, 'tenancy') where keys.ts and the identity step both pass context.environmentId. A scoped registration resolved without a scope id rejects UNBRANDED (Scope ID required for scoped service 'tenancy'), the classified lookup re-raised it correctly, and the gate answered 503 on a service that was never unwell — an outage the gate manufactured from its own omission.

Re-driven here at the previous head, entering at each door body, healthy scoped tenancy reporting isolated, environmentId = 'platform':

probe previous head after this round
POST /keys mint, org-less caller 400 walled refusal (correct — keys passed the scope) 400, unchanged
actions activation, tenant org admin 503 403 PERMISSION_DENIED, no row written
actions activation, PLATFORM_ADMIN operator 503 200, row written
automation toggle, tenant org admin 503 403 PERMISSION_DENIED, no write
automation toggle, PLATFORM_ADMIN operator 503 200, write lands
every door, throwing factory 503, nothing written 503, nothing written
every door, never registered admits, as before admits, as before

So the branch had converted one wrong admit and one correct admit into outages, and the caller it locked out is the platform operator — the one authority ADR-0126 §5 says this install-wide switch belongs to. It also re-created the very split this branch exists to remove: the identity step, which resolves with the scope, read that deployment as healthy while the gate called it an outage. Every direction and count above was written down before the run and matched.

Two more review findings are answered in the same round:

  • The gate has TWO doors, and only one was named. refuseUngrantedActivationWrite is one body called from domains/actions.ts:156 and from domains/automation.ts:1050 (POST /automation/:name/toggle, through refuseUngrantedFlowActivationWrite). Both await it, so nothing was ever unhandled — but the toggle door inherited the new throw exit while going unnamed and unpinned. It is now named in the gate's own doc block and pinned beside the other two doors, and the sentences that said otherwise are corrected in the commit that carries the change.
  • A sentence in commit 8d189af was overstated and would have landed in main verbatim. Corrected in a later commit that quotes it; the measurement is under Verification below.

Clause ② is yes, and the changeset is minor

DomainHandlerDeps is a published type: packages/runtime/src/index.ts re-exports it, the package's exports["."] resolves to dist/index.d.ts, and the emitted declaration carries the interface with the new required member resolveServiceOrLoud(…). A required member added to an exported interface is a new symbol on the published surface — every external const deps: DomainHandlerDeps = {…} stops compiling. This repository has graded that exact shape three times already, all minor, all under ## 17.0.0 / ### Minor Changes in packages/runtime/CHANGELOG.md (lines 7491, 7572, 7587: "DomainHandlerDeps grows two lazily-read members", "grows the shared facilities the body needs", "grows getRequestKernelService … and routeNotFound").

Round 1 declared no from an in-repo grep — one constructor, no implementer outside packages/runtime — which is evidence of presence and never evidence of absence; the mechanical floor exists precisely so likelihood is not the thing argued. So the declaration is yes on both carriers, the changeset is '@objectstack/runtime': minor, and the "Patch rather than minor" sentence that justified the level no longer in use is gone rather than left standing.

The conformance limb stays no by the repo's own written negative boundary: a runtime permission/security behaviour change belongs to the manual floor's security class, not to clause ②.

Nobody had run this. Now somebody has.

The card says of itself "source reading, not an end-to-end run", and triage added that its line-level check must not be inherited as proof that a broken tenancy service really lets these doors through. So this branch drove it first, against the pre-fix tree, with a real ObjectKernel whose tenancy is registered through a throwing factory:

PROBE keys-mint answer        = {"status":201,"rowsMinted":1,"keyEchoedOnce":true,"activeOrg":null}
PROBE activation-write answer = {"status":200,"activationRowWritten":1,
                                 "writeArgs":[{"name":"convert_lead","packageId":"crm","active":false}]}

The premise holds, exactly as written: POST /keys minted an organization-less key and echoed the raw secret once, where a walled posture refuses one; and a tenant organization administrator's install-wide activation write was served and the row written, where ADR-0126 §5 requires the platform operator.

Why this is security and not tidiness

The identity step has read this same fact through the classified lookup since PR #15909. So one deployment holds two different readings of its own wall question at the same moment: 503 at the identity step, and admission at these door bodies. Stated precisely, because the reach matters and the review measured it: for the throwing-factory class a WIRE request dies at the identity step and never reaches these gates, so the permissive half is reachable by an embedder calling handleKeys / handleActions / handleAutomation directly — which is exactly where the pins enter. The inconsistency is still worse than either behaviour alone, because there is no longer a single answer to what the deployment's posture even is.

The change

Ruled on the card 2026-09-06T13:51Z (director seat, decision batch #55), maintainer's reply verbatim and untranslated: 「同意」 — option A, the narrow two-site fix.

The gates now read the posture through a classified lookup, and the two facts are told apart by the registry's own brand (#13905), never by message text:

  • never registered ⇒ no posture, and today's answer is preserved exactly. An org-less key is still minted; a single-organization deployment's own administrator can still flip an install-wide switch, because with no tenancy service install-level and org-level are one scope (ADR-0093 D4/D5). Refusing here would lock every single-organization operator out of their own switch.
  • registered and unable to answerAuthzStoreUnavailableError, so the door answers 503 SERVICE_UNAVAILABLE. The posture is an authorization INPUT that was never read, so admission was never decided — 「A posture that could not be READ is not a posture that is ABSENT.」 ([finding] two more computeExecCtx seams read "failed" and "not wired" as one value, and both feed authorization inputs — tenancy posture and the ADR-0069 auth gate #13906 decision 1 option A). Nothing is minted and nothing is permitted.
  • and, from round 2: the read carries the request's own scope, exactly as the identity step and the mint gate do, so a healthy scoped tenancy is resolved rather than reported as an outage.

Two ruled call sites, three routes, because the activation gate is one body behind two doors. Sites, re-derived on this head:

file line what it is
packages/runtime/src/domains/keys.ts :135 the posture read; decision at :140 (const walled = …)
packages/runtime/src/domains/activation-gate.ts :217 the posture read; decision at :222, operator test at :228
packages/runtime/src/domains/actions.ts :156 door 1 onto that gate
packages/runtime/src/domains/automation.ts :1050 door 2, via refuseUngrantedFlowActivationWrite (:532)
packages/runtime/src/domain-handler-registry.ts :172 the new resolveServiceOrLoud deps member
packages/runtime/src/http-dispatcher.ts :408 its wiring to the dispatcher's existing private classified lookup

What is deliberately NOT here

Option C — rerouting 'tenancy' through the loud lookup for every domain — is explicitly not ruled, and the card and triage both say why: it would change every gate that reads the name in one stroke, and nobody has enumerated those gates. resolveService keeps its probe contract for every other name and every other domain. The classified read is a second, opted-into deps facility that a gate calls at one call site at a time, so no gate outside the three routes named above changes behaviour. The boundary — and the round-2 rule that a caller must pass the scope it holds, so a rejection describes the SERVICE and never the call site's own omission — is written on the interface member itself.

keys.ts carried a comment documenting "an ABSENT service means we cannot tell, and the honest answer at MINT time is to mint" as a deliberate choice. That choice is kept and is now scoped at the site to the class it was always true of — never-registered — so the next reader does not re-derive the collapsed reading.

Anchor rot, reported

Triage read activation-gate.ts:142 / :146. On this branch's base (a87163c89b9) the real sites were :161 / :165 — the file had moved by 19 lines before any work started. keys.ts:114 was exact. Every anchor in this PR was re-derived, not inherited, in both rounds.

Verification

Gate union derived mechanically, twice, from the delivered diff (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, plain and --commands) — never from a hand-written list, and never from a hand-made file list either: the script takes its own change set from the merge base.

  • 84 runnable families — all green. Three first answered PREREQUISITE NOT MET / "the package is not built" (check:skill-examples, check:dual-build-cjs-loads, check:type-check-debt); rather than record the non-measurement, the whole package closure was built (turbo run build --filter='./packages/*' --filter='./packages/*/*', 71 tasks) and all three then ran green.
  • 38 artifact-roster families — run, not read as silence. 35 green. Three are unmeasurable in this container and say so themselves rather than passing: check-partof-closing-keyword and check-single-claim-paths print NOT WIRED with no PR context (the first was then driven locally with this body and this branch's commit list — see below), and check:react-declaration-parity needs objectui's sdui.manifest.json, which this repository does not contain.
  • 10 declared-wide families — the block whose absence from the matched set is explicitly not a clearance. Run anyway: 10/10 green.
  • check:system-context-census reddened twice on line rot from these edits and was repaired both times by the gate's own --fix (the one docs line in the diff); the census population is unchanged at 105 sites / 140 anchors.
  • check-partof-closing-keyword driven locally with PR_BODY = this body and PR_COMMITS_FILE = this branch's nine commit messages: clean on both rules — no commit carries a card-relation trailer, and the body's first line is the single declaration.
  • check-changeset-no-major --event driven against the LIVE label set (needs:contract-review included) and this declaration: LEVEL AXIS green on yes + minor. It is a measurement, not a formality — the same run against a patch level (committed in a throwaway worktree, because this gate reads HEAD and a working-tree edit is invisible to it) answers exit 1: "This PR declares clause-② YES and grades a package it grew patch."
  • pnpm --filter @objectstack/runtime test238 files / 3360 tests, all passing (3352 before, +8 new pins). pnpm --filter @objectstack/runtime typecheck green, including check:test-typecheck. The test layer is proven to COVER the pin file rather than assumed to: tsc -p tsconfig.test.json --listFiles lists it (1 hit), and a plain run shows 191 pre-existing debt errors with zero in it.
  • pnpm lint — the whole repo, eslint . --no-inline-config, exit 0. Not a narrowed run, so no narrowing evidence is owed.

Every reading above was taken on the final head: 0795550f357. Round 2 was driven in session session_01D47qPfEWVPmhguWgBZCi5N.

The pins, and why they enter at the door body

14 pins across three doorsPOST /keys, the actions activation write, and the automation toggle — over three wirings of tenancy: never registered (the control), registered through a factory that throws (the class this card is about), and registered ServiceLifecycle.SCOPED and healthy (the class round 2 added). Without the never-registered control a pin cannot show the two classes were separated, only that the door got louder about both; without the scoped-healthy leg it cannot tell "loud on a broken service" from "loud on everything", which is exactly the defect the review found.

The operator leg is the load-bearing one there: every other caller in the file is one a refusal is a correct answer for, so a gate that answered "no" to everything would still satisfy them. The PLATFORM_ADMIN operator is the only caller whose correct answer is 200.

They call handleKeys / handleActions / handleAutomation directly rather than dispatch(), deliberately. On the dispatch() path the identity step runs first and already answers 503 for the throwing-factory fault, so a pin routed through it would go green on PR #15909's change with these gates left exactly as they were — a phantom check. Each pin asserts the ADR-0112 envelope (status and code, plus the outage brand or the §5 PERMISSION_DENIED), that the dispatcher's own error exit renders the outage as a 503 on the door, and — the load-bearing half — that nothing was minted and no activation row was written.

The fixture builds a real ObjectKernel rather than a hand-rolled brand: serviceNotRegisteredError is package-internal to @objectstack/core, so a double's own brand would be this file's opinion of the classification instead of the classification. In this tree tenancy is registered as an instance by plugin-auth, so both the failed-to-build and the scoped classes had to be CONSTRUCTED — a real registerServiceFactory, resolved through the real PluginLoader.getService.

The new fixture reddened two gates, and both were real

check:engine-double-contract and check:objectql-double-limit both read the new test file. The update / delete verbs on the doubles had been copied from a sibling fixture and no door under test reaches either, so they were coverage nobody was getting and would have owed the retained ledger two rows for pins that can never fire. They were removed rather than pinned — the ledger is untouched (744 rows held, none added; the limit baseline verified "no files added"). The find double applies the caller's limit by presence and after the filter, so it cannot answer a page the producer would not have returned. Round 2's automation double follows the same rule: getFlow and toggleFlow, nothing this door does not reach.

Ablation — predicted first, in both rounds

Round 1 mutated packages/core/src/service-not-registered.ts, the file that owns the classification the pins restate:

mutation predicted observed
M1 drop the brand from the loader's rejection 2 red: both never-registered controls 2 failed / 4 passed — exactly those two
M2 brand everything (the pre-fix collapse, rebuilt from the mirrored side) 4 red: the four throwing-factory legs 4 failed / 2 passed — exactly those four

Round 2 adds two legs against the enlarged file (14 tests):

mutation predicted observed
A the defect itself: the activation gate drops the scope id again 4 red — the four scoped-healthy legs on the gate's two doors; the keys leg stays GREEN because its own call site is untouched 4 failed / 10 passed — exactly those four, keys green
B the mirrored rule: invert PluginLoader's SCOPED guard so a scoped service can never resolve WITH a scope id 5 red — every scoped-healthy leg, on all three doors, proving they really resolve through the loader's SCOPED path 5 failed / 9 passed — exactly those five

Every mutation was confirmed on disk by blob hash plus grep counts of the removed and injected text, and every restore the same way — hash equality against the HEAD blob and an empty git diff HEAD — under a trap … EXIT INT TERM with absolute paths, and git checkout HEAD -- ABSOLUTE_PATH never a bare checkout.

…ctivation-write gates

Both gates derived the effective tenancy posture through
`DomainHandlerDeps.resolveService`, the dispatcher's capability PROBE, whose
fallback chain absorbs every rejection at every step and answers `undefined`.
So a `tenancy` service that was registered and FAILED to build arrived at both
gates as the same value a deployment that never registered one produces, and
both gates read that as "there is no wall": `POST /keys` minted an
organization-less key it would otherwise refuse, and an organization
administrator's install-wide activation write was served instead of refused.

Measured on the pre-fix tree with a real `ObjectKernel` whose `tenancy` is
registered through a throwing factory: the mint door answered 201 with one row
written and the raw secret echoed once, and the activation door answered 200
with `setActionActive` called.

The identity step already reads this fact through the classified lookup
(`resolveServiceOrLoud`, #13906 decision 1 option A). That made one failure
answer 503 at the identity step and admit at these two gates in the same
deployment, so "what is this deployment's state on the wall question" had two
answers at once. The gates now read the same classification: never registered
stays quiet and behaves exactly as before, every other resolution failure is
re-raised as `AuthzStoreUnavailableError` (503 `SERVICE_UNAVAILABLE`).

`resolveService` keeps its probe contract for every other name and every other
domain; the classified read is a second, opted-into deps facility, so no gate
that was not named here changes behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…nd honour the caller's bound

`check:engine-double-contract` and `check:objectql-double-limit` both read the new
fixture. The `update`/`delete` verbs were copied from a sibling and no door under test
reaches either, so they were coverage nobody was getting and would have owed the
retained ledger two rows for pins that can never fire; they are removed rather than
pinned, and the ledger is untouched (744 rows held, none added). The `find` double now
applies the caller's `limit` by presence and after the filter, so it cannot answer a
page the producer would not have returned.

`check:system-context-census` line rot from the gate edit is repaired by the gate's own
`--fix`: two anchors in the elevation-read page move with the lines they cite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
@github-actions github-actions Bot added the size/m label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 9 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/environment-routing.mdx (via HttpDispatcher (symbol, a top-level class))
  • content/docs/automation/webhooks.mdx (via HttpDispatcher (symbol, a top-level class))
  • content/docs/kernel/cluster.mdx (via HttpDispatcher (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via HttpDispatcher (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via automation.toggle (sdk, the route ledger binds it to POST /automation/:name/toggle), /automation/:name/toggle (route, a path literal on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4998efa71773154561c471075f4ef12566ecc455packageMentionDocs.

Which tree this was computed on

This run read content/docs from 58f0526cce4dcf83fddedbaf4d201105fcfbbce5 — the merge of head 0795550f3574c35bd100ac3dbffdee4b522fb717 into base 4998efa71773154561c471075f4ef12566ecc455, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 58f0526cce4dcf83fddedbaf4d201105fcfbbce5 && git checkout 58f0526cce4dcf83fddedbaf4d201105fcfbbce5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4998efa71773154561c471075f4ef12566ecc455 0795550f3574c35bd100ac3dbffdee4b522fb717 && git checkout -B drift-repro 4998efa71773154561c471075f4ef12566ecc455 && git merge --no-ff 0795550f3574c35bd100ac3dbffdee4b522fb717

node scripts/docs-audit/affected-docs.mjs --json 4998efa71773154561c471075f4ef12566ecc455

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4998efa71773154561c471075f4ef12566ecc455 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

VERDICT: CHANGES REQUIRED

Independent adversarial contract review of head 9b47f88a19c against card #15900 and the director-seat ruling (batch #55, option A). Everything below was driven in a dedicated worktree at the head (and a second one at the merge-base 455d0372f03), not read. I did not repair anything.

Clause ② — the headline. The declaration should be yes, and the changeset patch is therefore also wrong

Mechanical floor — YES. Derived from the tree, not from the ruling's forecast:

  • DomainHandlerDeps is a published type: packages/runtime/src/index.ts:106 re-exports it, package.json exports["."] resolves to dist/index.d.ts, and after building @objectstack/runtime at this head the emitted dist/index.d.ts carries the interface at line 2491 and the new required member resolveServiceOrLoud(...) at line 2554. The repo's own rule (.claude/skills/pm-dispatch/references/contract-review.md:12) is: 「新导出符号或已发布载荷上的新键恒 yes」— a new exported symbol is always yes, checkable in the tree at claim time. A required member added to an exported interface is a new symbol on the published type surface; every external const deps: DomainHandlerDeps = {…} stops compiling.
  • The implementer's argument for no is an in-repo grep: "exactly one constructor (HttpDispatcher.domainDeps) and no implementer anywhere outside packages/runtime". That is evidence of presence, never of absence. The type carries no @internal marker, and the registry's own header invites external domain bodies (「External packages that DO own a slot exclusively can still self-register via HttpDispatcher.registerDomainHandler」, which is public at http-dispatcher.ts:771). I checked the counter-case honestly: DomainHandler is (req, context) => … and receives no deps, no handle*Request is exported from index.ts, and domainDeps is private — so nothing published takes a DomainHandlerDeps. That makes external implementers unlikely, not impossible, and the floor is mechanical precisely so that likelihood does not get argued.
  • Precedent the PR does not cite: every earlier growth of this same type shipped as minor — three entries under ## 17.0.0 / ### Minor Changes in packages/runtime/CHANGELOG.md (lines 7491, 7572, 7587: "DomainHandlerDeps grows two lazily-read members", "grows the shared facilities the body needs: errorFromThrown …", "grows getRequestKernelService … and routeNotFound"). This PR grades the identical shape patch with the sentence "Patch rather than minor: no accept set widens". The LEVEL AXIS gate (check-changeset-no-major.mjs, [finding] No gate answers whether a changeset's LEVEL fits the surface — Check Changeset is green on patch and on minor for the same diff #16055) makes Clause-②: yes + patch a self-contradiction, so the two must move together: Clause-②: yes and '@objectstack/runtime': minor.

Doctrine says grade yes when the call is not clear. Here the published .d.ts measurably grew and the repo's own history graded that growth as a surface addition three times. no is the wrong call.

Conformance limb — NO, by the repo's written negative boundary, and I say so explicitly rather than inheriting it. The ruled change re-selects one input class (tenancy registered through a throwing factory) between two published verdicts (201/200503 SERVICE_UNAVAILABLE), which is the shape the limb asks about. But .claude/skills/pm-dispatch/SKILL.md:511 states 「负边界:运行时权限/安全行为变更不是条款②,归人工地板安全/权限边界类」— runtime permission/security behaviour changes are not clause ②; they belong to the manual floor's security class. The implementer cited that boundary accurately. The one place this PR re-selects an input class that is not a security narrowing (a healthy scoped tenancy + the platform operator: 200503, finding 1) is a defect to fix, not a surface to declare.

Findings

1. Over-firing 503 — the activation gate manufactures its own outage on a HEALTHY per-environment tenancy, locking out the platform operator. Severity: high (functional regression for the sanctioned caller; reachable through dispatch()).

activation-gate.ts:189 calls deps.resolveServiceOrLoud(context, 'tenancy') with no scope id, while keys.ts:135 passes context.environmentId and the identity step (http-dispatcher.ts:564) passes context.environmentId. For a tenancy registered ServiceLifecycle.SCOPED (the card itself lists "a scoped registration resolved without a scope id" as a reachable class), PluginLoader.getService throws the unbranded Scope ID required for scoped service 'tenancy' — from the gate's own omission, not from any outage. Driven at head with a healthy scoped factory returning { posture: 'isolated' } and context.environmentId = 'platform':

identity-step-shaped read WITH scope id            → resolves { posture: 'isolated' }   (healthy)
keys gate, org-less caller, same registration      → 400 (walled refusal; read the posture correctly)
activation gate, tenant org admin                  → 503 SERVICE_UNAVAILABLE, cause "Scope ID required…"   (should be the §5 403)
activation gate, posture PLATFORM_ADMIN (operator) → 503 SERVICE_UNAVAILABLE                                (should be 200 — the sanctioned authority)

Same probe on the merge-base: org admin 200 + row written (the pre-fix permissive collapse, wire-reachable because the identity step resolves with the scope and admits), operator 200 (correct). So for this class the PR converts one wrong admit and one correct admit into outages, and the identity step and this gate again give two answers about one healthy deployment — the exact inconsistency the PR's headline says it removes. The ruling's "any other resolution failure ⇒ 503" covers a failure the gate could not avoid; it does not license the gate discarding a scope id it already holds. Pass context.environmentId as keys.ts does, and pin the scoped-healthy class for both callers.

2. A third door inherited the new throw exit and has no pin. Severity: medium. refuseUngrantedActivationWrite is called from domains/actions.ts:156 and domains/automation.ts:1050 (POST /automation/:name/toggle, via refuseUngrantedFlowActivationWrite). Both call sites are awaited, so nothing is unhandled — but the PR body's "no gate that is not named above changes behaviour" and "two pins, one per gate" leave the flow-activation door unnamed and unpinned. The ruling names the gate, so this is within scope; the pins are not.

3. Wire reachability is overstated in the squash body. Severity: low, but it lands in main verbatim. Commit 8d189af: "That made one failure answer 503 at the identity step and admit at these two gates in the same deployment, so … had two answers at once." Driven at head and at the merge-base: dispatch('POST','/keys') with a throwing tenancy factory throws AuthzStoreUnavailableError (503) at the identity step, executionContext never set — the request never reaches either gate. Since #15909 the two gates' collapse is a door-body fact reachable by an embedder calling handleKeys/handleActions directly, not a second answer any wire caller could observe for that class. The PR body's pin section says this plainly; the commit message that will become the squash body does not. State it there.

4. Changeset level. Severity: blocking, and it follows from the clause-② judgement above. '@objectstack/runtime': patch contradicts three minor precedents for growth of this exact type and, once the declaration is corrected to yes, contradicts the LEVEL AXIS gate.

What I attacked and could NOT break

  • Pins at head: 6/6 green. Pre-fix tree (merge-base 455d037): exactly 4 failed | 2 passed, the four throwing-factory legs red, both controls green — as claimed.
  • Ablation, replicated on the mirrored side (packages/core/src/service-not-registered.ts, blob-hash guarded, restored to the HEAD blob each time): M1 drop the brand → 2 failed | 4 passed, exactly the two never-registered controls; M2 brand everything → 4 failed | 2 passed, exactly the four throwing-factory legs. The mutations reach the code under test (packages/runtime/vitest.config.ts aliases @objectstack/core to ../core/src/index.ts). The "first M1 attempt was a NO-OP caught by the hash guard" account is the implementer's session; I cannot verify it and did not need to — my own M1 changed the hash and reddened.
  • The fixture measures what it says: on an unstarted ObjectKernel the loader context is already set (kernel.ts:173), so the throwing-factory leg rejects with the factory's own message, not the loader's "Context not set". plugin-auth does register tenancy as an instance (auth-plugin.ts:615), so the class had to be constructed.
  • Option C creep: HttpDispatcher.resolveService body unchanged; the identity step's n === 'tenancy' routing unchanged; only the two ruled call sites plus the deps member and its wiring moved. No other name and no other domain reads the loud path.
  • Removed fixture verbs: update/delete on both doubles were genuinely unreached (the doors under test call insert/find/setActionActive; a reached verb would have thrown "not a function" and reddened). No ledger moved: git diff on scripts/ is empty; check:engine-double-contract prints 744 held at head; check:objectql-double-limit prints "baseline key set verified against 455d037: no files added".
  • check:system-context-census --fix: green at head (105 sites, 140 anchors resolve). The two moved anchors (activation-gate.ts:157→161, :211→239) are both the if (ec?.isSystem) return undefined; lines they always pointed at; the gate's own header says --fix re-anchors rot and refuses to move population, so this is not a baseline move.
  • Anchors: triage's :142/:146 versus :161/:165 on base a87163c89b9 — confirmed; keys.ts:114 exact — confirmed. Every line in the PR's site table matches head (keys.ts:135/:140, activation-gate.ts:189/:193, domain-handler-registry.ts:162, http-dispatcher.ts:408).
  • Typecheck / lint: tsc -p tsconfig.test.json → 191 errors, 0 in the new file; eslint --no-inline-config on the five changed TS files → exit 0.
  • Full runtime suite at head: 238 passed (238) files / 3352 passed (3352) tests, exit 0 — exactly the PR's numbers.
  • Every other sentence in the five commit messages, the PR body and the changeset checked against the tree; apart from findings 2–4 none is false.

Not measured by me: the 84+39 gate-family sweep and the whole-repo pnpm lint (I lint-checked the changed files only); the three "unmeasurable in this container" rosters.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review ADOPTED — CHANGES REQUIRED, at tier, verbatim. Carriers now hung; round 2 dispatched

domain:cli execution PM seat (#6024). Adopting the review posted above (5560878265).

✅ Tier verification

138 harness-stamped "model" fields across 121 assistant turns, every one claude-fable-5-1, zero off-tier. ⇒ Adoptable, adopted verbatim.

⭐ Clause ② is yes — the mechanical floor fires, measured on the BUILT tree

This seat deliberately withheld needs:contract-review and inverted the order — review first, label per the verdict — precisely because the declaration was the thing in question. The verdict:

DomainHandlerDeps is published. packages/runtime/src/index.ts:106 re-exports it; package.json exports["."] resolves to dist/index.d.ts; and the emitted dist/index.d.ts at this head carries the interface at line 2491 and the new required member resolveServiceOrLoud(...) at line 2554. Every external const deps: DomainHandlerDeps = {…} stops compiling.

⚠️ Round 1's argument for no was an in-repo grep — "one constructor, no implementer outside packages/runtime". ⭐ Evidence of presence is never evidence of absence, and the mechanical floor is mechanical exactly so that likelihood does not get argued. The reviewer checked the counter-case honestly (nothing published takes a DomainHandlerDeps) and still graded yes, which is the doctrine working: grade yes when the call is not clear.

And a precedent the PR never cited: every earlier growth of this same type shipped minor — three entries under ## 17.0.0 / ### Minor Changes in packages/runtime/CHANGELOG.md (7491, 7572, 7587). This PR graded the identical shape patch.

needs:contract-review is now hung on both carriers, and round 2 moves the declaration to yes and the changeset to minor together — the LEVEL AXIS gate makes yes + patch a self-contradiction.

⚠️ The conformance limb is no, and the reviewer says so explicitly rather than inheriting it: .claude/skills/pm-dispatch/SKILL.md:511 states 「负边界:运行时权限/安全行为变更不是条款②」. Round 1 cited that boundary accurately. The floor is what fires here.

⛔ Finding 1 is HIGH, and it is a regression this PR introduces

The activation gate manufactures its own outage on a HEALTHY scoped tenancy — and locks out the platform operator.

activation-gate.ts:189 calls resolveServiceOrLoud(context, 'tenancy') with no scope id, while keys.ts:135 and the identity step (http-dispatcher.ts:564) both pass context.environmentId. On a ServiceLifecycle.SCOPED registration, PluginLoader.getService throws the unbranded Scope ID required for scoped service 'tenancy' — an error caused by the gate's own omission, which the new classified lookup then faithfully re-raises as 503.

probe (healthy scoped, posture: 'isolated') this PR merge base should be
activation gate, tenant org admin 503 200 + row written §5 403
activation gate, PLATFORM_ADMIN operator 503 200 (correct) 200

⇒ ⭐ The PR converts one wrong admit and one correct admit into outages, and reintroduces the very identity-step/gate split it exists to remove.

⭐ This is exactly the attack the dispatch brief asked for — "are any of them ordinary conditions that now turn a working door into a 503? Construct one if you can." It was constructed. An over-refusal probe that only tries the obvious cases would have missed it, because the missing argument is invisible unless the registration is scoped.

The rest

  • F2 (medium)refuseUngrantedActivationWrite is also called from domains/automation.ts:1050 (POST /automation/:name/toggle). Awaited, so nothing is unhandled, but the PR's own claims ("two pins, one per gate"; "no gate not named above changes behaviour") leave that door unnamed and unpinned.
  • F3 (low, but lands in main verbatim) — commit 8d189af claims a failure "answer[ed] 503 at the identity step and admit[ted] at these two gates … two answers at once". Driven: dispatch('POST','/keys') throws 503 at the identity step and never reaches either gate. The collapse is a door-body fact. ⭐ The PR body says this correctly; the commit message does not — and the commit message is what enters main.

⭐ What the review attacked and could NOT break

Pins 6/6 at head and exactly 4 failed | 2 passed pre-fix; the ablation replicated independently (M1 → 2 failed | 4 passed, M2 → 4 failed | 2 passed); no option-C creep; the removed fixture verbs genuinely unreached with no ledger growth; check:system-context-census --fix not a baseline move; the anchors; the full runtime suite at 238 files / 3352 tests. "Every other sentence in the commits, PR body and changeset checked; apart from findings 2–4 none is false."

⚠️ One claim of the implementer's the reviewer declined to certify: "The 'first M1 was a NO-OP' account is unverifiable and unneeded — my M1 changed the hash and reddened." Recorded; nothing to fix.

⛔ Nothing flipped ready, enqueued, or armed. Round 2 carries F1 + F4 (declaration and level together) + F2 + F3.


Generated by Claude Code

…quest's own scope

The classified read this branch gave `refuseUngrantedActivationWrite` dropped
the scope id: `deps.resolveServiceOrLoud(context, 'tenancy')`, where the mint
gate in `./keys.ts` and the identity step in `../http-dispatcher.ts` both pass
`context.environmentId`. A `tenancy` registered `ServiceLifecycle.SCOPED` and
resolved without a scope id rejects UNBRANDED — `Scope ID required for scoped
service 'tenancy'` — which the classified lookup then re-raises, correctly. So
the gate answered 503 on a service that was never unwell, and it was the gate's
own omission that produced the fault it reported.

Driven at the previous head with a HEALTHY scoped `tenancy` reporting
`isolated` and `environmentId = 'platform'`, entering at each door body:

  actions/_activation  tenant org admin  threw 503 (cause "Scope ID required…")
  actions/_activation  PLATFORM_ADMIN    threw 503
  automation/:name/toggle  both callers   threw 503
  keys mint            org-less          answered 400 (correct: keys passed the scope)

So the branch converted one wrong admit AND one correct admit into outages: on
the merge base the tenant org admin was wrongly served 200 with the row written
(the collapse this card is about) and the platform OPERATOR was correctly served
200 — and the operator is the one authority ADR-0126 §5 says this install-wide
switch belongs to. It also re-created the split this branch exists to remove:
the identity step, which resolves with the scope, read that same deployment as
healthy while the gate called it an outage.

With `context.environmentId` passed, the same probe answers 403 PERMISSION_DENIED
for the tenant org admin (no row written) and 200 for the operator (row written)
on all three doors, and every throwing-factory leg still answers 503 with nothing
written. Predicted in writing before the run; all fifteen probe legs matched.

The pins grow a third wiring — `scoped-healthy`, a real `ServiceLifecycle.SCOPED`
factory that SUCCEEDS — because a pin file about outages that never registers a
HEALTHY service cannot tell "loud on a broken service" from "loud on everything".
The operator leg is the load-bearing one: every other caller here is one a refusal
is a correct answer for, so only the caller whose correct answer is 200 can catch
a gate that manufactured an outage.

The automation toggle door is pinned here too — see the following commit, which
corrects what the earlier message said about it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…ct two sentences in 8d189af

Commit 8d189af on this branch says, of the classified read:

  "That made one failure answer 503 at the identity step and admit at these two
  gates in the same deployment, so 'what is this deployment's state on the wall
  question' had two answers at once."

That is overstated for a WIRE caller and it would land in `main` verbatim,
because this repository squashes. Driven at this head, `dispatch('POST',
'/keys', …)` and `dispatch('POST', '/actions/_activation/…')` with a `tenancy`
factory that throws both answer 503 SERVICE_UNAVAILABLE with
`context.executionContext` never set, nothing minted and no activation row
written: the identity step raises first and the request never reaches either
gate. Since PR #15909 the collapse at these two gates is a DOOR-BODY fact — an
embedder calling `handleKeys` / `handleActions` directly, which is exactly where
this branch's pins enter — not a second answer a wire caller could observe for
that class. The pin section of the PR body already said this correctly; the
commit message did not. The severity argument is unchanged and does not rest on
the overstatement: one deployment still holds two different readings of its own
wall question, and the door-body reading is the permissive one.

The same commit also says:

  "the classified read is a second, opted-into deps facility, so no gate that
  was not named here changes behaviour."

True of `resolveService`'s other callers, false about the gate it edited:
`refuseUngrantedActivationWrite` is ONE body with TWO doors — `./actions.ts`
(`POST /actions/_activation/:object/:action`) and `./automation.ts` (`POST
/automation/:name/toggle`, through `refuseUngrantedFlowActivationWrite`) — so
the toggle door inherited the new throw exit while going unnamed and unpinned.
Both `await` the call, so nothing was ever unhandled; what was missing was the
statement and the coverage. Both doors are now named in the gate's own doc block
and pinned together in `tenancy-posture-outage-gates.test.ts`, which is where a
claim about "the gate" can be checked against both halves of the surface.

`DomainHandlerDeps.resolveServiceOrLoud` also gains the caller rule the scope-id
defect earned: pass the scope you hold, because a rejection out of the
classified read must describe the SERVICE and never the call site's own
omission. Under the plain probe that omission was invisible; under this one it
is a 503 for every caller of the door.

The census docs line moves with the anchors this edit shifted, by the gate's own
`--fix`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…ed interface

The changeset graded `@objectstack/runtime` `patch` and argued it in one line:
"Patch rather than minor: no accept set widens, and a declared guard returns to
enforced." That reasoning answers a different question than the level asks.

`DomainHandlerDeps` is a PUBLISHED type — `packages/runtime/src/index.ts`
re-exports it and the package's `exports["."]` resolves to the emitted
`dist/index.d.ts`, which carries the interface and the new REQUIRED member
`resolveServiceOrLoud(…)`. A required member added to an exported interface is a
new symbol on the published surface: every external `const deps:
DomainHandlerDeps = {…}` stops compiling. This repository's own history grades
that exact shape as minor — three entries under 17.0.0 / Minor Changes in
packages/runtime/CHANGELOG.md say `DomainHandlerDeps` grows two lazily-read
members, grows the shared facilities the body needs, and grows
`getRequestKernelService` and `routeNotFound`. Nothing distinguishes this growth
from those three.

The earlier `patch` rested on an in-repo grep — one constructor, no implementer
outside `packages/runtime` — which is evidence of presence and never evidence of
absence. The mechanical floor exists so that likelihood is not the thing being
argued.

So: level `minor`, and the clause-② declaration in the PR body moves to `yes`
with it — the LEVEL AXIS check makes `yes` plus `patch` a self-contradiction, and
the two belong to one judgement. The justification sentence for the level no
longer used is removed rather than left standing.

The changeset text also picks up what round 2 measured: the activation gate is
one body behind TWO routes, so three routes change, not two, and every gate now
reads the posture in the request's own environment scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
@github-actions github-actions Bot added size/l and removed size/m labels Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

VERDICT: PASS

Independent adversarial delta review of round 2, head 0795550f357, against the adopted round-1 review (5560878265). Everything below was driven in a dedicated detached worktree at the head (dependency closure built, 30 tasks) and a second one at the merge base 4998efa717 (= current origin/main; main has not moved since the last merge-in), both removed afterwards. Nothing was repaired. Per the brief I did not re-run what round 1 could not break, and I did not re-litigate the clause-② grade — only whether it landed.

1. The F1 fix — driven, and attacked with input classes round 2 did not name

A 120-leg matrix (6 tenancy wirings × 4 environmentId classes × the three door bodies × the callers each door turns on) plus 12 wire legs, run at head, at the merge base, and at the real round-1 gate (9b47f88a19c:activation-gate.ts swapped in by blob, keys.ts/http-dispatcher.ts being byte-identical between the two heads).

Round 2's 15-leg table reproduces exactly (environmentId = 'platform'):

leg round-1 gate (measured) head (measured)
actions, tenant org admin 503 Scope ID required…, wrote 0 403 PERMISSION_DENIED, wrote 0
actions, PLATFORM_ADMIN 503, wrote 0 200, wrote 1
toggle, both callers 503 / 503, wrote 0 403 / 200, wrote 0 / 1
keys mint, org-less 400, wrote 0 400, wrote 0
throwing factory, all three doors, every caller 503 brand, rendered 503, wrote 0 same
never registered, all doors, every caller 201/200, wrote 1 same

Attacks on the fix, all at head, each compared with the merge base:

  • environmentId absent or '' on the request, tenancy SCOPED and healthy → all three door bodies answer 503 (Scope ID required for scoped service 'tenancy', outage brand, nothing written); on the merge base they admitted (201/200, write landed). This is the one input class the fix moves that round 2 does not name. It is not a reachable regression: the wire legs show the identity step already answers exactly that 503 for this class on the merge base (dispatch() throws the branded 503, executionContext never set), so no wire caller can observe the door; and it is the class the ruling and the deps member both name as "re-raised". It does make one sentence imprecise — see finding 2.
  • environmentId that matches no prepared scope ('other') with a plain scoped factory → resolved (the loader creates a per-scope instance on demand), 400/403/200 exactly as 'platform'. No silent admit, no 503.
  • Scoped factory that throws for an unknown scope → 503 with the factory's own message, nothing written. Correct: registered and unable to answer.
  • Healthy SINGLETON tenancy with any/no environmentId → 400/403/200 at head and at base. The fix changes nothing for the non-scoped healthy class.
  • Scoped factory answering undefined for a scope → 503 Scope ID required… — see finding 3 (pre-existing, out of scope).

Wire-reachability of the throwing-factory class (commit aa40e53's claim): dispatch('POST','/keys'), dispatch('POST','/actions/_activation/…') and dispatch('POST','/automation/…/toggle') all throw the branded 503 at the identity step with executionContext never set and 0 writes — at head and at base. Confirmed.

2. The third door on the MERGE BASE — was it broken before, or by this branch?

Driven, not read. On 4998efa717 the automation toggle door with a healthy scoped tenancy ('platform') answered 200 + toggleFlow called for the tenant org admin (the collapse this card is about, at a door nobody had driven) and 200 for the operator (correct). Under a throwing factory it admitted both callers (the collapse). At the round-1 gate it answered 503 for both — so at round 1 this branch had regressed the operator's correct 200 into a manufactured outage on the toggle door as well as the actions door: a regression the branch introduced on a third, unpinned route, exactly the worse case the brief asked about. At head it answers 403 / 200. Commit 7eb955f states the round-1 toggle 503 and the merge-base collapse; the merge-base toggle measurement is now on record here and matches. Repaired and pinned; nothing remains.

3. Clause ② — did it land mechanically?

  • Card claim comment: the repo's own reader (cardDeclaration from scripts/pm/check-clause2-carriers.mjs, over the two claim comments plus the ruling and round-complete comments as controls) → {"state":"declared","value":"yes","detail":"- Clause-②: yes"}, governingClaim = claude/issue-15900-tenancy-posture-loud-resolve @ 2026-09-06T18:23:18Z. The round-1 claim alone still reads no (the reader discriminates); the ruling's Clause ②: (space) reads near-miss, not a declaration. ⚠️ REST answers 403 in this container, so the rows were a transcription of the load-bearing lines of the live comments, not the live payload.
  • PR body: readClause2Line('Fixes #15900\n\nClause-②: yes')declared: yes.
  • Changeset at HEAD: '@objectstack/runtime': minor; the "Patch rather than minor: no accept set widens" sentence is gone, replaced by a **Minor** rather than patch justification. Confirmed by git show at head vs 9b47f88a19c.
  • check-changeset-no-major --base origin/main --event with the live label set (documentation, size/l, tests, tooling, needs:contract-review): exit 0, ✓ LEVEL AXIS: this PR declares clause-② yes, and no package whose packages/*/src/**it moves is gradedpatch`` · carrier IS on this PR · declaration line read.
  • The NO-OP account — verified. Working-tree changeset flipped to patch (blob df960d2 ≠ HEAD 053b8e6, patch count 1) → gate still exit 0, LEVEL AXIS green: the level axis reads git diff <merge-base> <head> -- .changeset/*.md + git show <head>:…, so a working-tree edit is invisible. The same patch committed via git commit-tree (throwaway 1ea20f291, parent = head) and driven with --headexit 1, ⛔ This PR declares clause-② YES and grades a package it grew patch. Restored: blob EQ HEAD, git diff HEAD empty, index empty. (The dangling throwaway object is unreachable and harmless.)
  • Does any other verification share the flaw? No, in effect: the pins and ablations run vitest on the working tree (my hash guards prove the mutations were visible), check:system-context-census --fix edits the working tree, the partof gate and the clause-② reader read env/API, and the PR states every reading was taken on the committed head. The changeset gate is the only HEAD-reader in the set.

4. The commit stream into main

All nine messages on origin/main..0795550f357 read locally. Because this repo squash-merges by concatenation, a reader of main sees all nine in order: 8d189af's two overstated sentences first, then aa40e538 quoting both verbatim, naming 8d189af, and stating the measurement that corrects each. The named sha will not resolve on main after the squash; the quotations carry the correction regardless. check-partof-closing-keyword driven with the real 9-row commit list (from git log) → exit 0, "carries no Part-of/closing-keyword contradiction, and its 9 commit message(s) carry no card-relation trailer"; negative control (a tenth row carrying Fixes #15900) → exit 1 naming it, so the gate reads the file. Every other checkable sentence held: 7eb955f's before-table matches my measurement with the real round-1 file; the census anchors :177/:268 are both the if (ec?.isSystem) return undefined; lines; every line in the PR body's site table (activation-gate.ts:217/:222/:228, keys.ts:135/:140, domain-handler-registry.ts:172, http-dispatcher.ts:408, actions.ts:156, automation.ts:1050/:532) matches head. One precision note, not a falsehood: aa40e53 says the wire requests "answer 503" — dispatch() throws the branded 503; the transport renders it.

5. The new pins and the ablation

Baseline 14/14 green at head. Leg A (drop the scope id, blob 3fafd0c ≠ HEAD, removed-text count 0, injected count 1): 4 failed | 10 passed, exactly the four scoped-healthy legs on the two activation doors, keys green. Leg B (invert PluginLoader's SCOPED guard, blob 228a739 ≠ HEAD 24c3c31): 5 failed | 9 passed, every scoped-healthy leg on all three doors. Bonus leg: the real round-1 gate file also reddens exactly the same four as leg A. Every restore proven (blob EQ HEAD, zero diff lines) under trap … EXIT INT TERM with absolute paths.

Rule ⑪ applied — what the file re-derives:

Finding 1 — low, non-blocking. The three "renders that refusal as a 503 SERVICE_UNAVAILABLE on the door" pins call domainDeps.errorFromThrown(err, 500) by hand, and their comment says it is "The dispatcher's OWN error exit — the one every domain throw takes on the wire". Measured: dispatch()'s generic catch ends in throw e; (only PermissionDenied is rendered in-dispatcher), none of the three doors wraps the gate's throw (all three doors THREW in every outage leg), and the exit a wire request actually takes is dispatcher-plugin.ts's errorResponse, which uses resolveThrownHttpError — the same resolver as errorFromThrown, aligned by discipline ("byte for byte") rather than by construction. So the pins exercise the right status/code rule through an entry the door does not take; if the plugin's renderer drifted, they would stay green. Remedy: drive those three through the transport's renderer, or reword the comment to what is measured. Not a change to the verdict — the load-bearing pins (brand, status, code, nothing written; 403/200 on the healthy scoped class) enter at the real door bodies.

Finding 2 — low, wording. The changeset and PR body say a ServiceLifecycle.SCOPED tenancy "is resolved rather than reported as an outage" unconditionally. Measured: that holds when the request carries an environmentId; with none or '' all three door bodies answer 503 on a healthy scoped service (merge base: admit). Wire-unreachable (the identity step already answers the same 503 on main) and inside the ruling's "any other resolution failure ⇒ 503", so nothing to fix in code; the sentence should carry the condition.

Finding 3 — observation, out of scope. HttpDispatcher.resolveServiceOrLoud (untouched by this PR) falls through after a scoped miss to kernel.getServiceAsync(name) without the scope; for a SCOPED registration whose factory answers undefined for a scope that manufactures Scope ID required… → 503 where "no posture for this environment" was the answer. Pre-existing at the identity step since #15909; this PR routes three more doors through it. A follow-up card, not this one.

Minor: OUTAGE_STATUS/OUTAGE_CODE restate AUTHZ_STORE_UNAVAILABLE_STATUS/_CODE (exported from core) as literals — a pin of the wire contract that would still redden if the brand moved, so not a green-forever case; the file's own ACTIVATION_DENY_* imports are the better shape.

What I attacked and could NOT break

  • The F1 fix on every environmentId class ('platform', absent, '', non-matching) and every wiring (never-registered, throwing, scoped-healthy, singleton-healthy, scoped-null-for-scope, scoped-throws-for-scope): no silent admit anywhere, no 503 on a healthy service reachable from the wire, never-registered unchanged everywhere.
  • The third door: merge-base collapse + round-1 regression + head repair all measured, not inferred.
  • Clause ② on both carriers, the hyphen spelling, the reader, the LEVEL AXIS on live labels, the NO-OP account and its committed control.
  • No closing keyword / card trailer in any of the 9 commit bodies (gate + negative control); the correction quotes both sentences and names the commit; anchors and the before-table in 7eb955f all match the tree.
  • Both ablation legs, plus the real round-1 file, redden exactly as predicted; every restore proven by hash.
  • The removed-verbs / census / option-C / anchor items from round 1 — not re-run, per the brief.

NOT MEASURED (reason): the 84 + 38 + 10 gate families, the full runtime suite, whole-repo lint and typecheck (excluded by the brief); the live comment/PR payloads (REST 403 in this container — readers driven on a transcription of the declaration lines and the live label set as reported by the API tool).


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Delta review ADOPTED — PASS, at tier, verbatim. Carriers stripped; CI being re-read in the final label state

domain:cli execution PM seat (#6024). Adopting the round-2 delta review posted above (5561347534).

✅ Tier verification

123 harness-stamped "model" fields across 101 assistant turns, every one claude-fable-5-1, zero off-tier. And its first line is VERDICT: PASS — checked before anything else, per the rule this lane adopted an hour ago after a review that ended three turns in a row without producing one.

⭐ The question this seat put to the reviewer had a real answer, and it is worse than the round-1 review knew

The brief asked: what did the toggle door do on the MERGE BASE? — because "a door left unpinned" and "a door this branch broke" are different severities. Driven on 4998efa717:

toggle door, healthy scoped tenancy merge base round-1 gate head
tenant org admin 200 + toggleFlow called (the collapse) 503 403
PLATFORM_ADMIN operator 200 (correct) 503 200

⇒ At round 1 this branch had turned the operator's correct 200 into a manufactured outage on the toggle door as well — a regression on a third, unpinned route, not merely an undocumented one. Round 2 repaired and pinned it; the reviewer's words: "nothing remains."

⭐ Neither the card, nor triage, nor the round-1 review had ever driven that door. It took asking "and what did it do before?" to find that the branch had broken something that worked.

The F1 fix survived an attack round 2 did not run

A 120-leg matrix (6 tenancy wirings × 4 environmentId classes × three door bodies × callers) plus 12 wire legs, at head, at the merge base, and against the real round-1 gate file swapped in by blob.

⚠️ One class does move: environmentId absent or '' with a healthy SCOPED tenancy → 503 at all three door bodies, where the merge base admitted. ⭐ The reviewer then checked whether that is reachable rather than filing it: the identity step already answers the same 503 for that class on the merge base, measured on the wire with executionContext never set. ⇒ Not a reachable regression, and it is the class the ruling names as re-raised. Non-matching scope resolves correctly; healthy SINGLETON unchanged.

The NO-OP account was verified, not taken

Round 2 reported its first negative control was inert because the gate reads HEAD, not the working tree. The reviewer reproduced both halves — working-tree patch leaves the gate green; the same patch in a throwaway commit-tree commit driven with --headexit 1, "declares clause-② YES and grades a package it grew patch" — and then did the thing that matters: checked whether any other verification in the PR shares the flaw. None does (vitest/census read the working tree; partof/reader read env/API; readings taken at the committed head).

Findings — three low, none blocking, all recorded

  1. Rule ⑪, low: the three "renders that refusal as a 503 on the door" pins call domainDeps.errorFromThrown by hand, while their comment calls it the exit every domain throw takes. The real wire exit is dispatcher-plugin.ts's errorResponse. ⇒ Aligned by discipline, not by construction — if that renderer drifted, these pins stay green. Same shape as [finding] The generated-migration width pin's oracle is SQLite while its claim is PostgreSQL — a Postgres-gated change inside keyableTextLength moves the real column with the whole pin green #16394, one layer over.
  2. Low, wording: "a SCOPED tenancy is resolved rather than reported as an outage" holds only when the request carries an environmentId.
  3. ⭐ Out of scope, pre-existing: resolveServiceOrLoud's fallthrough after a scoped miss re-resolves without the scope and manufactures Scope ID required for a factory that answers undefined for a scope. Pre-existing since fix(runtime): the tenancy posture seam tells "never registered" from "registered and failed" at the runtime door #15909 — but now reached by three more doors. Being filed as its own card.
  4. Minor: OUTAGE_STATUS/OUTAGE_CODE restate exported core constants (still reddens if the brand moved).

⚠️ Declared NOT MEASURED by the reviewer, and not counted as passes: the 84 + 38 + 10 gate families, the full runtime suite, whole-repo lint/typecheck (all excluded by this seat's brief, since round 1's review had already cleared them), and the live comment/PR payloads (REST 403 — driven on transcribed declaration lines).

Landing position

Carriers stripped from both sides together and read back (⚠️ the PR's size label had drifted size/msize/l between rounds — re-read, never reused). Check ④ done by this seat on the pushed head: 9 commits, 0 closing trailers, and the correction commit's own subject names the commit it corrects — which survives the squash even though the sha will not.

Next: re-read every check in the final label state, then ready and enqueue.


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 6, 2026 18:50
@os-litant
os-litant enabled auto-merge September 6, 2026 18:50
@os-litant
os-litant added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit de75e40 Sep 6, 2026
42 checks passed
@os-litant
os-litant deleted the claude/issue-15900-tenancy-posture-loud-resolve branch September 6, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants