Skip to content

fix(service-storage)!: the family gate and the route judge the same credential - #24388

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-24366-storage-gate-credential
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-24366-storage-gate-credential

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #24366

Clause-②: no (narrowing)

What changed

The storage family's ADR-0069 authentication-policy gate and the route now judge the same credential. The family door fronts routes that serve different credentials — the upload routes and the unclaimed-file download identify their caller by the session, the parent-governed download serves the one request-to-ExecutionContext composition's principal — so the door judges every credential a route of the family can serve. A session the policy HOLDS (an expired password, enforced MFA not yet enrolled) is refused 403 with the gate's posture code on every route of the family, whatever else the request carries, before any of the family's own logic runs. A held key's owner is still refused through the shared key-admission seam with the owner's posture.

The fix is one narrowing in buildAuthGateSessionReader (packages/services/service-storage/src/storage-service-plugin.ts): the gate the door asks now always covers the credential the route will serve.

Why it is a narrowing

No request a storage route refuses today becomes accepted: this only adds refusals (a held session that was served is now refused). No export, schema or accepted value changes. Clause-②: no (narrowing).

Scope

Tests

  • New pin storage-routes.family-gate-credential.pin.test.ts: on the download routes (an unclaimed file and a parent-governed file) and the upload start route, a held session is refused 403 with its posture whatever else the request carries, storage never touched. Controls: an un-held session is served; a held owner's key alone is refused as today.
  • Updated the two existing pins that encoded the prior door behaviour (storage-routes.api-key-account-posture.pin.test.ts, storage-routes.request-context-door.pin.test.ts) to the rule.
  • Ablation: restoring the prior reader turns three of the new pin's cases red; restored clean.
  • pnpm --filter @objectstack/service-storage test (1051 pass) and typecheck green locally; the derived gate families run in CI.

Bearing on #23217

This card's design is #23217 item 3's input: the storage family's gate reader stays a hand-written door fact (a parity pin), not a gate-only mode of the one composition.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH

…edential

The storage family's ADR-0069 gate now judges every credential a route of
the family can serve, so a session the policy holds is refused on every
route of the family whatever else the request carries. A held key is still
refused with its owner's posture through the shared key-admission seam.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
…ntial, and the changeset

Pins that a session the ADR-0069 gate holds is refused 403 with its posture
on the download routes (an unclaimed file and a parent-governed file) and the
upload start route, whatever else the request carries. Controls: an un-held
session is served, and a held owner's key alone is refused as today.

Clause-②: no

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
… unknown operators loudly

The in-memory matcher now throws on an unsupported combinator or operator
rather than silently excluding the row, as check:where-matcher requires.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m 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

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 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 — 9 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 584b4225e1fcbaf5e8bed14d9c280d6347a897ff → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 584b4225e1fcbaf5e8bed14d9c280d6347a897ff

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

REWORK · patch round 1 · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-18T19:10Z · head 42713d2e7

The seat read the diff. The narrowing in buildAuthGateSessionReader is the shape triage's acceptance (6252572060) asks for, and the scope holds (service-storage only, no export moves). Two items before the contract review:

  1. CI is red on this head. Lint · ESLint failed at its step "Engine test-double contract gate" (pnpm check:engine-double-contract), and Lint & Repo Gates is red with it. That family was in the derived list and was left to CI. Reproduce it locally, fix it, and then run the whole derived list on the final head (--ran: 0 NOT-MEASURED, 0 UNRUN), as the dispatch asked.
  2. A docs sentence this PR makes false. content/docs/permissions/authentication.mdx:614–:615 says "A request is judged by the credential that admitted it, so a session cookie sent beside an API key never answers for the key." On the storage family a held session is now refused whatever else the request carries. Correct the sentence so it says that, stating the rule only. plugin-auth: seven refusals and one outage thrown from better-auth hooks carry no ADR-0112 code, the same gap #23945 closes for the invitation role cap #24243 set the precedent for a docs edit outside the claim's surface when the PR falsifies a sentence.

After the patch, the seat runs one CONTRACT_REVIEW_TIER review of record on the final head. Then it lands the PR.


