Skip to content

refactor(service-storage): the storage download door calls the one request-to-ExecutionContext composition, resolveRequestExecutionContext - #24364

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

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

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Part of #23217

Clause-②: no
Ruling-ref: 6227928430

Basis for the Clause-② line: the ruling's "later slices Clause-②: no". The storage door's outward answers are unchanged (a 12-row ADR-0096 / ADR-0069 pin and a 47-wiring fault probe read the same on both trees), and no export moves in @objectstack/service-storage. The function's export landed in slice 1.

Slice 4 of #23217 under ruling record 6227928430 (letter A): the storage door, the download authorizer buildFileReadAuthorizer in packages/services/service-storage/src/storage-service-plugin.ts, calls the one resolveRequestExecutionContext that slice 1 landed in @objectstack/core. @objectstack/core's function is not touched (patch round 1 corrects two rows of its module docblock table, comment text only): every difference of the download door is one of the function's existing named parameters, so letter B does not arise for it. The card stays open: slice 5 (plugin-hono-server) remains, and the storage family's ADR-0069 gate reader is an item-3 question (below), not converted.

What changed

The door

buildFileReadAuthorizer keeps its door facts and calls the function once. Each parameter was measured on the hand-written door (origin/main 68a090fb9, door blob ff2307ad9) before converting:

parameter the download door passes what the hand-written door did
anonymous 'fail-closed' no userId answered 'unauthenticated' (401 AUTH_REQUIRED); unchanged
authGate 'resolved' handed on the raw context, whose authGate is the posture the admitting read decided. The family door's gate reader runs first, so this door is not a second gate; 'reread-when-active' would read the session a third time and add a 503 arm this door never had
credentials 'api-key-and-session' the real request headers, so x-api-key is honoured
sessionApi 'optional' an auth service with no session read still admits a key. With no auth service at all there is no authorizer (a door fact, unchanged)
session 'request' the auth service's in-process read
tenancy its own resolver, tenancyServiceResolver(registry) the async accessor when the host has one (#15352, #16013); the resolution stays at the door
localization, requestLocale 'withhold', undefined none resolved
accessToken 'carry' the raw context carried the session bearer to the owner read and to a file-access delegate (session.accessToken for a hook)
oauth, onAdmission undefined none

Door facts that stay at the door: the auth-service lookup (once per binding, as the gate reader does it), the header normalisation, the tenancy wiring, the owner shortcut, the owner-record and attachment-parent reads, and the fail-closed catch that re-raises the two brands.

The tenancy wiring was split as settings' was: tenancyServiceResolver(registry) is the door's way to the service, handed to the function; the gate reader's resolveAdmissionTenancyPosture now classifies through the same resolver. The private GateAuthService type became @objectstack/core's RequestAuthService, the slice of the auth service the function reads.

What the door hands on

The context that reaches readableFieldOwners (the owner-record read and a fileAccessDelegate service) and the attachment-parent reads is now the assembled ExecutionContext envelope instead of the raw ResolvedAuthzContext. Measured on every resolved row of the probe: the key set gains isSystem (false) and principalKind ('human') and loses credential; every other key and value is the same. Readers measured on origin/main: no source under packages/objectql, plugin-security, plugin-sharing or plugin-approvals reads context.credential; no reader compares isSystem with false or undefined; principalKind is read for 'guest', 'agent', 'service' and 'system' only (plus the security plugin's memo key, which is a cache key); the one file-access delegate (ApprovalService.authorizeFileRead) is typed for ExecutionContext already. No door-side projection is needed: the door itself reads only userId.

Readers moved in the same PR

  • packages/core/src/security/authz-store-unavailable.test.ts: the storage row of TRANSPORT_LEDGER left (the file no longer holds resolveAuthzContext({), and the composition's comment names the fourth door.
  • scripts/check-single-authz-resolver.mjs: DELEGATORS gained the storage row (resolveRequestExecutionContext).
  • packages/runtime/src/security/request-execution-context.conformance.test.ts: a STORAGE_ROW with its own capture point (the read of the record that owns a private field-owned file, through the real GET /files/:fileId/url on the booted plugin), and a storage CONTROL. DoorRow gained answeredBeforeComposition: the storage door answers two requests before its composition runs (its family gate refuses the held session while a gate is active, and a host with no auth service binds no authorizer), so the row declares them, hands nothing on for them, and the CONTROL reads the answer the door gave (403 PASSWORD_EXPIRED; 200 served unasked). The world fixture gained the owned file, the owner object, a findOne, and an auth service without a session read; the storage sessionApi is told apart there. Request gained held.
  • packages/runtime/package.json: @objectstack/service-storage as a devDependency (no workspace cycle: its 18-package closure, devDependencies included, never reaches @objectstack/runtime; check:workspace-manifest-cycles exit 0), pnpm-lock.yaml regenerated by pnpm install (+3 lines), and an anchored test-only alias in packages/runtime/vitest.config.ts (check:test-source-alias).
  • scripts/slot-lookup-baseline.json: not touched. buildGetSession's any-typed lookup is still reached by its two other readers, and check:slot-lookup holds at 5 for the file.
  • No service-storage test read the door's source text, so nothing was re-aimed.

The pin (ruling item 2)

packages/services/service-storage/src/storage-routes.request-context-door.pin.test.ts boots the real plugin, drives GET /files/:fileId/url for a private field-owned file, and reads what reached the service (which context the owner-record read received, and whether the adapter minted a capability). 12 rows: four ADR-0096 refusals (no credential, an unknown cookie, a failed session read with no gate active, an ex-member's key beside a cookie), four ADR-0069 rows (a held session 403, a failed re-read under an active gate 503, a held owner's key 403, a key beside a held cookie answers for its owner), the held session served with no gate active carrying the admitting read's posture, two CONTROLs (an ungated session carrying its bearer; a key on a host whose auth service has no session read) and the wiring row (no auth service: served unasked).

Evidence

All on a shared container; lock seconds are shared-box readings.

  • The pin on the hand-written door (d2888fa3d, door blob ff2307ad9): Tests 13 passed (13) (the pin's 12 plus the one-off probe), and on the converted door (0d76d89eb, door blob 1bdd64a15): Tests 13 passed (13).
  • A one-off fault probe (never committed) drove the booted plugin's download route on 47 wirings (tenancy outage, unregistered or no async registry; permission store, key store or owner object throwing; auth service absent, without a session API, getApi rejecting, its session read throwing, its gate probe throwing, its posture computation or key policy throwing, and its registry lookup throwing only after boot; held sessions and held owners' keys with the gate active and inactive; revoked, refused and org-less keys, alone and beside cookies). diff of the before and after readings: IDENTICAL on all 47 answers (status, code, who the owner read was asked as, capability minted). The only difference is the context's key set described above, plus one row: with the auth service's registry lookup throwing after boot and a held owner's key under an inactive gate, the context now carries the owner's authGate, because the door asks the auth service it bound for the posture where it used to look it up again per request; the verdict and the answer are unchanged (200, read as the key's owner).
  • ABLATION, declared direction RED, at f24d8a103 through scripts/ablation-replace.mjs inside the verify lock (both suites read the door from src: relative imports in the package, the runtime alias): leg 1 authGate: 'resolved', to 'withhold' (marker ABLATION-23217-s4-gate, on disk marker 1 / anchor 0): pin Tests 1 failed | 11 passed (12), conformance Tests 2 failed | 9 passed (11). Leg 2 accessToken: 'carry', to 'withhold' (marker ABLATION-23217-s4-bearer): pin Tests 2 failed | 10 passed (12), conformance Tests 2 failed | 9 passed (11). Each restore: blob equals HEAD (1bdd64a15) and git diff HEAD empty, marker 0. Observed direction equals declared.

Tests (HEAD f24d8a103, after merging origin/main dd72a047f)

  • @objectstack/service-storage: Test Files 60 passed (60), Tests 1041 passed (1041); typecheck exit 0 (test layer 0 / 0 / 0 held), and tsc -p tsconfig.test.json --listFiles includes the new pin.
  • @objectstack/runtime: the conformance test, Test Files 1 passed (1), Tests 11 passed (11) (6 requests, the identity test, 4 CONTROLs); typecheck exit 0 (test layer 27 files / 190 errors / 68 signatures held, none in the conformance file). The rest of the runtime suite was not run locally: no other runtime test imports @objectstack/service-storage (only comments and strings name it), the alias is anchored, and CI's Test Core runs it.
  • @objectstack/core: the repo-project ledger src/security/authz-store-unavailable.test.ts, Tests 37 passed (37) (38 before, minus the storage row); typecheck exit 0 (test layer 4 / 4 / 4 held). Core's code is untouched; patch round 1 (e2891bfbc) edits two docblock table rows.
  • Build: turbo run build --filter=!@objectstack/docs --concurrency=2 after the merge, 76 successful, 76 total.

Gates (HEAD f24d8a103)

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 102 commands; each ran with its exit code captured before any pipe. 101 exited 0 on the first pass; pnpm check:type-check-debt exited 3 (PREREQUISITE NOT MET: its closure build did not get a turn on the verify lock), and was re-run under the lock on the same head: exit 0 (none above its recorded number). --ran: 102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. Among them check:authz-resolver, check:slot-lookup (100 unswept site(s) in 25 file(s), none new), check:test-source-alias, check:cross-package-test-inputs, check:workspace-manifest-cycles, check:dual-build-cjs-loads (exit 0 on the built tree), check:dts-closure, check:published-files, check:type-check-coverage, check:pm-dispatch-gates.
  • The declared wide population (dispatch-gates.mjs with the changed paths, no --commands): all 11 families run, all exit 0: check:init-service-contract, check:live-db-isolation, check:meta-type-normalized, check:optional-error-sink, check:resume-authority-declared, check:route-envelope, check:runner-env-posture, check:settings-bind-window, check:startup-registry-verdict, check:verify-stand-in, check:wildcard-fallthrough. Also check:adr-anchors and check:adr-symbol-anchors exit 0, and node scripts/check-platform-checklist.mjs --changed-from origin/main exit 0.
  • Edited gate script: node scripts/check-single-authz-resolver.mjs exit 0 and --self-test exit 0.
  • pnpm exec eslint --no-inline-config over the six changed .ts / .mjs files: exit 0, no output.

Item-3 question, not converted: the storage family's ADR-0069 gate reader

buildAuthGateSessionReader runs before every storage route (mountFamilyRoute). It re-composes the function's gate decision beside it: the key arm (resolveApiKeyAdmission with the door's posture, a held key answers its owner's posture) and, while isAuthGateActive(), a session re-read whose failure is AuthzStoreUnavailableError('auth_gate'). That is decideAuthGate's 'reread-when-active' arm plus resolveAuthzContext's key arm, so it is a second copy of the gate decision. The one function cannot carry it as an existing parameter without changing answers: the probe asked the function, with storage's parameters and 'reread-when-active', what the gate reader decides on the same 45 applicable wirings, and they differ on 14 (the function resolves the whole principal even with no gate active, so a tenancy, permission-store or posture outage becomes a 503 on every storage route; an active gate whose re-read fails is a 503 at the gate and a pass in the function, whose first read swallows the failure; the key arm differs too, detail withheld with #24366). So it was not converted. The seat's decision (review 6252129939): it stays as it is on this card; the gap is #24366, where a parity pin or a gate-only mode is decided. The card's acceptance (git grep -c of the composition's distinctive lines reads 0 in the five files) therefore does not yet read 0 for storage-service-plugin.ts: resolveApiKeyAdmission( and classifyAdmissionTenancyPosture( remain there, in the gate reader.

Acceptance notes


Generated by Claude Code

@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

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/core, @objectstack/runtime, @objectstack/service-storage, touching 10 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/core/src/security/request-execution-context.ts, 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/kernel/contracts/auth-service.mdx (via isAuthGateActive (symbol, a method of type GateAuthService))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/core/src/security/request-execution-context.ts, packages/runtime/vitest.config.ts) — pages documenting those are invisible to this run
  • 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 — 48 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 3a734e088899c8a50d8dbdefdd252e83b63a1308 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3a734e088899c8a50d8dbdefdd252e83b63a1308

⚠️ 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 3a734e088899c8a50d8dbdefdd252e83b63a1308 → 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

席位核对 — REWORK (patch round 1: one docblock table, no behaviour)

domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T16:12Z · on head f24d8a103 · #23217 slice 4 (claim 6250804277, report 6252058631). This is the seat's own review at the default tier (Clause-②: no), read against GitHub, not the report.

Accepted as it stands:

  • buildFileReadAuthorizer calls resolveRequestExecutionContext once, with storage's named parameters measured on the merged code. @objectstack/core's function is not edited.
  • The door's answers are unchanged: a 12-row ADR-0096 / ADR-0069 pin on GET /files/:fileId/url is green on the hand-written door and after, and a 47-wiring one-off probe reads identical answers on both trees.
  • The two ablation legs (authGate, accessToken) went red in the declared direction.
  • The readers moved in this PR: the TRANSPORT_LEDGER storage row, the DELEGATORS row, and the conformance STORAGE_ROW with its answeredBeforeComposition hook, devDependency, lockfile and anchored alias.
  • The deviations are accepted as reported.

Item 3 (the family gate reader), the seat's decision:

Owed in this PR:

Redaction (done by the seat in this act): the reproduction of the gate gap had reached GitHub in this PR's body and in your report. Both were replaced with a pointer to #24366, under the maintainer's disclosure ruling (2026-08-18, #9387). ⛔ Do not reintroduce it.

Before pushing: read this head's checks (Test Core was still running at this stamp) and fold any red into the same round.


Generated by Claude Code

… download door's 'resolved' gate and carried bearer (WIP)

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T17:04Z · on head e2891bfbc · #23217 slice 4 (claim 6250804277, amended in place; reports 6252058631 and 6252633955). This is the seat's own review at the default tier (Clause-②: no), read against GitHub, not the report.

  1. Shape: draft against main. The first line is Part of #23217, with no closing keyword (slice 5 remains), and Clause-②: no. Assignee zhuangjianguo. 10 files, +700 / −88, with a patch changeset on @objectstack/service-storage. Not governed, under 3,000.
  2. The door:
    • buildFileReadAuthorizer calls resolveRequestExecutionContext once, with storage's named parameters measured on the merged code: fail-closed, authGate: 'resolved', api-key-and-session, sessionApi: 'optional', the door's own tenancy resolver, localization withheld, accessToken: 'carry'.
    • Its answers are unchanged: a 12-row ADR-0096 / ADR-0069 pin on GET /files/:fileId/url is green before and after, and a 47-wiring one-off probe reads identical answers on both trees.
    • The two ablation legs went red in the declared direction.
  3. Readers moved: the TRANSPORT_LEDGER storage row out; a DELEGATORS row in; a conformance STORAGE_ROW with its CONTROL, a runtime devDependency, the lockfile from pnpm install and an anchored alias. Patch round 1 (e2891bfbc) corrects core's module docblock table rows authGate and accessToken for the storage download door, as comment text, read on the diff (+2 / −2). Declared on [PM seat] domain:engine — ⏳ vacant #6367 6250813594 and 6252143076.
  4. Item 3: buildAuthGateSessionReader stays as it is on this card. The gate gap is security: a session the ADR-0069 auth gate holds can still be served by the storage route family (detail withheld under the disclosure ruling) #24366 (security), where a parity pin or a gate-only mode is decided.
  5. Redaction: the reproduction of security: a session the ADR-0069 auth gate holds can still be served by the storage route family (detail withheld under the disclosure ruling) #24366 was removed by the seat from this PR's body and from report 6252058631, under the disclosure ruling (2026-08-18, skills: five principle gaps the #9296 wave exposed in checklist-test / checklist-author / dogfood-verification #9387). That covers the outcome table and the two key-arm clauses of the item-3 comparison. The PR body's patch-round corrections were also written by the seat, as the dev role file assigns.
  6. Gates:
    • Locally at e2891bfbc: 102 derived commands exit 0, with 0 NOT-MEASURED. The 11 declared-wide families exit 0, and so do check:adr-anchors and check:adr-symbol-anchors. eslint exits 0.
    • CI on f24d8a103 completed with no failure, Test Core 1–6 included; CI on this head is running at this stamp.
    • ⛔ Not ready until every check on the head is green.
  7. Deviations accepted: as reported in both rounds (the conformance test's answeredBeforeComposition hook, the door-internal lookup timing, the tenancy wiring split, and A4 partly falsified with the slot-lookup baseline unmoved).
  8. Out of scope: security: a session the ADR-0069 auth gate holds can still be served by the storage route family (detail withheld under the disclosure ruling) #24366 (filed by the seat), and 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 (the key-store read failure, already filed). The rest goes to slice 5 (handover_slice5).
  9. Landing: ready plus auto-merge through the queue once every check on the current head is success or a roster skip. Then the card is released to pm:queue for slice 5 (plugin-hono-server).

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