Skip to content

refactor(service-settings): the settings door calls the one request-to-ExecutionContext composition, resolveRequestExecutionContext - #24297

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23217-s3-settings-door
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23217-s3-settings-door

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #23217

Clause-②: no
Ruling-ref: 6227928430

Slice 3 of #23217 under ruling record 6227928430 (letter A): the settings door, SettingsServicePlugin's verifiedContextFromRequest in packages/services/service-settings/src/settings-service-plugin.ts, calls the one resolveRequestExecutionContext that slice 1 landed in @objectstack/core. The card stays open for its two remaining doors, service-storage (slice 4) and plugin-hono-server (slice 5). @objectstack/core's function is not touched: every settings difference is one of its existing named parameters, so the letter-B fallback (item 5) does not arise.

Basis for the Clause-②: no line: the ruling's "later slices Clause-②: no". The settings door's outward answers are unchanged (the ten-row pin below and a 29-wiring fault probe read the same on both trees), and no export moves in @objectstack/service-settings. The one new method, tenancyServiceResolver, is private.

What changed

The door

verifiedContextFromRequest keeps its door facts in their old order and calls the function once. Each parameter was measured on the hand-written door at 1e001c7df before converting:

parameter settings passes what the hand-written door did
anonymous 'fail-closed' An unresolved request became a context with no userId, which the family's anonymous floor refuses with 401. The function now yields no context and the door answers its existing fail-closed { enforced: true }: the same floor, the same 401.
authGate 'resolved' It carried authz.authGate from the read that admitted the caller. There was no re-read, and isAuthGateActive() was never asked.
credentials 'api-key-and-session' The request's headers went to resolveAuthzContext, so the API-key door was open.
sessionApi 'optional' The session read was lazy and swallowed every failure, so a key admitted without any session read.
session 'request' api.getSession(inProcessSessionReadInput(headers)) (.api, else getApi()).
tenancy the door's async-registry thunk, or undefined on a host with none resolveAdmissionTenancyPosture: getKernel().getServiceAsync('tenancy'), classified by classifyAdmissionTenancyPosture.
localization 'withhold' It read none.
requestLocale undefined It read none.
accessToken 'withhold' Slice 1's table did not cover it, so it was measured: SettingsContext declares no bearer field, and the door's projection never read authz.accessToken. The bearer never left the door, so 'withhold' also keeps it off the intermediate envelope.
oauth, onAdmission undefined none

What stays at the door, in its old order:

  • the engine lookup, first, inside the fail-closed try;
  • the header normalisation;
  • the auth-service lookup, where a lookup that throws counts as no auth service;
  • the tenancy wiring, now a resolver thunk (tenancyServiceResolver). resolveAdmissionTenancyPosture, the settings service's own posture read, classifies that same thunk as before;
  • the projection to SettingsContext: userId, tenantId, system capabilities and grants as one list, enforced: true, and authGate when one holds;
  • the catch that re-raises the store-outage and API-key-lifecycle brands and otherwise fails closed.

One ordering moved: the auth-service lookup now runs before the tenancy read (it used to run after). Both are registry lookups whose throw the door swallows, so no answer can tell the two orders apart. The probe below reads the same 29 answers on both trees.

Readers moved in the same PR

  • scripts/check-single-authz-resolver.mjs: DELEGATORS gains the settings row (resolveRequestExecutionContext), the seat's decision recorded in the claim. The gate answers "all 4 entry points delegate"; --self-test exit 0.
  • packages/core/src/security/authz-store-unavailable.test.ts: the settings row of TRANSPORT_LEDGER is out. The door no longer calls the resolver itself, and the comment on the composition's row now names settings as its third caller.
  • packages/runtime/src/security/request-execution-context.conformance.test.ts: a settings row and a settings CONTROL (details below), with @objectstack/service-settings added to runtime's devDependencies and pnpm-lock.yaml regenerated by pnpm install (+3 lines).
  • Outside the claim's declared surface: packages/runtime/vitest.config.ts gains an anchored alias for @objectstack/service-settings to its src/index.ts. Without it, the new row would import the settings door from its built dist/, and check:test-source-alias requires a test to read a workspace dependency's source, not its build (the comment in the config says so). With it, the gate exits 0.
  • Outside the claim's declared surface: scripts/slot-lookup-baseline.json changes the settings door's count from 2 to 1. The deleted getSession closure held one of the file's two service lookups erased to any (const authService: any = ctx.getService('auth')). check:slot-lookup fails on a decrease too ("ratchet DOWN: run --update"), and pnpm check:slot-lookup --update wrote this one-line change.