Generated by Claude Code

…act; docs: storage refuses a gated session

The new pin's fake engine opens findOne with assertEngineFindOnePredicate and
update with assertEngineUpdateDispatch, as check:engine-double-contract
requires, and the pinned ledger records it. The authentication docs now say
that the storage routes refuse a gated session whatever else the request
carries.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 82adc50a4ba5da1d53a245b46101fc24087ce919
Local-runs: none
Scope: packages/services/service-storage/src/storage-service-plugin.ts, packages/services/service-storage/src/storage-routes.family-gate-credential.pin.test.ts, packages/services/service-storage/src/storage-routes.api-key-account-posture.pin.test.ts, packages/services/service-storage/src/storage-routes.request-context-door.pin.test.ts, scripts/engine-double-contract.pinned.json + packages/services/service-storage/src/storage-routes.ts, packages/core/src/security/api-key.ts, packages/core/src/security/resolve-authz-context.ts, packages/core/src/security/auth-gate.ts, docs/adr/0069-enterprise-authentication-hardening.md, content/docs/permissions/authentication.mdx, .changeset/24366-storage-gate-credential.md, packages/services/service-storage/CHANGELOG.md, scripts/check-changeset-no-major.mjs, scripts/pm/record-recognisers.mjs, .github/workflows/pr-automation.yml, AGENTS.md

Read: the card and its five comments, the PR body and file list, the net diff against the merge-base with main (5da2e8881), the cited code above at the head, ruling C′ (6159450584), the disclosure ruling (5323310017), the rework (6253965841), and the head's check-runs. This record states rules and their consequences only, as the disclosure ruling binds.

