Skip to content

v0.1.25.47 — TENANT_CLOSED Rule 2 guard + webhook matcher parity

Choose a tag to compare

@amavashev amavashev released this 10 Jul 18:55
eb6db8b

Governance conformance release: the runtime half of the tenant-close cascade (Rule 2 terminal-owner guard) plus webhook scope-filter matcher parity with the admin plane.

Added

  • 409 TENANT_CLOSED on the four reservation mutations (create, commit, release, extend) once the owning tenant's status=CLOSED flip is durable in the shared Redis. Implemented inside the reserve/commit/release/extend Lua scripts — atomic with the budget mutations and immune to the tenant-config cache TTL. Implements governance CASCADE SEMANTICS Rule 2 / Mode B invariant (a); runtime spec revision v0.1.25.13 (runcycles/cycles-protocol#125). Closes the two paths the auth layer could not: admin-on-behalf-of release on a closed tenant (previously succeeded; now 409) and the auth-check→script race window. Tenant-key requests on closed tenants keep their existing per-request 401.
  • Truthful non-persisting evaluations: a fresh dry_run create or POST /v1/decide on a CLOSED tenant returns 200 decision=DENY with reason_code=TENANT_CLOSED (new DecisionReasonCode value) instead of what could previously be a signed ALLOW attestation for a request whose live execution must fail. Cached pre-close replays keep their original payload.
  • Fail-closed governance reads: a present-but-malformed tenant record (undecodable JSON, non-object, missing status, or a status outside the closed ACTIVE|SUSPENDED|CLOSED enum) fails the request 500 INTERNAL_ERROR with no mutation. Absent tenant record (runtime-only deployments) ⇒ no guard, unchanged behavior.
  • Signed denial receipts: mutation-surface 409 TENANT_CLOSED (persisting create, commit, release) emits an error CyclesEvidence envelope and stamps cycles_evidence, like the other budget/lifecycle denial codes (declared in evidence ErrorResponseMirror, cycles-evidence-v0.2.yaml 0.2.1). TENANT_CLOSED takes precedence over RESERVATION_FINALIZED/RESERVATION_EXPIRED for non-replay attempts; same-key idempotent replays return their original response.

Fixed

  • Webhook scope_filter matching is now byte-identical to the admin plane's spec-conformant matcher (cycles-server-admin #206): a blank/whitespace event scope is treated as unscoped (excluded from every scope-filtered subscription — a bare * filter no longer matches a blank "" scope), and a trailing-/* filter requires a non-empty child segment (tenant:a/* no longer matches the degenerate tenant:a/). Both planes' matchers now pin the same test table.

Compatibility

  • Additive ErrorCode and DecisionReasonCode value (TENANT_CLOSED), per runtime spec v0.1.25.13. Deployments without a governance plane see no behavior change; closed tenants previously surfaced only as auth-layer 401s or budget-state errors.
  • Matcher change affects runtime webhook dispatch only for blank-scope events and empty-child trailing-wildcard filters.
  • No HTTP wire-shape, Redis schema, or event-wire change. The Lua mutation scripts gain one in-script GET tenant:<id> + decode per call — no additional network round trip.

1,058 tests green including integration + contract validation against spec v0.1.25.13; JaCoCo ≥95% line coverage on all modules; six external review rounds. Full details: CHANGELOG.md [0.1.25.47], AUDIT.md 2026-07-10 entries, PR #231.