fix(rest): keep "failed" and "not wired" apart at the two computeExecCtx authorization-input seams - #15020
fix(rest): keep "failed" and "not wired" apart at the two computeExecCtx authorization-input seams#15020os-trump wants to merge 6 commits into
Conversation
…thz-input seams Phase 2 of the #13906 measurement, implementing the maintainer ruling of 2026-09-02 (decision 1 = A + B', decision 2 = B). Both seams measured fail-OPEN: an absorbed FAILURE read as "this check does not apply", so an authorization refusal was skipped rather than produced. - tenancy posture (A): absorb only the registry's branded not-registered rejection; any other rejection raises AuthzStoreUnavailableError, the same loud answer wiredEngineOrLoud gives the engine seam. The wiring fact comes from `kernel`'s presence, never inferred from the returned value. - single-kernel wiring (B'): a configured wall-enforcing posture is refused loudly at boot, because that wiring never reads a posture at all. - ADR-0069 auth gate (B): fail closed in the measured window only — isAuthGateActive() answered true AND the session re-read then failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…e B' The six phase-1 pins that recorded the PERMISSIVE answers go red by design under the repair; each is inverted IN PLACE with its superseded text quoted beside it, per the file's own standing instruction. Adds rest-api-plugin-tenancy-posture-boot-refusal.test.ts, which drives the ruling's own opening question for B' (can a walled posture be configured on the single-kernel wiring at all? yes) plus the refusal and four narrowness controls, and a narrowness control for decision 2 B. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…ne double Two follow-ons from driving the repair against the whole @objectstack/rest suite: - LiteKernel/KernelBase-shaped hosts have no `getServiceAsync`, so the bare dereference would raise an unbranded TypeError and turn "this host shape has no async registry" into a 503. The wiring fact now includes the accessor's presence, mirroring the shipped objectQLProvider's split. - ui-view-environment-ownership.test.ts's kernel double spelled ABSENT services as a bare Error. The real registry brands the never-registered rejection and reserves the unbranded one for a service that failed to construct, so the double was claiming every absent service had broken. It now resolves undefined, the spelling the seam contract itself names for absence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…ensus check:doc-authoring refuses `#NNNN` inside runtime prose (maintainer ruling 2026-08-12) — an operator reading a boot failure cannot resolve one. The ids move to adjacent source comments and the boot message asserts its REMEDY instead. check-system-context-census --fix re-anchors ten line citations in content/docs/permissions/system-context.mdx that this PR's insertions shifted. Pure line rot; no elevation behaviour changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
📓 Docs Drift CheckThis PR changes 1 package(s): 2 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 — 13 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 5c3d4ce242dd976ab8db3a66e78d2833e0b4fa4f && git checkout 5c3d4ce242dd976ab8db3a66e78d2833e0b4fa4f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6665c5c916caf6d95022734328ae82154e1a5f10 9de1b0ef34b25931ee63c828ce1db5d5a2652e24 && git checkout -B drift-repro 6665c5c916caf6d95022734328ae82154e1a5f10 && git merge --no-ff 9de1b0ef34b25931ee63c828ce1db5d5a2652e24
node scripts/docs-audit/affected-docs.mjs --json 6665c5c916caf6d95022734328ae82154e1a5f10
|
…mputeexecctx-seams
The merge deferred the generated `content/docs/permissions/system-context.mdx` to the merge driver (AGENTS.md §11); this commit discharges it by regenerating from the merged tree with the gate's own `--fix`. Line re-anchoring only — 20 anchors re-pointed, no elevation behaviour touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Fixes #13906
Phase 2 of this card: implements the maintainer ruling of 2026-09-02 (director seat, summon #8 — decision 1 = A + B′, decision 2 = B). Phase 1 was the measurement, landed separately as PR #14272.
The direction, driven before anything was changed
The card was filed as a code reading with no direction measured. Driven on today's tree, every fault leg beside a positive control that is the same fixture with the one fault removed:
Both seams are fail-OPEN. The seam's value is absent, but the consumer reads absent as "this check does not apply" — so a refusal is skipped and an authorization input is decided in the caller's favour. That is the security direction, and it is what separates this card from #13476 / #13904, where an unknown became an extra refusal.
Interim reading posted on the card before any code push: comment
5528512501.What changed, and what each row measures
isolatedtenancy, ex-member's org-stamped API keytenancynever registered (supported no-tenancy composition)tenancyregistered and FAILED to constructisAuthGateActive()itself THROWSDecision 1 A — tenancy posture. Only the registry's branded not-registered rejection is absorbed (
isServiceNotRegisteredError, the discriminator the shippedobjectQLProvideralready uses one layer down). Every other rejection raisesAuthzStoreUnavailableError, the same loud answerwiredEngineOrLoudgives the engine seam, carried by the same nets. The classification is the registry's, never message text. The wiring fact comes from the kernel's presence, never inferred from the returned value.Decision 1 B′ — single-kernel wiring.
computeExecCtxreads the posture off a localkernelthat only the kernelManager branches assign, so on the shipped single-kernel provider wiring the posture is never asked for — measured with a recording factory, invocation count 0, no failure required. There is no posture to repair at request time, so the plugin now refuses to start in exactly that composition. ⛔ Option B (wiring a tenancy provider into that path) was not taken.Decision 2 B — ADR-0069 auth gate. Fails closed in one precisely measured window:
isAuthGateActive()answeredtrueand the gate's session re-read then failed. The re-read deliberately bypasses the swallowinggetSessionclosure, because that closure re-collapses the same two facts.Blast radius of B′, measured rather than assumed
B′ cannot fire on the open or dev path.
plugin-authis the only production registrar of atenancyservice, and itsposturegetter returnsisolationActive() ? requestedPosture : 'single'— without the enterprise multi-org runtime installed and entitling the posture, it reportssingle, andpostureEnforcesWall('single')is false. So the refusal reaches only a deployment that genuinely runs a wall and cannot enforce it, which is the ruled intent. Four narrowness controls pin this.File face
packages/rest/src/rest-server.tspackages/rest/src/rest-api-plugin.tspackages/rest/src/execctx-authz-input-seam-reachability.test.tspackages/rest/src/rest-api-plugin-tenancy-posture-boot-refusal.test.tspackages/rest/src/ui-view-environment-ownership.test.tscontent/docs/permissions/system-context.mdx--fix.changeset/execctx-authz-input-seams-fail-closed.mdThe two non-obvious ones, declared rather than slipped in:
ui-view-environment-ownership.test.ts— its fake kernel spelled ABSENT services as a bareError. The real registry brands the never-registered rejection and reserves the unbranded one for a service that IS registered and failed to construct, so the double was claiming every absent service had broken. Under the old collapse that inaccuracy was free; it is not free now. The double now resolvesundefined, the spelling the seam contract itself names for absence. Caught by running the whole package suite, and attributed with a control: on BASE source that file passes 6/6, so the reds were mine.content/docs/permissions/system-context.mdx— this PR's insertions shifted line numbers thatcheck-system-context-censusanchors. Repaired with the gate's own--fix; ten anchors re-pointed, no elevation behaviour touched. Notcontent/docs/releases/.Red-first proof
The six phase-1 pins that recorded the permissive answers go red by design under the repair, and each is inverted in place with its superseded text quoted beside it, per that file's own standing instruction. Their reds were the negative arms, not a broken harness — the positive controls (healthy tenancy 401, active gate 403, inactive gate admitted,
unregisteredstill 200) all stayed green in the same run, and the auth-gate red wasAuthzStoreUnavailableError { status: 503, object: 'auth_gate' }.B′'s guard got its own ablation. Neutering only the refusal branch turned the 2 refusal tests red and left all 6 controls green (
expected true to be false— the plugin booted); restore verified bygit diff HEADempty and blob hash identical to the HEAD blob. The mutation was proven on disk before the run and the restore proven after, both by hash.Verification
All heavy runs through
scripts/pm/os-verify-lock.sh(slotissue-13906); exit codes captured before any pipe. Union re-run on final commit34a1fbce8c:pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2— Test Files 176 passed (176) / Tests 2943 passed (2943)pnpm --filter @objectstack/rest typecheck— green on both halves, and the test layer is genuinely measured:check:test-typecheck: OK — @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.jsonnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackderived 62 families on the final tree (24 of them appeared only after the docs file entered the diff — the re-derivation earned its keep). 59 green by their own printed verdict lines; the other 3 are NOT MEASURED, not red, each saying so itself:check-test-completenessandcheck:dual-build-cjs-loadsreportPREREQUISITE NOT MET(they read a CI-supplied test log / built dist), andcheck-half-statesreports the same because this container's repo-scoped reads are refused.check:skill-examplesfirst reportedPREREQUISITE NOT MET(client-react unbuilt). Building its closure and re-running gave a real green — recorded as measured, not assumed.Notes for the reviewer
computeExecCtx, which still never reads a posture there.AuthzStoreUnavailableErroris reused rather than a new error class introduced —packages/rest/src/error-response.tshas a single writer (rest:classifyDataError's structured-code arms ship the QuickJS debug wrapper aserrorfor a sandboxed producer — the mirror of #11588, on the door #11588 did not touch #14704) and is untouched here, so reusing the ruled 503 vehicle was the only in-fence option as well as the right one.packages/spec(read only, forpostureEnforcesWall),packages/core(read only), the [finding] resolveExecCtx 的.catch(() => undefined)把执行上下文解析失败静默降级为「无上下文」— 该行为在包管理门上可达什么错误状态,未测 #13255 auth-service andgetSessionseams, and the settings/localization seam.isAuthGateAllowlistedwould have exempted, so a gated user's remediation routes are unreachable during a transient session-backend fault. The ruling named exactly two conditions and an allowlist carve-out is new policy on the manual floor, so I did not invent one.🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code