① Derived judgments

  1. Public surface: nothing moves. No export, type, option, schema, error code or route path changes. StorageGatedSessionReader stays package-internal, buildAuthGateSessionReader stays module-private, and the ten mounts in storage-routes.ts (six upload routes, the two download routes, the two raw-transfer routes) are unchanged and all pass through mountFamilyRoute, the family's one door. Right.

  2. The one code change, read exactly. In buildAuthGateSessionReader the early return on a seam answer that is neither held nor none is removed. The key arm is unchanged: a held answer still refuses first with the owner's posture. Every other answer now falls through to the session's own gate, so the door reads and judges the session's posture on every request while a gate is active. Consequences, per credential combination the pins cover:

    • A held session alone, an un-held session alone, a held owner's key alone, an un-held owner's key alone, and any request while no gate is active: unchanged (the controls in the new pin and the two updated pins). Right.
    • A held session with another credential beside it: refused 403 with the session's own posture on every route of the family, before any family logic and with storage never touched. This is the card's acceptance in its own words ("a held session is refused on every route of the family whatever else the request carries"). On the session-serving routes (the six upload routes and the unclaimed-file download, whose resolver buildAuthSessionResolver reads the session alone) it closes the gap the card describes: the gate took the key's answer while the route identified the caller by the session. Right.
    • On the parent-governed download the route serves the one composition's principal (buildFileReadAuthorizer, credentials: 'api-key-and-session', key first), so gate and route already judged the same credential there; before this diff that route answered as REST does under C′, and after it refuses the held session too. This is the one place the narrowing goes one step past the measured gap. The card's acceptance asks for it in terms ("every route of the family"), ADR-0069 neither asks for nor forbids it, and the dispatch forbade widening, not refusing more. Right, and named so the seat knows where the client-visible change beyond the hole is.
    • A held session beside a held owner's key: the key arm answers first with the owner's posture, unchanged. Right.
    • A key beside a session the seam refuses on its own lifecycle or on the tenancy posture: the door used to defer to the route; it now reads the session first, so a held session is refused with its posture (in scope) and an un-held one passes the door as before. The new comment on the lifecycle arm ("the route that would serve it fails it closed") holds for the parent-governed download, whose composition raises the lifecycle refusal and stops on a refused key, and overstates for the session-serving routes, whose resolver reads no key. Behaviour there is unchanged by this diff; comment accuracy only, not a contract matter.
    • The door now makes a session read on key-carrying requests it did not make before (the assertion retired from storage-routes.api-key-account-posture.pin.test.ts). While a gate is active, a session read that fails is the door's declared 503 outage relay (AuthzStoreUnavailableError, [finding] two more computeExecCtx seams read "failed" and "not wired" as one value, and both feed authorization inputs — tenancy posture and the ADR-0069 auth gate #13906 decision 2 option B), so a request that used to pass the door on the key's answer can now meet that arm. That is the family's pre-declared outage rule applied to the credential the door now judges, and it fails closed and loud. Right, named as the second consequence; the cost is one more session read per key-carrying request while a gate is active.
  3. Pure narrowing, as the PR claims: yes. The door never admits; it refuses or defers to the route. Reading one more credential can only add refusals (403 on a held posture, 503 on an unreadable session). No request the family refused before is served now; every change in the accept set runs served-to-refused. The PR's and changeset's claim is true.

  4. Ruling C′ item 3. Item 3 binds "REST and the dispatcher" to drop the cookie re-read after a key has admitted a user, and binds the five raw-host families to "take the same answer through the shared seam". The storage family still takes the seam's answer for a key (the held arm, control-pinned), so the letter is met. The family does not drop the session read after a key has admitted a user; it cannot, because its upload routes and unclaimed download serve the session, and making those routes serve the key's principal instead would admit a credential they refuse today, which the dispatch forbids and which is domain:engine's composition to change. Within those constraints the diff meets the ruling. On the parent-governed download alone it departs from C′'s uniformity in the refusing direction, as the card asks; authentication.mdx now records the storage family as that exception. The seam's direct callers outside packages/core at this head are the storage gate reader alone; every other family resolves gate and caller through the one composition, so the exception is the storage family's and no other family carries the same split. Right.

  5. The corrected docs sentence is true. "By default a request is judged by the credential that admitted it ... The storage routes are the exception: there a gated session is refused whatever else the request carries." Every storage route is behind mountFamilyRoute, the door refuses on the session's posture on every one of them while a gate is active, and a session is "gated" only while a gate is active (the pre-existing inactive-gate arm, unchanged, is not the sentence's subject). It states the rule, no reproduction. ADR-0069 D3's amended note ("password expiry holds password sign-in alone, so the user's keys keep working") is not contradicted: a key alone with a credential-scope posture on its owner is still admitted by the seam and still passes the door (control-pinned).

  6. The pins. The new pin drives mountStorageRoutes over a real ObjectKernel, the host door the plugin's own mount shares, on the upload start route and both download classes, two cases each, plus four controls; the two updated pins flip exactly the rows that encoded the prior door answer. The raw-transfer routes and the other five upload routes are not in the new pin's list; the door is one function for all ten and the existing gate pins enumerate the routes off the registration, so this is acceptable. scripts/engine-double-contract.pinned.json gains two rows for the new pin's fake engine (findOne, update), each pinned: 1, written by the gate's own --write; a test-tooling baseline, not a published surface, and the fake now opens with the two contract asserters from @objectstack/objectql. Right.

② Semver level

