docs(specs): assign the stale-key re-run to the S17 write path
What
S04-A said the caller re-acquires the namespace key and retries when the
ns_key_id FK refuses a credential write stamped from a retired key. It named
no caller, and no caller does it. This assigns the re-run to the owner that can
perform it, and records the obligation on that owner's own spec.
Ownership is split rather than moved wholesale: the S17 management write path
owns the rollback and the re-run, while the key-cache eviction hook that makes
a re-run recover rather than repeat the refusal belongs to internal/crypto
and to S04-B's rotation and retirement tooling.
Raised in review on !2553 (merged), where the sentinel this recovery is built on has no consumer outside tests.
Why the datastore write could not be the owner
The rejection aborts the transaction the datastore credential-write helper runs in, so that helper cannot re-acquire a key and re-issue the write inside the same call. It logs the rejection and returns its format's stale-key sentinel.
The management write path specified in
S17
composes credential writes into one transaction with the url write, which
makes it that transaction's owner rather than a layer outside it. Its
recovery is therefore a rollback and a re-run of the whole operation, not a
re-issue of the one write: the url write, the last_health_status and
last_health_checked_at resets and, on create, the kind-agnostic and
per-format child inserts all re-run with it.
The re-run is also not immediate. Step 1 resolves the key through
KeyManager.GetKey, which serves a live cache entry without touching the
store, so a key another pod retired is re-served until that entry's TTL lapses
and a prompt re-run is refused identically. Closing that needs an eviction hook
on the key manager, internal/crypto's to expose and S04-B's rotation and
retirement tooling to call.
What changed
Two files. Line numbers are at 85f21cd8e.
docs/specs/S04-a-column-level-encryption.md, one commit per review finding
across both rounds:
| Passage | Change |
|---|---|
:381 |
FK-rejection paragraph: names the S17 write path, why the helper cannot re-run, and that the re-run waits on the key cache |
:1454 |
Write path: the helper does not retry and returns its format's stale-key sentinel, named rather than described |
:1463 |
Recovery is S17's rollback and re-run, with what re-runs alongside it, and the key-cache window that refuses a prompt re-run |
:1491 |
The construction claim names remote.CredentialManager, the one undefined component name the rename pass left behind |
:1499 |
The constraint name keys the mapping; the retry site keys on the sentinel and never sees a constraint name |
:1511 |
The warn line is booked where the stamped key id is in hand, not where the rejection is classified |
:1815 |
Crypto-shredding passage: the same owner and key-cache corrections |
:2242 |
Error Cases row: splits the two actors and carries the same corrections in its one cell |
docs/specs/S17-rest-management-api.md, recording the obligation on the
receiving side and keeping the section it lands in self-consistent:
| Passage | Change |
|---|---|
:50 |
References gains the S04-A entry the list did not carry |
:85 |
Dependencies gains an S04-A row: the integration point, and the state measured at f9cd4b12f - the encrypted-credential columns and their validated check constraints on all three *_remote_repositories tables, each format's Err<Format>RemoteStaleNamespaceKey declared in internal/datastore, and no package outside internal/datastore consulting one |
:1008 |
Non-hosted repository CRUD (Phase 6): the refusal, the sentinel the datastore helper returns, the rollback and re-run this surface owes, the writes that re-run with it, the eviction enqueue the rollback does not undo, and the key-cache window that refuses a prompt re-run |
:1013 |
Points at the refusal's own Error Cases row rather than at the table's catch-all |
:1020 |
The eviction enqueue precedes the commit, matching the acceptance criterion at :1526 and the backend rule at :1022 |
:1624 |
Error Cases gains the Create and Update row for the refusal |
:1670 |
Security Considerations: the same enqueue ordering, stated through the mechanism that earns the claim |
The accepted failure window is written as a condition and its event, and it closes on two events: the S17 Phase 6 endpoints are what make the refusal reach a user, and the eviction hook is what makes a re-run recover rather than repeat the refusal.
The refusal now carries its own Error Cases row on Create and Update. That row
invents no code: it answers the same 500 and internal_server_error the
table's unexpected-server-failure catch-all answers, so what changed is that
the refusal is itemized rather than folded into the catch-all.
docs/dev/writing-discipline.md is why it
earns a row, under ## A contract table listing one status of a class lists the whole class: the table already itemizes the Connection test's 500, so
listing none of that class for Create and Update read as a deliberate
selection rather than a uniform absence. The gap sits in the row as a
condition and its event.
Accepted cost
Two costs, and they are different kinds. Each applies to a different pair of open MRs, and neither pair carries the other pair's cost.
Spec text that moved under work in flight. Two S17 Phase 6 step MRs are
open against
the Phase 6 plan:
!2665 (merged) (37/42) and !2666 (merged) (38/42), the markers read off those MRs' own titles
and the denominator re-counted from that plan's Status table at 85f21cd8e,
which carries 42 rows. They implement steps 37 and 38, virtual create and
delete for npm and container, which the same
Non-hosted repository CRUD (Phase 6)
section this MR edits specifies. The paragraphs this MR adds to that section
are remote-credential text, which a virtual create or delete does not execute,
so what those two get is a section that moved under them mid-flight rather
than behavior their code now has to serve. That was accepted over a separate
S17 amendment MR or a hard gate, so the obligation and the spec that has to
serve it land together. Neither carries any overlap cost:
git diff --name-only origin/main...origin/mr/2665 returns 22 files at
d5bb0662d and the same command for origin/mr/2666 returns 26 at
079197162, and no path in either list is under docs/specs/.
File overlap on the S17 spec. The two open MRs that do edit
docs/specs/S17-rest-management-api.md are !2652 (merged)
(1150/spec-s17-parents-count-detail-only), whose diff against origin/main
is that one file, and !2660 (merged) (1150/spec-tag-index-claims), whose diff is that
file plus docs/specs/S12-container-oci-hosted.md, which this MR does not
touch. Both come back exit 0 from
git merge-tree --write-tree --name-only --no-messages HEAD <mr head>,
re-derived at 85f21cd8e against 04aedb1c2 and 1f9dd80f5, so no merge
order is required. ## Merge order below carries the section-by-section
reason. Neither of these two is a Phase 6 step MR, so neither carries the
moved-under cost above.
What this does not do
- No plan change. The obligation belongs to the S17 management write path,
not to a new S04-A step, so
the per-format columns plan
keeps its 11 Status rows and the
4/11markers on !2553 (merged) and !2554 (merged) stay correct, re-counted atf9cd4b12f. - No work item. Recording the obligation in S17 is the first of the two options the review offered; the fallback, a work item cited from S04-A, is not used.
- No second copy of the store-side rules.
internal/datastore/maven_remote_repositories_errors.mdlanded with !2553 (merged) and already rules that nothing retries in the store, gives the key-cache mechanism, and names the eviction hook as the real blocker. This MR cites that document rather than restating it.
Merge order
None required, re-derived at 85f21cd8e: 76 open MRs, 0 unchecked heads. Four
of them edit one of the two files:
- !2662 (merged) (
hswimelar/s04a-container-drop-plaintext-columns) - S04-A - !2630 (merged) (
dmeshcharakou/s04a-per-format-credential-columns-step-5) - S04-A - !2660 (merged) (
1150/spec-tag-index-claims) - S17 - !2652 (merged) (
1150/spec-s17-parents-count-detail-only) - S17
All four come back exit 0 from
git merge-tree --write-tree --name-only --no-messages HEAD <mr head>.
Two heads needed a forced refspec to fetch at all, !2630 (merged) and !2622 (merged), both having
been force-pushed since the previous scan. A plain
refs/merge-requests/<iid>/head fetch skips a non-fast-forward head in
silence, so the scan reports them as unchecked rather than clean; + in front
of the refspec is what makes it apply. !2622 (merged) edits neither file.
On the S04-A side, both edit the identifier-limit section and the
check-constraint discussion under it, and nothing else: !2630 (merged) spans main :417
to :509, !2662 (merged) a single hunk at main :470 to :491. This MR's S04-A
passages are the FK-rejection paragraph, the write path, the
accepted-failure-window paragraph, the crypto-shredding passage and the Error
Cases row, so the two sets share no section. !2662 (merged)'s earlier hunk in the
crypto-shredding purge order, named here in a previous revision, is gone as of
its 7c4bef5f0 head.
On the S17 side, !2660 (merged) is a single hunk at main :748, in the Phase 3
tag-index discussion. !2652 (merged)'s hunks span the Phase 3 manifest and list
sections, the Acceptance Criteria block (main :1582 to :1592) and the
Resolutions block (main :1837 to :1950). Neither touches ## Error Cases
(main :1596 to :1647) or ## Security Considerations (main :1647 to
:1692), which is where this MR's :1624 and :1670 land, so the two new
S17 hunks sit in sections no other open MR edits.
!2642 (merged) edits the sibling S04-B spec, not either file. !2542 (merged) and !2543 (merged), named here in an earlier revision, have both merged.
Checks
markdownlint-cli2, vale and lychee run individually against both files,
all three passing on both at 85f21cd8e. lychee runs with
--include-fragments, so the cross-spec anchors resolve in both directions:
S04-A's #non-hosted-repository-crud-phase-6,
#remote-repository-settings-phase-6 and #dependencies into S17, and S17's
#write-path into S04-A alongside its own #error-cases and
#non-hosted-repository-crud-phase-6.
Related to #1212 (closed)