docs: TENANT_CLOSED runtime conformance + scope-filter matcher parity (spec v0.1.25.13 / server 0.1.25.47)#787
Conversation
Runtime spec v0.1.25.13 added TENANT_CLOSED to the runtime ErrorCode enum
with a normative closed-tenant binding, and cycles-server 0.1.25.47
implements it (plus DecisionReasonCode TENANT_CLOSED on fresh dry-run /
decide evaluations, and the webhook scope-filter matcher refinements).
Close the "reference implementation gap" callout, version-scope the old
behavior (<= 0.1.25.46), and add TENANT_CLOSED to every complete runtime
error-code / reason-code enumeration.
- protocol/tenant-close-cascade-semantics.md: rewrite "What the runtime
plane sees" around the shipped guard (409 on persisting reservation
create/commit/release/extend, precedence over reservation-state errors,
replay exception, 200 DENY reason_code=TENANT_CLOSED on fresh
dry-run/decide, 500 fail-closed on malformed tenant records, error
CyclesEvidence on create/commit/release, usually-401-first note); add
runtime rows to the version gate matrix and backward-compat list.
- protocol/webhook-scope-filter-syntax.md: document the 0.1.25.47 runtime
matcher refinements (blank scopes unscoped; trailing-/* requires a
non-empty child), converging with the queued admin fix on a shared
pinned test table; version-scope the old runtime edge cases.
- blog/tenant-lifecycle-cascade-semantics-at-scale.md: replace the
"does not yet include TENANT_CLOSED" parenthetical; clarify the
409-vs-401 window in the example comment.
- how-to/tenant-creation-and-management-in-cycles.md: runtime-plane
sentence after the cascade paragraph; troubleshooting entry notes both
planes.
- protocol/error-codes-and-error-handling-in-cycles.md: 16 -> 17 runtime
wire codes; runtime-plane paragraph under TENANT_CLOSED (409); update
the four reservation-operation tables to cite cycles-server 0.1.25.47 /
spec v0.1.25.13; remove the incorrect TENANT_CLOSED row from the Event
errors table (the guard does not cover /v1/events) and add a note;
closed-tenant note in Decide errors; TENANT_CLOSED reason-code row;
INTERNAL_ERROR fail-closed variant.
- protocol/implement.md, protocol/api-reference-for-the-cycles-protocol.md,
protocol/how-decide-works-*.md, protocol/dry-run-shadow-mode-*.md,
protocol/event-payloads-reference.md, glossary.md: add TENANT_CLOSED to
the error-code and reason-code enumerations ("six" -> "seven" known
DecisionReasonCode values).
- changelog.md: refresh the spec-revision rows (runtime v0.1.25.13,
governance v0.1.25.37) and add the v0.1.25.13 spec-suite bullet.
cycles-server row stays at v0.1.25.46 (0.1.25.47 is not yet tagged).
Merge-gated (draft) on the cycles-server v0.1.25.47 release tag and the
next cycles-server-admin release; "queued" phrasings in the scope-filter
docs get a final s/queued/shipped-in-X.Y.Z/ pass before merge.
…ime row Codex external review (REVISE-MINOR) on PR #787. | # | Finding | Action | |---|---|---| | 1 | reservation.released_via_tenant_cascade example showed per-reservation fields | APPLIED — data block rewritten to the verified emission (ledger_id, scope, unit, released_amount, cascade_reason="tenant_closed"; TenantCloseCascadeService.emitReservationReleaseEvent). Sweep found the same bug class in the three sibling examples on the page: budget.closed (final_* / closed_at / cascade_origin -> prior_status/new_status/cascade_reason + actor type "admin"), api_key.revoked (revoked_at/cascade_origin -> prior_status/new_status/cascade_reason), webhook.disabled (url/disabled_at/cascade_origin -> prior_status/new_status/name/cascade_reason) — all fixed against the emitter; added a shared EventDataTenantCascade shape note. No other page carries a cascade-event JSON example. | | 2 | Blog Rule 1 "strictly ordered sequence ... flip LAST" contradicts Mode B | APPLIED — sequence scoped as the Mode A presentation; explicit note that Mode B (the runcycles implementation) inverts it (flip first, children converge under Rule 2). No restructuring. | | 3 | "Release-like language" for cycles-server 0.1.25.47 | SKIPPED — PR is draft-gated on the v0.1.25.47 tag (MERGE GATES item 1); prose is accurate at publish time by construction. | | 4 | changelog.md minimum-versions row lacked the runtime half | APPLIED — split into admin-plane row (unchanged content, retitled) + new runtime-guard row: cycles-server v0.1.25.47 (runtime spec v0.1.25.13). Latest-tag rows stay v0.1.25.46 per gate 1. |
Codex review round 1 — REVISE-MINOR — resolved in 0de2e39
|
Codex round 2 (REVISE, one P2): the round-1 blog caveat said the commit
example's 409 TENANT_CLOSED is reachable with "an admin key", but the
runtime admin dual-auth allowlist (AdminApiKeyAuthenticationFilter:
GET /v1/reservations, GET /v1/reservations/{id},
POST /v1/reservations/{id}/release) exposes only RELEASE of the four
guarded mutations to X-Admin-API-Key — create/commit/extend are
deliberately not admin-reachable.
| Hit | Verdict |
|---|---|
| blog commit-example caveat | APPLIED — tenant-key post-flip window is the reachable path for commit; note that only release also accepts an admin key (admin-on-behalf-of release hits the 409 with no race) |
| tenant-close-cascade-semantics.md observability note ("admin-key mutations") | APPLIED — now: admin-on-behalf-of release (only admin-exposed guarded mutation; allowlist is list/get/release) + tenant-key race window for any of the four |
| error-codes TENANT_CLOSED runtime paragraph (same phrase) | APPLIED — same correction |
| api-reference lines 10/24, glossary AdminKeyAuth/Admin-on-Behalf-of, implement.md dual-auth note, changelog v0.1.25.8 rows | correct-as-is — pre-existing text already states list/get/release only |
| error-codes + api-reference op-table TENANT_CLOSED rows, how-to runtime sentences | correct-as-is — no key-type claims |
Codex review round 2 — REVISE (one P2) — resolved in 2e8018cFinding (P2, applied): the round-1 caveat on the blog's commit example said the Claim-class sweep across the PR's changed files (statements naming which key types can hit
Codex verdicts so far: round 1 REVISE-MINOR → applied (0de2e39); round 2 REVISE (this P2) → applied (2e8018c). Everything else from round 2 confirmed: cascade payload fields match |
|
External review (codex) complete: SHIP after round 3. Round 1 (REVISE-MINOR): all four tenant-cascade event JSON examples rewritten against the actual TenantCloseCascadeService emitter; blog Rule 1 sequence scoped as Mode A with the Mode B flip-first note; changelog minimum-versions row split admin-plane vs runtime-guard; 0.1.25.47 release-language flag skipped with reasoning (draft is merge-gated on the tag). Round 2 (P2): 409 TENANT_CLOSED key-type reachability corrected — of the four guarded mutations only release is admin-key reachable (AdminApiKeyAuthenticationFilter allowlist); create/commit/extend reach the 409 only via the post-flip/pre-revocation tenant-key window. Round 3: confirmed clean. PR remains DRAFT per the merge gates: (1) cycles-server v0.1.25.47 tag, (2) next cycles-server-admin release tag + final queued→shipped pass on the scope-filter page. |
… tagged
Both merge gates for this branch are satisfied: cycles-server v0.1.25.47
(TENANT_CLOSED Rule 2 guard + webhook matcher parity) and
cycles-server-admin v0.1.25.49 (spec-conformant webhook scope-filter
matching + replay filtering) are release-tagged 2026-07-10.
- protocol/webhook-scope-filter-syntax.md: every "queued for the next
cycles-server-admin release" phrasing replaced with the shipped version
(admin 0.1.25.49); the divergence callout leads with "Resolved in
current releases" and links the release's migration notes; softened
::: danger -> ::: warning now that the divergence is version-scoped
history rather than a live issue in current releases (matches the
page corpus convention: danger for issues live in latest, tip/warning
for resolved version-scoped behavior); syntax-summary spec column and
Examples intro note both-planes conformance on 0.1.25.49+/0.1.25.47+.
- changelog.md: current-version rows -> cycles-server v0.1.25.47 and
cycles-server-admin v0.1.25.49 (2026-07-10); new release entries in
both component sections (styled like neighbors, admin entry carries
the BEHAVIOR CHANGE migration warning); compatibility-matrix rows
bumped. Minimum-versions rows were already consistent.
- quickstart/architecture-overview-how-cycles-fits-together.md: the
"latest tagged versions" line updated to the 2026-07-10 tags (its
prior correct-as-is verdict was justified by the tags not existing).
Corpus sweep for the queued/pending class ("queued", "next
cycles-server-admin release", "pending release", "not yet tagged",
"untagged", "fix ships"): zero release-pending survivors — every
remaining "queued"/"PENDING" hit is delivery-queue or
graceful-degradation semantics (verdict: correct-as-is).
PR stays a draft per owner instruction; body updated to mark both
gates satisfied.
Merge gates satisfied — final version-stamp pass in a0d98ffBoth gates landed today: cycles-server v0.1.25.47 and cycles-server-admin v0.1.25.49 (verified via
Sweep (queued/pending class — "queued", "next cycles-server-admin release", "pending release", "not yet tagged", "untagged", "fix ships"): zero release-pending survivors. Every remaining corpus hit is delivery-queue or graceful-degradation semantics (webhook PR body updated: MERGE GATES → both satisfied with tag links. Content is final; the PR remains a draft per owner instruction — ready to merge pending owner approval. |
Reviewer round on PR #787: High (changelog rows stale) rejected — branch head a0d98ff already carries v0.1.25.47/v0.1.25.49 at changelog.md:20-21 and :304-305; reviewer checkout was stale. Medium applied: - how-to/tenant-creation-and-management-in-cycles.md: the runtime-plane sentence after the cascade paragraph now carries the auth-layer caveat (tenant-key calls usually 401 at the auth filter before the guard; the runtime 409 surfaces on admin-on-behalf-of release and the post-flip/pre-revocation race window), cross-referencing the cascade page's observability note instead of duplicating it. The v0.1.25.36+ version-table row is narrowed to the admin plane ("Any admin-plane mutation attempt returns 409 TENANT_CLOSED; runtime tenant-key mutations usually fail 401 first"). - changelog.md 0.1.25.47 entry: same one-clause qualifier added (sweep hit of the same class). - changelog.md compatibility cell "Cycles Admin Server | v0.1.25.49 | Governance spec v0.1.25.34" left as-is deliberately: that column tracks the component's own declared spec alignment, and the admin repo's README on current main (post-0.1.25.49) still declares "Aligned with Cycles Protocol v0.1.25.34" — governance .35-.37 are additive revisions admin has not re-declared against. The separate "Current component versions" row for the governance spec itself correctly reads v0.1.25.37. Sweep (runtime-409-without-401-qualifier class) across the changed-file set: remaining "returns 409" hits are per-endpoint error-table cells, the summary code-by-status enumeration, normative spec statements inside the section that carries the observability note, glossary/blog admin-plane contract descriptions, and version-gate capability rows — all correct-as-is (they enumerate conditions, not observed likelihood).
Reviewer round — High rejected as stale, Medium applied in 40161fdHigh (changelog.md current-version rows still v0.1.25.46/48): REJECTED — stale checkout. On branch head
No change needed. Medium (missing 401-first caveat on the how-to runtime sentences): VALID — applied.
Compatibility cell check ( Claim-class sweep (runtime mutations "return 409" near closed/cascade without the 401-first qualifier), changed-files set:
PR stays a draft; content ready pending owner approval. |
…0.1.25.37) cycles-server-admin #207 (merge abda6a9) bumped the admin README's declared governance-spec alignment v0.1.25.34 -> v0.1.25.37 (verified per-revision upstream: .35 EventDataTenantCascade schema, .36 admin_on_behalf_of actor, .37 documentation-only). The changelog.md compatibility cell mirrors the component's declared alignment, so it follows: "Cycles Admin Server | v0.1.25.49 | Governance spec v0.1.25.37" — now consistent with the current-versions table's "Governance spec (admin) | v0.1.25.37" row. Corpus sweep for other declared-alignment-as-.34 citations: none — the remaining "0.1.25.34" hits are the admin v0.1.25.34 release entry, the dashboard v0.1.25.34 component release, and the dashboard v0.1.25.59 historical spec-alignment note, all correct-as-is.
Reviewer Medium (compatibility cell) — now resolved in 0c7a77c via the upstream fixThe earlier "left as-is" verdict on The cell now follows the corrected declaration — Sweep for other citations of the admin plane's declared alignment as PR remains a draft; ready pending owner approval. |
examples, no emission-order guarantee, observable-outcome scoping Five findings, each verified against the yaml specs (authority) and the admin implementation before editing. 1 (MEDIUM, applied + class sweep): the four *_via_tenant_cascade names ARE declared in the governance spec's EventType enum since revision v0.1.25.35 (cycles-governance-admin-v0.1.25.yaml lines 1625/1641/1663/ 1658; enum now 51 values; the runtime spec defines no EventType enum at all — event_type is a free dotted string there). Audit entries are MUST / RESERVED; Event emission is SHOULD. Rewrote the "absent from the published spec enum" claim and every variant, and re-derived the counts: - event-payloads-reference.md: intro (47->51, per-category counts), currently-emitted list gains the cascade row, cascade section intro + heading (now "governance spec v0.1.25.35+"), emission-summary table (per-category totals + 51), "additive reference-server payloads" list now correctly covers only reservation.reserved/.committed/.released/ .extended + event.applied (verified genuinely absent from the enum). - tenant-close-cascade-semantics.md + how-to/tenant-creation: enum claims corrected; "do not rely on cross-server event_type= filtering" replaced (filtering is spec-valid; emission is SHOULD-level). - webhook-event-delivery-protocol.md: description + "Event types (51)" + per-category counts + tenant-accessible 27-of-47 -> 29-of-51 + fan-out section rewrite. - concepts/webhooks-and-events.md (heading, table, 29-of-51), admin-api/guide.md, how-to/webhook-integrations.md (29-of-51), how-to/managing-webhooks.md (category counts 17/6), how-to/migrating-from-custom-rate-limiter (51), prometheus-metrics (51 twice), glossary Event entry, blog/real-time-budget-alerts live count (dated as-of-post-date figures left untouched). 2 (MEDIUM, applied): WebhookCreateRequest requires event_types with minItems 1 (yaml :2591/2608; server @notempty). Fixed the two invalid examples on webhook-scope-filter-syntax.md (event_types: [] and the omitted-both form): the "everything" form is a representative type + all-categories event_categories (union semantics), cross-referencing managing-webhooks' category-based subscriptions. Sweep: no other page shows an empty/omitted event_types create example. 3 (MEDIUM, applied): the governance spec has NO cascade event-emission ordering guarantee (its ordering language is the Mode A mutation-order SHOULD); the implementation interleaves budget.closed + the reservation aggregate per budget, then webhooks, then keys. Rewrote the event- payloads "Ordering guarantee" paragraph: order is implementation- defined, do not rely on arrival order, join via correlation_id. Sweep: no other order-reliant statements (the cascade page's Rule 1 ordering text is about mutations and already says "do not rely on this"). 4 (LOW, applied): blog "same client-observable outcome ... every mutation returns 409" scoped to admin-plane mutations, with the runtime post-auth/401-first door noted per the post's own walkthrough. Class re-sweep (observable outcome / uniform / identical): cascade page's variant already says "admin-plane mutations"; no other hits. 5 (NIT, applied): changelog.md `dry_run`//v1/decide markup fixed.
Reviewer round 2 — all five findings resolved in 73d0d69Each finding was verified against the yaml specs (authority:
PR remains a draft; ready pending owner approval. |
v0.1.25.38 / admin 0.1.25.50)
The reviewer flagged three tenant-self-service-boundary statements as
currently-false; the upstream fix landed — governance spec revision
v0.1.25.38 (cycles-protocol#126) makes category validation NORMATIVE on
both POST /v1/webhooks and PATCH /v1/webhooks/{id} (an admin-only
event_categories entry MUST be rejected 400 INVALID_REQUEST, since
categories are additive with types in delivery matching), and
cycles-server-admin 0.1.25.50 (released, tagged) enforces it and closes
the legacy empty-both match-ALL update door. So the statements are now
true AND enforced.
Verified against cycles-governance-admin-v0.1.25.yaml (version 0.1.25.38,
TENANT-ACCESSIBLE BOUNDARY NORMATIVE block at ~line 7243; "29 of 51
types") and the admin 0.1.25.50 CHANGELOG entry.
- protocol/webhook-event-delivery-protocol.md (~157, normative-ish home):
added the one version-scoped note — boundary normative in spec
v0.1.25.38 (both event_types AND event_categories validated, 400 on an
admin-only entry, rationale: categories are additive); admin 0.1.25.50
enforces the category check + closes the empty-both door; 0.1.25.49 and
earlier validated event_types only; operators should audit existing
tenant subscriptions (release-notes link).
- concepts/webhooks-and-events.md, how-to/webhook-integrations.md,
admin-api/guide.md tenant-self-service lines: left as plain capability
descriptions (now simply correct; not over-annotated per the single-
note guidance). Counts were already 29 of 51 from the prior round.
- Fixed two stale "47-event-type" cross-reference mentions
(admin-api/guide.md, how-to/webhook-integrations.md related-links) to
51.
Tenant-context webhook-create examples (admin-api/guide, managing-
webhooks, webhook-integrations) all use tenant-accessible event_types
only, no event_categories, no admin category shown — no change needed.
Dated blog figure (real-time-budget-alerts, "27 of 41 as of post date;
live 29 of 51") left as-is — already points to the live count.
Tenant-webhook category-boundary claim — High resolved by upstream fix + release, in f23e03dThe reviewer's High (three docs statements said tenant self-service webhooks are restricted to budget/reservation/tenant with api_key/policy/webhook/system admin-only — accurate for
So the three statements are now true and enforced. Changes:
Sweep of the tenant-webhook-boundary claim class (self-service / admin-only / event_categories / accessible-type counts) across the corpus: no other statement implies categories are unvalidated or shows an admin category on a tenant subscription. Both tenant-context webhook-create examples (admin-api/guide, how-to/webhook-integrations, managing-webhooks) use tenant-accessible Also fixed two stale "47-event-type" cross-reference mentions (admin-api/guide related-link, webhook-integrations related-link) → 51, completing the 47→51 count sweep from the previous round. PR remains a draft; ready pending owner approval. |
Reviewer Medium: docs said the server rejects an empty event_types list. Wrong on two counts — category-only subscriptions (empty event_types + non-empty event_categories) are legal, and what the server actually rejects is the empty-BOTH state. Aligns docs to the Option-1 design (governance revision v0.1.25.39, pending; shipped in cycles-server-admin 0.1.25.50). Verified against the shipped impl (governance v0.1.25.39 not yet on protocol main — main is 0.1.25.38): - WebhookCreateRequest.eventTypes = @notempty -> create requires non-empty event_types. - WebhookUpdateRequest.eventTypes = no validation annotation. - WebhookService.update guard rejects only empty-BOTH: "Subscription must retain at least one event_type or event_category" (400 INVALID_REQUEST). So the create/update asymmetry is real: create needs event_types; PATCH may clear event_types when event_categories is non-empty. - protocol/webhook-scope-filter-syntax.md ~95: rewrote the "event_types required / server rejects empty list" line to the real rule (at least one of the two selectors must be non-empty; empty-both -> 400; create needs event_types, update may go category-only). Fixed the "receive everything" note's "cannot be empty or omitted (minItems: 1)" claim the same way. - how-to/managing-webhooks.md: scoped the create "required" line and the category-note "always required" to create; added a category-only PATCH example (event_types: [] + event_categories) to the update section. - how-to/webhook-integrations.md: budget category count 16 -> 17 (stale residual from the 47->51 sweep; budget.closed_via_tenant_cascade is a budget.* type delivered to a budget category subscription). Sweep (event_types-always-required / empty-rejected / must-name-a-type): managing-webhooks:18 + :70 fixed (were blanket/"always"); create-only statements left correct; no other hits. Both tenant-context create examples still use non-empty event_types (create path) — unchanged.
Medium (empty-
|
| Hit | Verdict |
|---|---|
webhook-scope-filter-syntax.md:95 ("required, minItems: 1, rejects empty list") |
Updated — real rule (type-or-category; create-vs-update asymmetry) |
| webhook-scope-filter-syntax.md:137 ("cannot be empty or omitted") | Updated — scoped to create; category-only possible on PATCH |
managing-webhooks.md:18 ("Required fields: event_types (at least one)") |
Updated — scoped to create; added the update/category-only rule |
managing-webhooks.md:70 ("event_types is always required") |
Updated — "always" was the overreach; scoped to create |
| managing-webhooks.md create examples, tenant-context create examples (admin-api/guide, webhook-integrations) | Correct-as-is — all on the create path, which does require non-empty event_types |
No other hits corpus-wide. PR #787 stays a draft; ready pending owner approval.
…v0.1.25.39 Releases moved while the PR sat as a draft. Verified against source: cycles-server-admin latest tag = 0.1.25.50; cycles-governance-admin yaml info.version on cycles-protocol main = 0.1.25.39 (PR #127 merged); admin README declares alignment with governance v0.1.25.39. - changelog.md current-version table: cycles-server-admin 0.1.25.49 -> 0.1.25.50 (2026-07-10); governance spec (admin) v0.1.25.37 -> v0.1.25.39. - changelog.md admin component section: new [0.1.25.50] entry above [.49] summarizing the SECURITY fix (tenant-plane event_categories validated against the tenant-accessible boundary; empty-both match-ALL update door closed; upgrade-past-.49 + audit note; release-notes link; category-only-valid-on-update per gov v0.1.25.39; provenance issue cycles-server-admin#209). The [.49] entry was already present. - changelog.md compatibility matrix: Cycles Admin Server v0.1.25.49 -> 0.1.25.50, governance spec v0.1.25.37 -> v0.1.25.39 (matches the admin README's declared alignment). - quickstart/architecture-overview: latest-tagged line date 2026-07-10 -> 2026-07-11, admin 0.1.25.49 -> 0.1.25.50. - protocol/webhook-scope-filter-syntax.md ~95: dropped "pending" from the governance v0.1.25.39 reference (merged on main). Sweep verdicts: - "0.1.25.49" current/latest: none. Remaining hits are historical provenance (scope-filter matcher parity shipped in .49; the .49 changelog entry; "0.1.25.49 and earlier validated event_types only" version-scoping; dashboard .49; the new .50 entry's upgrade-past-.49 note) or version-scoped matcher statements ("admin 0.1.25.49+") — all correct-as-is. - Governance ".37"/".38" as current: none. The two .38 refs are correct provenance (the revision that made the category boundary normative), not current-version claims. - ".39 (pending)" / pending governance: none remaining after the fix.
Version-currency reviewer round — High + Medium resolved in 487bd6fThe releases moved while this PR sat as a draft. Verified against source before editing:
HIGH — docs no longer point at the pre-security-fix admin release
MEDIUM — governance lineage current, "pending" dropped
Sweep (per-hit)
This resolves the reviewer's High (docs no longer point users at the pre-security-fix admin 0.1.25.49) and Medium (governance lineage current at v0.1.25.39, "pending" dropped). PR #787 stays a draft; ready pending owner approval. |
Follow-up docs for the TENANT_CLOSED runtime-conformance wave merged today: runtime spec v0.1.25.13 (runcycles/cycles-protocol#125 — TENANT_CLOSED on the runtime ErrorCode enum + normative closed-tenant binding; companion evidence 0.2.1, protocol-extensions 0.1.27, governance 0.1.25.37) and cycles-server 0.1.25.47 (runcycles/cycles-server#231 — the four-Lua-script Rule 2 guard + webhook scope-filter matcher parity with the admin #206 fix).
MERGE GATES — both SATISFIED (2026-07-10) — ready to merge pending owner approval
cycles-server— SATISFIED: v0.1.25.47 — TENANT_CLOSED Rule 2 guard + webhook matcher parity is tagged (Docker image published, benchmark gate passed).v0.1.25.47release tag existschangelog.mdnow carries the 0.1.25.47 entry and the current-version row.The next cycles-server-admin release (first after 0.1.25.48) is tagged— SATISFIED: v0.1.25.49 — spec-conformant webhook scope-filter matching + replay filtering is tagged (its full-stack compose pins server 0.1.25.47). The finals/queued/shipped-in-0.1.25.49/pass overprotocol/webhook-scope-filter-syntax.mdis done, the divergence callout is now version-scoped history (softened::: danger→::: warning), andchangelog.mdcarries the admin 0.1.25.49 entry.The PR remains a draft per owner instruction; content is final and ready to merge pending owner approval.
Per-file summary
Reference implementation gapwarning: "What the runtime plane sees" now documents the shipped guard — 409TENANT_CLOSEDon persisting reservation create/commit/release/extend once the CLOSED flip is durable; precedence overRESERVATION_FINALIZED/RESERVATION_EXPIREDfor non-replay attempts (same-key replays return the original response); fresh dry-run//v1/decide return200 decision=DENY reason_code=TENANT_CLOSED(attestation rationale); 500 fail-closed on malformed tenant records; no guard without a tenant record; error CyclesEvidence on create/commit/release (extend is not an evidence endpoint); "usually 401 first" observability note; version scope for 0.1.25.46 and earlier. Added a runtime row to the version-gate matrix and the backward-compat list./*requires a non-empty child) and that the runtime and queued-admin matchers are pinned to the same test table. Syntax-summary*and.../*rows and the Edge cases section are version-scoped (0.1.25.46-and-earlier behavior retained).TENANT_CLOSED" parenthetical now says implemented in cycles-server 0.1.25.47 (spec v0.1.25.13) with a 0.1.25.46-and-earlier note; the 409 example's comment clarifies it's the pre-revocation-window/admin-key path (revoked tenant keys 401 first).TENANT_CLOSEDentry now names both planes. No runtime row invented in the admin version-behavior table.TENANT_CLOSEDin v0.1.25.13). New runtime-plane paragraph underTENANT_CLOSED (409). Reserve/Commit/Release/Extend tables re-attributed from "(v0.1.25.35+)" (an admin version on runtime endpoints) to cycles-server 0.1.25.47 / spec v0.1.25.13 with precedence notes. Removed the incorrectTENANT_CLOSEDrow from the Event errors table (see discrepancies) and added a note. Decide-errors note covers the DENY reason + 500 fail-closed.TENANT_CLOSEDrow added to the DecisionReasonCode table.INTERNAL_ERRORdocuments the malformed-tenant-record fail-closed variant.TENANT_CLOSED(409) added to the MUST error-semantics list (scope, precedence, DENY rule, fail-closed, governance-plane exemption).409 TENANT_CLOSEDrows added to the create/commit/release/extend error tables; dry-run paragraph includes the closed-tenant condition; both "six documented known values" sentences are now seven incl.TENANT_CLOSED./v1/eventstable intentionally unchanged.TENANT_CLOSEDadded to the base reason-code enumerations ("six" -> "seven").reservation.deniedreason_codeknown values gainTENANT_CLOSED(fresh dry-run/decide DENYs, 0.1.25.47+ — matches governance spec 0.1.25.37's same addition).TENANT_CLOSEDentry now covers both planes with versions.Docs-vs-shipped discrepancies found beyond the planned updates
TENANT_CLOSEDon/v1/events(error-codes page, "(v0.1.25.35+)"). The runtime spec binding scopes the guard to the persisting reservation mutations only, and the server guards onlyreserve/commit/release/extend.lua—event.luahas no tenant-status check. A closed tenant surfaces on/v1/eventsas 401 orBUDGET_CLOSED. Row removed, note added.LIMIT_EXCEEDEDverification (requested): present in every enumeration meant to be complete — error-codes page (section + summary + intro count), implement.md MUST list, api-reference public-endpoint 429 rows. It is absent from the glossary (no entry; the glossary doesn't enumerate error codes individually beyond TENANT_CLOSED) and from the how-to error-handling "transient vs non-transient" tables — but those tables also lack other codes (401/403/BUDGET_FROZEN etc. depending on page) and are guidance tables, not complete enumerations, so TENANT_CLOSED was likewise not added there.Full-sweep hit table (zero survivors)
implementation gap,enum lacks,only through the cascade, TENANT_CLOSED0.1.25.48 and earlier,queued conformance, matcher claimsdoes not yet include0.1.25.46, tenant_closed0.1.25.46, TENANT_CLOSED0.1.25.46, TENANT_CLOSED humanizer0.1.25.460.1.25.46not yet implementnot yet implementedVerification notes
cycles-protocol-v0.yamlERROR SEMANTICS closed-tenant bullet, DecisionReasonCode KNOWN VALUES, createReservation DRY-RUN RESPONSE RULES, /v1/decide CLOSED TENANT block, CONFORMANCE.md) and cycles-server 0.1.25.47 changelog/README/Lua sources — not secondhand summaries.reservation.deniedclaim cross-checked against the server controllers (event emitted on any fresh DENY from dry-run create and /v1/decide) and governance spec changelog v0.1.25.37.#tenant-closed-409,#decision-reason-codes,#edge-cases, page paths) verified to exist.