The ruling's one ADR-0096 / ADR-0069 pin

packages/services/service-settings/src/settings-routes.request-context-door.pin.test.ts has ten rows on the real listing route, GET /api/settings. The plugin is booted for real with no seam captured, and each row reads the context that reached the settings service:

row answer
ADR-0096: no credential 401 UNAUTHENTICATED, service never asked
ADR-0096: a cookie the auth service does not recognise 401
ADR-0096: a session read that fails (it resolved no caller: the floor, not an outage) 401
ADR-0096: an ex-member's organization-stamped key under the walled posture, a healthy cookie beside it 401; the cookie never answers for the refused key
ADR-0069: a held session 403 PASSWORD_EXPIRED with its message
ADR-0069: the same held session while the gate probe says no gate is active 403; the posture is the admitting read's
ADR-0069: a key whose owner holds an account-scope posture 403 MFA_REQUIRED with its message
ADR-0069: a key beside the held cookie 200 as the key's owner, no authGate
CONTROL: an ungated session 200 as that principal, with tenant, enforced, no authGate
CONTROL: an API key on a host with no auth service 200 as its owner

It was committed first, on the hand-written door (78e9382c6, door blob 7c72080fa): Tests 10 passed (10). It is green after the conversion too.

The conformance row

The settings door hands on a projection, SettingsContext, not the context. So the row declares that projection (project), and the door's answer must equal the projection of the function's context under the row's parameters. Its identity facts are userId, tenantId and permissions. The other two rows are unchanged: their entries are now named constants, typed by what each door hands on (DoorRow is generic over it), which re-indents them.

The drive goes through the door's public entry: the real plugin booted on a plugin context over the world's services, and one GET /api/settings. The door returns its context to no caller. The family door's first read of it is the ADR-0069 check, evaluateAuthGate(context, ''), which runs on every route and every request ahead of the anonymous floor, so the drive reads the context there. It does so through a pass-through mock of that one @objectstack/core export, which records its argument and answers exactly as the real export does.

The settings CONTROL shows that the requests tell apart:

  • anonymous: the guest entry would have handed on a different context;
  • authGate: 'reread-when-active' drops the posture when no gate is active;
  • credentials: 'session-only' admits nobody here;
  • tenancy: no posture admits the ex-member's key;
  • sessionApi: 'required' admits nobody with no auth service;
  • and the projection's union: the key owner's system capability export_reports reaches permissions.

What no request can tell apart at this door: the projection carries no locale, time zone or bearer, so localization, requestLocale and accessToken cannot be told apart here. The row declares the values the door passes, and the file's header says so.