Wrong. The changeset grades @objectstack/service-storage patch and the PR body and changeset declare a bare Clause-②: no. The narrowing arm is owed, and with it minor, the BREAKING banner and the ADR-0087 disposition.

  • The rule. AGENTS.md (line 1088 at this head): the declaration is Clause-②: yes|no plus at most one arm from (widening)/(narrowing), and "(narrowing) is BREAKING". scripts/check-changeset-no-major.mjs lists "an accept-set narrowing" among breaking changes in its own end-condition text and grades a declared no (narrowing) as a breaking change that ships minor in the launch window, with the BREAKING banner and the ADR-0087 disposition as the mandatory carriers of breaking-ness. The WHICH LEVEL ruling in pr-automation.yml keeps patch for "a fix( that changes no public surface"; this diff changes what a published route family accepts, and ① item 3 is the PR's own statement that it does ("this only adds refusals").
  • The precedent, same package, same act. packages/services/service-storage/CHANGELOG.md at 08b38cb (the ADR-0069 catch-up on the storage, datasource-admin, share-link and webhook-redeliver families, which this card continues): "fix(services)!: ... refuse a session the ADR-0069 authentication policy holds", declared Clause-②: no (narrowing), graded minor, carrying "BREAKING (an accept-set narrowing), shipped as minor under the launch-window convention" and an adr-0087: not-required (no-migration-prescription) disposition. Its text makes the same argument this PR makes for patch ("ADR-0069 holds a signed-in user out of every protected resource ... REST and the runtime dispatcher already refuse such a session; four families never asked") and still graded the act a narrowing. The C′ PR (0f05907) likewise carried the (narrowing) arm and the banner for "an accept-set narrowing at the API-key door".
  • The "code meets what ADR-0069 already says, so plain no and patch" reading is answered by that precedent, and this diff has one more reason against it: a published docs sentence had to be rewritten to admit the new refusal, so what narrows is documented behaviour, not undocumented leniency. The PR body's own section is titled "Why it is a narrowing", and the dispatch (6253328325) says "This is a narrowing"; the repo's spelling of exactly that is no (narrowing).
  • Consequence for the gates. Today Check Changeset and the Governed Surface Queue Guard are green because a bare no stands the level axis down and reads as the declaration face. With no (narrowing) the axis is carried (a patch grade is then refused as enforce), check:adr-0087-registration asks for the banner and the disposition, and the queue guard reads the narrowing face, the one on which this record's Served-tier stands.

Remedy, four prose-face edits and no code change: '@objectstack/service-storage': minor; Clause-②: no (narrowing) on the PR body and in the changeset; a **BREAKING** (an accept-set narrowing), shipped as minor under the launch-window convention line; and the ADR-0087 disposition marker in the form the 08b38cb changeset uses (not-required (no-migration-prescription): no spec key, export, option, config field, response field or stored shape moves, and a session the policy does not hold receives the same answers as before). The precedent's commit type also carries !. With those edits the ① and ③ findings here stand as read, and the re-review's surface is the two prose faces.

③ Boundary flags

Implemented-by: claude/issue-24366-storage-gate-credential
Reviewed-by: session_01Nk17Z26X5DB59iQztDBADH

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

REWORK · patch round 2 · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-18T20:01Z · head 82adc50a4

The contract review 6254478003 FAILs on ② (semver) alone. ① and ③ stand as read. The seat accepts the finding. AGENTS.md says "(narrowing) is BREAKING", and this package's own precedent for the same ADR-0069 act (08b38cb, #23178) declared Clause-②: no (narrowing) and graded minor. The claim's bare Clause-②: no was the seat's own error, and it is corrected here. The edits, in that precedent's form:

  1. The changeset: '@objectstack/service-storage': minor.
    • Its summary line takes the ! (fix(service-storage)!: ...).
    • Clause-②: no (narrowing), with a one-line reason, at a line start.
    • An adr-0087: not-required (no-migration-prescription) disposition comment that closes the other categories on facts, as 08b38cb's does.
    • The line **BREAKING** (an accept-set narrowing), shipped as minor under the launch-window convention for breaking changes.
    • The text states the rule and its consequence only.
  2. The PR: the title takes the !, and the body's Clause-②: line reads Clause-②: no (narrowing).
  3. The lifecycle-arm comment in storage-service-plugin.ts that the review names (① item 2, escalation 2): "the route that would serve it fails it closed" overstates for the session-serving routes. Correct it to what is true on each route.

No accept-set change rides on any of these. After the push, the seat runs the re-review on the new head. Its surface is the two prose faces and the comment.


Generated by Claude Code

…g, and correct the lifecycle-arm comment

