Repository navigation
refactor(service-settings): the settings door calls the one request-to-ExecutionContext composition, resolveRequestExecutionContext - #24297
Conversation
…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>
…ings door (WIP) Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 32 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older
|
席位核对ACCEPT ·
Generated by Claude Code |
Part of #23217
Clause-②: no
Ruling-ref: 6227928430
Slice 3 of #23217 under ruling record 6227928430 (letter A): the settings door,
SettingsServicePlugin'sverifiedContextFromRequestinpackages/services/service-settings/src/settings-service-plugin.ts, calls the oneresolveRequestExecutionContextthat slice 1 landed in@objectstack/core. The card stays open for its two remaining doors,service-storage(slice 4) andplugin-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-②: noline: the ruling's "later slicesClause-②: 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
verifiedContextFromRequestkeeps its door facts in their old order and calls the function once. Each parameter was measured on the hand-written door at1e001c7dfbefore converting:anonymous'fail-closed'userId, which the family's anonymous floor refuses with401. The function now yields no context and the door answers its existing fail-closed{ enforced: true }: the same floor, the same401.authGate'resolved'authz.authGatefrom the read that admitted the caller. There was no re-read, andisAuthGateActive()was never asked.credentials'api-key-and-session'resolveAuthzContext, so the API-key door was open.sessionApi'optional'session'request'api.getSession(inProcessSessionReadInput(headers))(.api, elsegetApi()).tenancyundefinedon a host with noneresolveAdmissionTenancyPosture:getKernel().getServiceAsync('tenancy'), classified byclassifyAdmissionTenancyPosture.localization'withhold'requestLocaleundefinedaccessToken'withhold'SettingsContextdeclares no bearer field, and the door's projection never readauthz.accessToken. The bearer never left the door, so'withhold'also keeps it off the intermediate envelope.oauth,onAdmissionundefinedWhat stays at the door, in its old order:
try;tenancyServiceResolver).resolveAdmissionTenancyPosture, the settings service's own posture read, classifies that same thunk as before;SettingsContext:userId,tenantId, system capabilities and grants as one list,enforced: true, andauthGatewhen one holds;catchthat 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:DELEGATORSgains the settings row (resolveRequestExecutionContext), the seat's decision recorded in the claim. The gate answers "all 4 entry points delegate";--self-testexit 0.packages/core/src/security/authz-store-unavailable.test.ts: the settings row ofTRANSPORT_LEDGERis 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-settingsadded to runtime'sdevDependenciesandpnpm-lock.yamlregenerated bypnpm install(+3 lines).packages/runtime/vitest.config.tsgains an anchored alias for@objectstack/service-settingsto itssrc/index.ts. Without it, the new row would import the settings door from its builtdist/, andcheck:test-source-aliasrequires 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.scripts/slot-lookup-baseline.jsonchanges the settings door's count from 2 to 1. The deletedgetSessionclosure held one of the file's two service lookups erased toany(const authService: any = ctx.getService('auth')).check:slot-lookupfails on a decrease too ("ratchet DOWN: run--update"), andpnpm check:slot-lookup --updatewrote 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.tshas 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:401 UNAUTHENTICATED, service never asked401401401; the cookie never answers for the refused key403 PASSWORD_EXPIREDwith its message403; the posture is the admitting read'saccount-scope posture403 MFA_REQUIREDwith its message200as the key's owner, noauthGate200as that principal, with tenant,enforced, noauthGate200as its ownerIt was committed first, on the hand-written door (
78e9382c6, door blob7c72080fa):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 areuserId,tenantIdandpermissions. The other two rows are unchanged: their entries are now named constants, typed by what each door hands on (DoorRowis 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/coreexport, 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;export_reportsreachespermissions.What no request can tell apart at this door: the projection carries no locale, time zone or bearer, so
localization,requestLocaleandaccessTokencannot 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.
accessTokenwas measured as'withhold'.A2 (source-text pins): falsified. No test in
@objectstack/service-settingsreads the door's source text. Its onereadFileSyncpin (value-domains.shared-predicate.pin.test.ts) readsvalue-domains.ts. Nothing was re-aimed.A3 (bare-term censuses): I took an identifier-token census of the door file at
1e001c7dfand at the conversion. 182 distinct tokens changed count. Code identifiers among them:resolveAuthzContext4 to 2 (both left in comments),resolveRequestExecutionContext0 to 3,getSession3 to 0,inProcessSessionReadInput2 to 0,userAuthGateSourceOf2 to 0,apiKeyLifecyclePolicySourceOf2 to 0,getApi2 to 0,getService9 to 8,any13 to 10,tenancyServiceResolver0 to 5. Three readers count these over this file:TRANSPORT_LEDGER(resolveAuthzContext({),DELEGATORS(resolveRequestExecutionContext) and the slot-lookup ratchet (theany-erased lookup). All three moved in this PR. The dogfoodauthz-probe-blind-spotcensus 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 throughGET /api/settings: the hand-written door at door blob7c72080fa, then the converted door at1d3595a69. All 29 answers are identical. The double-fault rows:getApi()rejects + sessionThe 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
devDependencycreates 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
6328d1141(origin/main9822b053emerged in).@objectstack/service-settings:Test Files 49 passed (49),Tests 871 passed (871).typecheckexit 0, and its program includes the new pin (tsc --listFiles).@objectstack/core: local projectTest Files 104 passed (104),Tests 2551 passed (2551). The repo-project ledgerauthz-store-unavailable.test.ts:Tests 38 passed (38), slice 2's 39 minus the settings row.typecheckexit 0, test layer 4 / 4 held.@objectstack/runtime: local projectTest Files 416 passed (416),Tests 6558 passed | 19 skipped (6577).typecheckexit 0, test layer 27 files / 190 errors / 68 signatures held. The conformance test, verbose:Tests 10 passed (10): three rows, six requests, three CONTROLs.authGate: 'resolved',was mutated to'withhold'(markerABLATION-23217-s3-gate) byscripts/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 fromsrc(in-package relative imports, and runtime's new alias), so no dist preflight applies.Tests 8 failed | 27 passed (35), 3 of 3 files red.Tests 3 failed | 7 passed (10): the two gated-session requests and the settings CONTROL.pnpm exec eslint --no-inline-configover the six changed.ts/.mjsfiles: exit 0, no output. The repo-widepnpm lintis CI's.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 100 commands at6328d1141. Each was run with its exit code captured before any pipe.pnpm check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: nodist/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".--ranreconciliation: "100 derived, 100 run, 0 NOT-MEASURED, 0 UNRUN".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/mainandcheck-changeset-no-major --base origin/main.test:repoprojects, beyond the one core ledger file this diff edits.Acceptance notes
catchis unchanged. The two brands are re-raised, and any other throw is the fail-closed{ enforced: true }.401, before and after. The probe row "engine read rejects + key" answers401 UNAUTHENTICATED, where the same fault on a session request answers503. The cause is in@objectstack/core, not this door:resolveApiKeyAdmissioncatches a failingsys_api_keyread and returns{ outcome: 'none' }. It is identical before and after this PR, and it is reported to the seat rather than changed here.@objectstack/runtime. Its diff is adevDependency, a vitest alias and a test, and none of those ships to a consumer.@objectstack/service-settingscarries thepatch.Generated by Claude Code