Skip to content

fix(rest): keep "failed" and "not wired" apart at the two computeExecCtx authorization-input seams - #15020

Open
os-trump wants to merge 6 commits into
mainfrom
claude/issue-13906-computeexecctx-seams
Open

fix(rest): keep "failed" and "not wired" apart at the two computeExecCtx authorization-input seams#15020
os-trump wants to merge 6 commits into
mainfrom
claude/issue-13906-computeexecctx-seams

Conversation

@os-trump

@os-trump os-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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

wiring before after
healthy isolated tenancy, ex-member's org-stamped API key 401 refused 401 — unchanged
tenancy never registered (supported no-tenancy composition) 200 served 200 — unchanged
tenancy registered and FAILED to construct 200 served, full grants 503
auth gate INACTIVE admitted admitted — unchanged
auth gate ACTIVE, healthy re-read, gated user 403 403 — unchanged
isAuthGateActive() itself THROWS admitted admitted — unchanged
auth gate ACTIVE, session re-read FAILS admitted, no wire trace 503

Decision 1 A — tenancy posture. Only the registry's branded not-registered rejection is absorbed (isServiceNotRegisteredError, the discriminator the shipped objectQLProvider already uses one layer down). Every other rejection raises AuthzStoreUnavailableError, the same loud answer wiredEngineOrLoud gives 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. computeExecCtx reads the posture off a local kernel that 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() answered true and the gate's session re-read then failed. The re-read deliberately bypasses the swallowing getSession closure, 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-auth is the only production registrar of a tenancy service, and its posture getter returns isolationActive() ? requestedPosture : 'single' — without the enterprise multi-org runtime installed and entitling the posture, it reports single, and postureEnforcesWall('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

file why
packages/rest/src/rest-server.ts both seams (the ruled repair)
packages/rest/src/rest-api-plugin.ts B′ boot refusal
packages/rest/src/execctx-authz-input-seam-reachability.test.ts the six phase-1 pins, re-aimed
packages/rest/src/rest-api-plugin-tenancy-posture-boot-refusal.test.ts new — B′ and its controls
packages/rest/src/ui-view-environment-ownership.test.ts fixture triage, explained below
content/docs/permissions/system-context.mdx line re-anchoring only, by the gate's own --fix
.changeset/execctx-authz-input-seams-fail-closed.md user-visible behaviour change

The two non-obvious ones, declared rather than slipped in:

  • ui-view-environment-ownership.test.ts — its fake kernel spelled ABSENT services as a bare Error. 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 resolves undefined, 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. ⚠️ I could not enumerate open-PR file holdings from this seat (repo-scoped REST is refused for this session and a wide MCP scan is banned); the last landing on that file, fix(rest): require the resolved environment to belong to the caller at GET /ui/view/:object/:type #13625, is merged.
  • content/docs/permissions/system-context.mdx — this PR's insertions shifted line numbers that check-system-context-census anchors. Repaired with the gate's own --fix; ten anchors re-pointed, no elevation behaviour touched. Not content/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, unregistered still 200) all stayed green in the same run, and the auth-gate red was AuthzStoreUnavailableError { 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 by git diff HEAD empty 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 (slot issue-13906); exit codes captured before any pipe. Union re-run on final commit 34a1fbce8c:

  • pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2Test 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.json
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derived 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-completeness and check:dual-build-cjs-loads report PREREQUISITE NOT MET (they read a CI-supplied test log / built dist), and check-half-states reports the same because this container's repo-scoped reads are refused.
  • check:skill-examples first reported PREREQUISITE NOT MET (client-react unbuilt). Building its closure and re-running gave a real green — recorded as measured, not assumed.

Notes for the reviewer

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…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
@github-actions github-actions Bot added the size/l label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authentication.mdx (via createRestApiPlugin (symbol, a top-level function))
  • content/docs/plugins/packages.mdx (via createRestApiPlugin (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 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 — 13 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 6665c5c916caf6d95022734328ae82154e1a5f10packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5c3d4ce242dd976ab8db3a66e78d2833e0b4fa4f — the merge of head 9de1b0ef34b25931ee63c828ce1db5d5a2652e24 into base 6665c5c916caf6d95022734328ae82154e1a5f10, 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 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

⚠️ 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 6665c5c916caf6d95022734328ae82154e1a5f10 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026
@os-trump
os-trump marked this pull request as ready for review September 3, 2026 17:46
@os-trump
os-trump enabled auto-merge September 3, 2026 17:46
@os-trump
os-trump disabled auto-merge September 3, 2026 20:20
@os-trump
os-trump enabled auto-merge September 3, 2026 20:20
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
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