The changeset now grades @objectstack/service-storage minor, carries the
narrowing arm, the BREAKING banner and the ADR-0087 disposition, in the form
of this package's own precedent for the same ADR-0069 act. The lifecycle-arm
comment in the gate reader now says what is true on each route. No code
change.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot changed the title fix(service-storage): the family gate and the route judge the same credential (#24366) fix(service-storage)!: the family gate and the route judge the same credential Oct 18, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8ab3f986c351d5bb184c344bb9119731492396ce
Local-runs: none
Scope: packages/services/service-storage/src/storage-service-plugin.ts, packages/services/service-storage/src/storage-routes.family-gate-credential.pin.test.ts, packages/services/service-storage/src/storage-routes.api-key-account-posture.pin.test.ts, packages/services/service-storage/src/storage-routes.request-context-door.pin.test.ts, scripts/engine-double-contract.pinned.json + packages/services/service-storage/src/storage-routes.ts, packages/core/src/security/api-key.ts, packages/core/src/security/resolve-authz-context.ts, packages/core/src/security/auth-gate.ts, packages/core/src/security/authz-store-unavailable.ts, docs/adr/0069-enterprise-authentication-hardening.md, content/docs/permissions/authentication.mdx, .changeset/24366-storage-gate-credential.md, packages/services/service-storage/CHANGELOG.md, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, scripts/pm/clause2-line.mjs, scripts/pm/record-recognisers.mjs, .github/workflows/pr-automation.yml, AGENTS.md

Second record on this PR, after the FAIL at 6254478003 on head 82adc50a4 (② alone) and the patch-round-2 rework (6254490409). Read: the card and its seven comments (the triage re-grade 6254427208 and the round-2 dev report 6254808750 included), the live PR title and body, the delta 82adc50a4..8ab3f986c (one commit), the net diff against the merge-base with main (5da2e8881), the cited code at this head, and the head's check-runs last. This record stands on its own; where it refers to 6254478003 the reading was re-checked against this head. It states rules and their consequences only, as the disclosure ruling (5323310017) binds.

① Derived judgments

  1. The delta moves no code path. 82adc50a4..8ab3f986c touches two files: .changeset/24366-storage-gate-credential.md (a prose face) and one comment block in buildAuthGateSessionReader (storage-service-plugin.ts, the lifecycle-arm note beside the resolveApiKeyAdmission call). No statement changes. The net diff against the merge-base lists the same seven files as before, and every hunk outside those two is byte-identical to the head 6254478003 read. Right.

  2. The accept-set and public-surface readings of 6254478003 ① stand on this head, verdict unchanged: right. In one paragraph, so this record carries them: no export, type, option, schema, error code or route path moves, and all ten routes stay behind mountFamilyRoute; the one code change (unchanged since 82adc50a4) removes the gate reader's early return on a seam answer that is neither held nor none, so the door judges the session's own posture on every request while a gate is active; a held session with any other credential beside it is refused 403 with the session's posture on every route, which is the card's acceptance in its own words; the held-key arm is unchanged and control-pinned; the parent-governed download is the one route where the refusal goes a step past the measured gap, asked for by the card's "every route of the family"; the door's declared 503 outage relay is now reachable on key-carrying requests while a gate is active; it is a pure narrowing (the door never admits, it refuses or defers, so reading one more credential only adds refusals); C′ item 3 (6159450584) is met in its letter and within the family's constraints, with the storage family the only direct seam caller outside packages/core; and the authentication.mdx sentence is true.

  3. The corrected lifecycle-arm comment is true on each route it names. The new text: a key its own lifecycle refuses "carries no posture. The parent-governed download, whose composition reads keys, refuses it with its own code; the session-serving routes (uploads, the unclaimed-file download) never read a key and answer by the session alone; the raw-transfer routes answer by their capability token."

    • Parent-governed download: resolveAuthzContext raises the branded ApiKeyLifecycleRefusalError on the lifecycle outcome and never falls through to the session; buildFileReadAuthorizer's catch re-raises it by brand; authorizeDownload in storage-routes.ts relays err.status and err.code (the 403 and the ledger code the refusal carries). "Refuses it with its own code": true.
    • Session-serving routes: requireUploadSession and requireDownloadSession call buildAuthSessionResolver, which reads getSession(headers) and nothing else. "Never read a key and answer by the session alone": true.
    • Raw-transfer routes: both _local/raw/:token handlers call localAdapter.verifyToken(token, verb) and read neither a session nor a key; the family door still runs before them, as the door's own text says. "Answer by their capability token": true.
      Right. No accept-set change rides on the comment.
  4. The changeset's behavioural statements are true against the code. "Refused now": a held session gets 403 with the gate's code and message on every storage route whatever else the request carries, before the family does anything else; the three codes it lists are the closed AuthGateCodeSchema vocabulary. "While a gate is active, a session that cannot be read is the family's declared 503 (SERVICE_UNAVAILABLE)": AuthzStoreUnavailableError carries AUTHZ_STORE_UNAVAILABLE_STATUS 503 and AUTHZ_STORE_UNAVAILABLE_CODE SERVICE_UNAVAILABLE, relayed by refuseHeldSession. "Unchanged": an un-held session served as before apart from that outage answer; an account-scope held owner's key refused as before; nothing changes while no gate is active. All three hold on the code as read in ① item 2 of 6254478003. Right.

② Semver level

Right on this head. Judged fresh against AGENTS.md (line 1088), check-changeset-no-major.mjs, check-adr-0087-registration.mjs, clause2-line.mjs, and the same-package precedent 08b38cb in packages/services/service-storage/CHANGELOG.md.

  • Level: minor. The act is an accept-set narrowing of a published route family (① item 2); AGENTS.md grades (narrowing) BREAKING, and a breaking change ships minor in the launch window. 08b38cb graded the same ADR-0069 act on this package minor. Right.
  • Arm: Clause-②: no (narrowing). In the changeset, at a line start, with a one-line reason after an em dash, the precedent's spelling. In the PR body, line 3, bare Clause-②: no (narrowing). readClause2Line reads both as declared, value no, arm narrowing (readArmToken takes the exact (narrowing)), so the level axis is carried and minor discharges it; Check Changeset is green on this head on both of its runs. Right.
  • Banner. **BREAKING** (an accept-set narrowing), shipped as minor under the launch-window convention for breaking changes. is the 08b38cb line to the word. breakingDeclaration reads three signals on this changeset (BREAKING, bang, clause-②-narrowing), none of them major. Right.
  • ADR-0087 marker. Exactly one HTML-comment marker, not-required (no-migration-prescription) followed by its why, which readDisposition parses to that category; the why closes the other categories on facts (the package publishes; no ADR-0087 id covers these routes and none is added; no published interface or type changes), the same structure as 08b38cb's with the family named in the singular. The category's re-validation refuses only a changeset whose own body carries a migration prescription (a FROM-to-TO rewrite in arrow or table form); this body carries none, its "What changes for you" paragraph is user remediation, as the precedent's was. Lint · CI, release & engine gates and Lint & Repo Gates, which run the registration gate, are green on this head. Right.
  • Bang. The changeset summary reads fix(service-storage)!: and the live PR title reads fix(service-storage)!: the family gate and the route judge the same credential; the precedent's commit type was fix(services)!:. Consistent across the two carriers. Right.
  • One residue, owed before landing, no head move. The live body's "Why it is a narrowing" paragraph still ends with the words Clause-②: no inside a code span, left from round 1. Mechanically it declares nothing: the reader returns the first declaration line (line 3) and treats a mid-sentence key as an inline-key residue that is quoted only when no declaration reads. But it is a public sentence that now contradicts the declaration two lines above it. One body edit (no (narrowing) there, or drop the span) fixes it; it moves no head and re-owes no review, since the body is not a face this record reads code from. Not a FAIL.

③ Boundary flags

Implemented-by: claude/issue-24366-storage-gate-credential
Reviewed-by: session_01Nk17Z26X5DB59iQztDBADH

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-18T20:56Z · head 8ab3f986c3. Contract review PASS 6254884596 at CONTRACT_REVIEW_TIER on this head covers the whole diff, on the narrowing face. It follows FAIL 6254478003 on 82adc50a4 (② alone). This seat review records the rest. It lands through the merge queue once every check on this head is green. ⚠️ No reproduction here, under the disclosure ruling (#9387, 5323310017).


Generated by Claude Code

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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: a session the ADR-0069 auth gate holds can still be served by the storage route family (detail withheld under the disclosure ruling)

2 participants