Measurements (the dispatch's mechanism assumptions)

  • A1 (parameters): held, row by row, as tabled above. accessToken was measured as 'withhold'.

  • A2 (source-text pins): falsified. No test in @objectstack/service-settings reads the door's source text. Its one readFileSync pin (value-domains.shared-predicate.pin.test.ts) reads value-domains.ts. Nothing was re-aimed.

  • A3 (bare-term censuses): I took an identifier-token census of the door file at 1e001c7df and at the conversion. 182 distinct tokens changed count. Code identifiers among them: resolveAuthzContext 4 to 2 (both left in comments), resolveRequestExecutionContext 0 to 3, getSession 3 to 0, inProcessSessionReadInput 2 to 0, userAuthGateSourceOf 2 to 0, apiKeyLifecyclePolicySourceOf 2 to 0, getApi 2 to 0, getService 9 to 8, any 13 to 10, tenancyServiceResolver 0 to 5. Three readers count these over this file: TRANSPORT_LEDGER (resolveAuthzContext({), DELEGATORS (resolveRequestExecutionContext) and the slot-lookup ratchet (the any-erased lookup). All three moved in this PR. The dogfood authz-probe-blind-spot census reads no settings file.

  • A4 (ordering): the auth-service lookup moved ahead of the tenancy read, and this door has no early session-API check (sessionApi: 'optional'). A one-off probe, never committed, drove 29 wirings through GET /api/settings: the hand-written door at door blob 7c72080fa, then the converted door at 1d3595a69. All 29 answers are identical. The double-fault rows:

    wiring before after
    tenancy outage + auth lookup throws + session 503 503
    tenancy outage + session read throws 503 503
    tenancy outage + no session API + key 503 503
    tenancy outage + getApi() rejects + session 503 503
    tenancy outage + a revoked key 503 503
    tenancy outage + engine lookup throws 401 401
    tenancy outage + kernel accessor throws 401 401
    engine read rejects + auth lookup throws + key 401 401
    revoked key + engine lookup throws 401 401

    The single-fault and healthy rows are identical too: healthy session or key 200; no credential 401; a revoked key 403 API_KEY_REVOKED; no session API, getApi() rejecting or no auth service with a key 200 and with a cookie 401; an engine read that rejects on a session 503.

  • A5 (readers): held. Adding the runtime devDependency creates no cycle: the transitive closure of @objectstack/service-settings (17 workspace packages over dependencies, devDependencies, peer and optional) never reaches @objectstack/runtime.

  • A6: held (@objectstack/core: workspace:*).

Evidence

  • Head: 6328d1141 (origin/main 9822b053e merged in).
  • @objectstack/service-settings: Test Files 49 passed (49), Tests 871 passed (871). typecheck exit 0, and its program includes the new pin (tsc --listFiles).
  • @objectstack/core: local project Test Files 104 passed (104), Tests 2551 passed (2551). The repo-project ledger authz-store-unavailable.test.ts: Tests 38 passed (38), slice 2's 39 minus the settings row. typecheck exit 0, test layer 4 / 4 held.
  • @objectstack/runtime: local project Test Files 416 passed (416), Tests 6558 passed | 19 skipped (6577). typecheck exit 0, test layer 27 files / 190 errors / 68 signatures held. The conformance test, verbose: Tests 10 passed (10): three rows, six requests, three CONTROLs.
  • Ablation, declared direction RED: the door's authGate: 'resolved', was mutated to 'withhold' (marker ABLATION-23217-s3-gate) by scripts/ablation-replace.mjs: "anchor 1 to 0, blob 1d3595a6982d to 3b27a1db91ba" (the tool prints arrows), with the marker counted on disk as 1. Both suites read the door from src (in-package relative imports, and runtime's new alias), so no dist preflight applies.
    • Observed in the settings door pins (the new pin, the ADR-0069 producer pin, the key account-posture pin): Tests 8 failed | 27 passed (35), 3 of 3 files red.
    • Observed in the conformance test: Tests 3 failed | 7 passed (10): the two gated-session requests and the settings CONTROL.
    • Restore: "blob == HEAD (1d3595a6982d) and git diff HEAD is empty", marker 0.
    • The observed direction equals the declared one.
  • Lint: pnpm exec eslint --no-inline-config over the six changed .ts / .mjs files: exit 0, no output. The repo-wide pnpm lint is CI's.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 100 commands at 6328d1141. Each was run with its exit code captured before any pipe.
    • 99 exited 0.
    • pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: no dist/ for 41 entry points). So I built the whole tree under the verify lock (turbo run build, docs excluded: 76 tasks, 75 cache hits) and re-ran it on the same head: exit 0, "111 published require entry point(s) across 69 package(s) load".
    • --ran reconciliation: "100 derived, 100 run, 0 NOT-MEASURED, 0 UNRUN".
    • Among them, all exit 0: check:authz-resolver, check:cross-package-test-inputs, check:test-source-alias, check:slot-lookup, check:engine-double-contract, check:objectql-double-limit, check:where-matcher, check:type-check-coverage, check:type-check-debt, check:published-files, check:nul-bytes, check-adr-0087-registration --base origin/main and check-changeset-no-major --base origin/main.
    • Not run locally, and left to CI's Test Core: the core and runtime test:repo projects, beyond the one core ledger file this diff edits.

Acceptance notes

  • The settings door's own catch is unchanged. The two brands are re-raised, and any other throw is the fail-closed { enforced: true }.
  • A key's read failure answers 401, before and after. The probe row "engine read rejects + key" answers 401 UNAUTHENTICATED, where the same fault on a session request answers 503. The cause is in @objectstack/core, not this door: resolveApiKeyAdmission catches a failing sys_api_key read and returns { outcome: 'none' }. It is identical before and after this PR, and it is reported to the seat rather than changed here.
  • No changeset on @objectstack/runtime. Its diff is a devDependency, a vitest alias and a test, and none of those ships to a consumer. @objectstack/service-settings carries the patch.

Generated by Claude Code

…ADR-0069 gate on the listing route (WIP)

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
…o-ExecutionContext composition (WIP)

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
…ort ledger, slot-lookup ratchet, a settings conformance row (WIP)

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/service-settings, touching 4 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/runtime/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/protocol/kernel/index.mdx (via SettingsServicePlugin (symbol, a top-level class))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/runtime/vitest.config.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 32 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 d537a318b3116a2bcb4f70cf85ffd2a05c13d002 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from fd03fa48d76ee0197163fe047758b81c9f31e907 — the merge of head 6328d114172818c0e0946fb67b2355ada98d84bc into base d537a318b3116a2bcb4f70cf85ffd2a05c13d002, 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 fd03fa48d76ee0197163fe047758b81c9f31e907 && git checkout fd03fa48d76ee0197163fe047758b81c9f31e907
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d537a318b3116a2bcb4f70cf85ffd2a05c13d002 6328d114172818c0e0946fb67b2355ada98d84bc && git checkout -B drift-repro d537a318b3116a2bcb4f70cf85ffd2a05c13d002 && git merge --no-ff 6328d114172818c0e0946fb67b2355ada98d84bc

node scripts/docs-audit/affected-docs.mjs --json d537a318b3116a2bcb4f70cf85ffd2a05c13d002

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T09:57Z · on head 6328d1141 · #23217 slice 3 (claim 6247548866, amended at this review; report 6248458478). This is the seat's own review at the default tier (Clause-②: no under ruling 6227928430's "later slices Clause-②: no"; no packages/spec path; not governed), read against GitHub, not the report.

  1. Shape: draft against main. The first line is Part of #23217, and no closing keyword is in the body (full-body scan). Clause-②: no is line-initial and bare, with its basis in prose. Assignee zhuangjianguo. 10 files, +608 / −140. check-governed-merges --pr 24297: NOT governed.
  2. Premise, measured: the door diff matches letter A item 3. The hand-rolled composition (resolveAuthzContext, the getSession closure, the inline posture read) is replaced by one resolveRequestExecutionContext call, with settings' differences as named parameters (fail-closed, resolved, api-key-and-session, optional, request, a tenancy resolver, withhold ×2, no locale, no OAuth, no admission channel). The SettingsContext projection and the fail-closed catch stay at the door. ⛔ No edit to @objectstack/core, so letter B does not arise. The 10-row ADR-0096 / ADR-0069 pin on GET /api/settings is green on the hand-written door (78e9382c6) and after. The 29-wiring fault probe reads identically on both trees.
  3. Readers, read in the diff:
    • DELEGATORS gains the settings row (the seat's decision in the claim).
    • The core TRANSPORT_LEDGER settings row leaves, because the door no longer calls resolveAuthzContext({ itself.
    • The conformance test gains a settings row with a CONTROL.
    • packages/runtime gains a devDependencies entry, with the lockfile regenerated.
    • The anchored test-only vitest alias is added.
    • The slot-lookup baseline goes 2 → 1, a ratchet decrease.
  4. Gates: the report's local union of 100 derived families is all exit 0. check:dual-build-cjs-loads was turned from exit 3 into a reading by a whole-tree build and a re-run on the same head, and the --ran reconciliation shows 0 NOT-MEASURED. eslint over the changed files: exit 0. CI on this head at this stamp: 30 completed, all success or skipped; 9 in_progress; no failure. ⛔ Not ready until every check on the head is green.
  5. Deviations accepted:
    • vitest.config.ts and slot-lookup-baseline.json lie outside the declared surface. Both are required reader moves, and the claim is amended in place for them.
    • The conformance test changed beyond "one DOORS row". DoorRow became generic over a door's projection, because settings hands on SettingsContext, not the ExecutionContext. The existing runtime and REST rows keep their values. Its header states which parameters the projection cannot tell apart.
    • A2 was falsified: no service-settings test reads the door's source text.
    • One door-internal ordering moved (the auth-service lookup now runs before the tenancy read), with the same outward answers on all 29 wirings.
    • No changeset on @objectstack/runtime: its diff is a devDependency, an alias and a test.
    • The ablation went red in the declared direction and was restored to the HEAD blob.
  6. Out of scope: finding 1 (class (b): a rejecting sys_api_key read becomes "no key" in resolveApiKeyAdmission, 401 for a key where a session gets 503) was confirmed by the seat's own read of packages/core/src/security/api-key.ts and filed as core(api-key): resolveApiKeyAdmission turns a failed sys_api_key read into "no key", so a key holder gets 401 during a store outage where a session caller gets 503, and the request falls on to the session arm #24299. Shared core code; not this PR's.
  7. Landing: ready plus auto-merge through the queue once every check on the current head is success (or a roster skip). Then read origin/main and the queue branch for the merge. The card stays open for slices 4 and 5 (handover_slice4 in the report): this seat releases it to pm:queue with a Release: line in the same act as the landing record.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants