Releases: runcycles/cycles-server
Release list
v0.1.25.48 — TENANT_CLOSED guard on POST /v1/events
Extends the tenant-close terminal-owner guard (TENANT_CLOSED) to the runtime event-emission endpoint.
Added
- 409
TENANT_CLOSEDonPOST /v1/eventswhen the owning tenant'sstatus=CLOSEDflip is durable.POST /v1/eventsis a persisting budget debit (post-only accounting — it directly mutatesremaining/spenton every budgeted derived scope and may accrue overdraft debt underALLOW_WITH_OVERDRAFT), so it is on the same runtime-plane persisting surface as the reservation mutations and is now guarded identically. The check runs insideevent.lua(byte-identical to the reserve guard): after idempotency-replay handling and before any debit, so a pre-close same-key replay still returns its original stored response while a fresh event on a CLOSED tenant is rejected. Fail-closed on a malformed/undeterminable tenant record (500INTERNAL_ERROR, no debit); no restriction when no tenant record exists (runtime-only deployments). Runtime spec revision: v0.1.25.14 (runcycles/cycles-protocol#128) addsPOST /v1/eventsto the closed-tenant binding.- Exposure closed: narrower than the reservation surface —
/v1/eventsis not on the admin-key allowlist, and tenant keys on a durably-closed tenant are already 401'd at the auth filter. The guard closes the remaining auth-check→script race window (Mode B invariant (a)). - No error-evidence:
/v1/eventsis outside the evidence-endpoint set (an accounting endpoint, not a decision endpoint) — consistent with its other denial outcomes, which also emit none.
- Exposure closed: narrower than the reservation surface —
Compatibility
- Additive.
TENANT_CLOSEDis a pre-existingErrorCode;/v1/eventsalready declared a 409. No wire, Redis, event, or evidence schema change; the existinghandleScriptErrormapping already routesTENANT_CLOSEDto 409, so no Java change was required. Deployments without a governance plane are unaffected. - Release prep: prod + full-stack compose self-pin bumped
0.1.25.46 → 0.1.25.48(the bundled stack had not been bumped since .46 and predated the tenant-close guards; upgrade recommended).
1,066 tests green including integration + contract validation against runtime spec v0.1.25.14; JaCoCo ≥95% on all modules; external review (codex) SHIP. Full details: CHANGELOG.md [0.1.25.48], AUDIT.md 2026-07-11 entries, PR #232.
v0.1.25.47 — TENANT_CLOSED Rule 2 guard + webhook matcher parity
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_CLOSEDon the four reservation mutations (create, commit, release, extend) once the owning tenant'sstatus=CLOSEDflip 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-ofreleaseon 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_runcreate orPOST /v1/decideon a CLOSED tenant returns 200decision=DENYwithreason_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 closedACTIVE|SUSPENDED|CLOSEDenum) fails the request 500INTERNAL_ERRORwith 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 anerrorCyclesEvidence envelope and stampscycles_evidence, like the other budget/lifecycle denial codes (declared in evidence ErrorResponseMirror, cycles-evidence-v0.2.yaml 0.2.1).TENANT_CLOSEDtakes precedence overRESERVATION_FINALIZED/RESERVATION_EXPIREDfor non-replay attempts; same-key idempotent replays return their original response.
Fixed
- Webhook
scope_filtermatching 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 degeneratetenant:a/). Both planes' matchers now pin the same test table.
Compatibility
- Additive
ErrorCodeandDecisionReasonCodevalue (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.
v0.1.25.46 — 429 rate limiting on public evidence/JWKS endpoints
Security release: 429 rate limiting on the public endpoints (#224, hardened by #225).
Added
- 429 throttling on
GET /v1/evidence/*and the CyclesEvidence JWKS — the spec's SHOULD-level rate limiting on the only anonymous surface. Fixed 60s window per client IP, per instance; over-limit requests get429witherror=LIMIT_EXCEEDED(new ErrorCode, spec v0.1.25.12),Retry-After,X-RateLimit-Reset,X-RateLimit-Remaining: 0, and the standard correlation headers. Authenticated/v1endpoints are not throttled (abuse there is key-attributable). - Config:
CYCLES_PUBLIC_RATE_LIMIT_ENABLED(defaulttrue),CYCLES_PUBLIC_RATE_LIMIT_REQUESTS_PER_MINUTE(default300). - Client map is hard-bounded (#225): stale-window entries evicted first; a unique-key flood within one window resets the map entirely (heap protection — per-IP limiting cannot constrain an address-rotating attacker).
Compatibility
New behavior only for anonymous callers exceeding 300 req/min/IP on the two public endpoints. The limiter is per instance and keyed on the socket peer — behind a connection-terminating ingress, prefer ingress limiting and/or raise the limit. No wire, Redis, Lua, event, or evidence schema change; Enums.ErrorCode gains LIMIT_EXCEEDED (additive).
501 tests green incl. integration + contract validation against spec v0.1.25.12; spec coverage 11/11; JaCoCo ≥95%. Full details: CHANGELOG.md [0.1.25.46], AUDIT.md 2026-07-04 entries.
[benchmark-skip] — the new filter gates only the public evidence/JWKS endpoints, which the benchmark suite does not exercise; on benchmarked paths it adds a single string prefix comparison per request, indistinguishable from shared-runner noise.
v0.1.25.45 Harden runtime production readiness
What's Changed
- Harden runtime production readiness by @amavashev in #218
Gates operational endpoints (/actuator/prometheus, /actuator/info, aggregate /actuator/health, API docs, Swagger) behind the configured admin key, while liveness/readiness probes and the protocol-public CyclesEvidence/JWKS endpoints stay open. Bounds async event emission with a saturation-logged queue (no request-path blocking), normalizes filter ordering + MDC handling, hardens auth-filter error responses, and removes the tracked dump.rdb snapshot (now gitignored).
[benchmark-skip]
Security/infra hardening — no wire-format or hot-path changes.
Full Changelog: v0.1.25.44...v0.1.25.45
v0.1.25.44 — production compose hardening: Prometheus tenant-label off, SpringDoc off, exec PID-1, fleet pins
[benchmark-skip] — Deployment-config-only release (production Compose hardening, Dockerfile, docs, pom dependency pins). Zero Java, Lua, or hot-path change since 0.1.25.43, so the reserve/commit/release/event latencies the benchmark measures cannot have regressed. The release gate flaked on shared-runner noise (all metrics uniformly +32–44%, with the rolling median itself inflated by today's slower runners). Bypassed per the workflow's documented policy for infra-only releases.
[0.1.25.44] — 2026-06-26
Fixed
- Production Compose now disables tenant labels on custom Prometheus domain
metrics viaCYCLES_METRICS_TENANT_TAG_ENABLED=false, avoiding tenant-id
disclosure and high-cardinality series when/actuator/prometheusis exposed
to a shared scrape path. - Production Compose now disables public SpringDoc API docs and Swagger UI by
default. Local/developer defaults remain unchanged. - Production Compose no longer sets
DASHBOARD_CORS_ORIGINon the runtime
server, because the runtime service does not consume that variable. The
admin service still owns dashboard CORS in the full-stack deployment. - Full-stack production Compose now references current sibling service images:
cycles-server-admin:0.1.25.47andcycles-server-events:0.1.25.20. - Full-stack production Compose now probes admin/events readiness endpoints
instead of aggregate health, matching the sibling services' Redis-aware
readiness split. The events worker is published on its management port
9980, not the internal worker port7980. - Full-stack production Compose now requires
WEBHOOK_SECRET_ENCRYPTION_KEY
for the admin/events deployment path and sets
WEBHOOK_SECRET_ENCRYPTION_REQUIRED=truefor admin. - Production Compose examples now point at the
cycles-server:0.1.25.44image
tag. - The runtime image entrypoint now uses
exec java ...so the JVM is PID 1 and
receives container SIGTERM directly for Spring Boot graceful shutdown.
Compatibility
- Deployment hardening only. No HTTP API schema, Redis data model, Lua, event,
evidence, or protocol behavior change. Custom Prometheus metrics emitted from
the production Compose deployment no longer include thetenantlabel unless
explicitly re-enabled. Full-stack production deployments now fail fast if the
webhook secret encryption key is missing.
v0.1.25.43 — production readiness: Redis health contributor, requestId MDC, stateless security hardening
[benchmark-skip] — Infra/ops-only release (Redis health contributor, requestId MDC, stateless Spring Security config). No hot-path, Lua, or wire change. The release benchmark gate flaked on reserve_p50 only (runner noise: bounced 3.9–4.4 ms vs ~3.5 ms normal, <1% over threshold on a single metric while all others passed); bypassed per the workflow's documented policy for infra-only releases that the benchmark cannot meaningfully measure.
[0.1.25.43] — 2026-06-25
Fixed
/actuator/health/readinessnow includes a RedisPINGhealth contributor.
Container, Dockerfile, and release-smoke healthchecks now use readiness and go
unhealthy when the ledger dependency is unreachable instead of reporting a
live HTTP process as healthy while API operations return Redis-backed 5xx
errors./actuator/health/livenessremains process-only.RequestIdFilternow placesrequestIdin MDC for the lifetime of each
request, matching the operations runbook and making structured logs
consistently joinable by bothrequestIdandtraceId.- Spring Security is now explicitly stateless/API-key-only: HTTP Basic, form
login, logout, and Spring Boot's generated default user autoconfiguration are
disabled. - Redis connection failures during API-key validation now log one concise
redis_unavailablewarning instead of a full stack trace per affected
request; unexpected validation exceptions still keep stack traces. - Production Compose examples now point at the
0.1.25.43image tag.
Compatibility
- Operational readiness change only. No HTTP API schema, Redis data model, Lua,
event, evidence, or protocol behavior change. The unauthenticated readiness
endpoint can now return DOWN/503 when Redis is unavailable.
v0.1.25.42 — operational logging context + log sanitization + release-gate rolling baseline (rolls up .40/.41)
[0.1.25.42] — 2026-06-25
Fixed
- Release benchmark gating now compares the candidate median against the
rolling median frombenchmark-data/benchmarks/history.jsonlwhen history is
available, while keeping the strict 25% threshold. This avoids blocking a
release on one unusually fast prior release sample;baseline.jsonremains
updated after successful releases for reference/bootstrap. - Guarded tenant-authorization DEBUG logging so request-time sanitizer work is
skipped when DEBUG is off.
Compatibility
- CI/workflow and benchmark script change only. No HTTP request/response,
Redis, Lua, event, evidence, or spec change.
[0.1.25.41] — 2026-06-24
Fixed
- Flattened CR/LF characters in dynamic operator-log fields added by the
logging-context review so request/config/exception values cannot inject
misleading log lines. - Removed API-key prefix/masked-token material from debug logs; auth logging now
reports only key presence/length plus sanitized tenant/key/reason context on
failures. - Sanitized JWKS retired-key parsing warnings and auth rejection logs while
preserving method, path, request id, trace id, and error context. - Extended the same CR/LF flattening to data-plane repository/service failure
logs (reservation, audit, event-emitter, evidence, expiry) via a shared
LogSanitizerutility, so request-derived strings logged below the
controller layer cannot inject log lines either. - Sanitized the remaining exception-handler path/route/reservation-id fields
and tenant-authorization DEBUG values; added a regression assertion for
handled protocol-exception log flattening. - Per-request controller request logs are emitted at
DEBUG, notINFO.
This removes default production success-path request log volume and keeps
sanitize/attribute lookups behindisDebugEnabled; exception and
side-effect-failure logs remain atINFO/WARN. EnableDEBUGfor that
logger to restore per-request lines. A later release-CI follow-up found the
benchmark gate was also over-sensitive to a single fast baseline sample; see
0.1.25.42.
Compatibility
- No HTTP request/response, Redis, Lua, event, evidence, or spec change. Default
log volume drops (per-request request logs move from INFO to DEBUG).
[0.1.25.40] — 2026-06-24
Fixed
- Replaced the class-only
Landed in cycles exception handlerlog with
structured protocol-exception logs carrying method, path, matched route,
status, error code,request_id,trace_id, andreservation_id. - Added the same operational context to validation, malformed-body, and
unexpected exception handler logs so 4xx/5xx responses can be joined to
application logs. - Added request-context fields to controller request logs for reservations,
balances, decisions, events, and evidence retrieval. - Made formerly silent non-blocking controller side-effect failures visible at
WARNwithout changing response behavior. - Tightened auth and async event/evidence/audit logs to include safe identifiers
such as tenant, resource, event, request, and trace context while avoiding
full validation DTOs, request DTOs, API keys, and raw idempotency keys.
Validation
mvn -B -pl cycles-protocol-service-api -am "-Dtest=GlobalExceptionHandlerTest,ApiKeyAuthenticationFilterTest,AdminApiKeyAuthenticationFilterTest" "-Dsurefire.failIfNoSpecifiedTests=false" testmvn -B -pl cycles-protocol-service-api -am "-Dtest=ReservationControllerTest,DecisionControllerTest,EventControllerTest,BalanceControllerTest,EvidenceControllerTest" "-Dsurefire.failIfNoSpecifiedTests=false" "-Dcontract.validation.enabled=false" testmvn -B -pl cycles-protocol-service-data -am "-Dtest=ApiKeyRepositoryTest,ApiKeyValidationServiceTest,AuditRepositoryTest,EventEmitterRepositoryTest,EventEmitterServiceTest,ReservationExpiryServiceTest,EvidenceEmitterTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
v0.1.25.39 — protocol review fixes (SCAN pagination, idempotent replay, auth cache freshness, prod Redis hardening)
Fixed
- Legacy SCAN pagination for
GET /v1/reservationsandGET /v1/balances
now uses opaque intra-batch cursors when a page limit is reached mid-batch,
preventing deterministic skipped rows on follow-up pages for an unchanged
SCAN batch. As with Redis SCAN generally, pagination remains best-effort
under concurrent writes or Redis rehashing. - Sorted
GET /v1/reservationsno longer truncates matches at 2000 hydrated
rows. Large result sets still warn so operators can narrow filters or add
indexed listing support later. - Auth-filter error bodies now include
trace_idand set
X-Cycles-Trace-Idwhen the trace filter has not already done so. POST /v1/eventsidempotent replay now returns the stored original response
payload and skips duplicate event metrics/balance event emission.- Invalid tenant
default_commit_overage_policyvalues now fail closed with
INVALID_REQUEST; Lua commit/event paths also reject unknown overage
policies defensively. - API key validation no longer caches full allow/deny decisions. It still
caches BCrypt verification results briefly, including repeated wrong-secret
checks keyed by the current stored hash, while status, expiry, tenant, and
tenant-status checks are read on every request. - Production Compose files require
REDIS_PASSWORD, authenticate Redis health
checks, stop publishing Redis on the host port, and pincycles-serverto
0.1.25.39.
Documentation
- Clarified idempotency status codes: header/body key mismatch is
400 INVALID_REQUEST; same-key/different-payload replay is
409 IDEMPOTENCY_MISMATCH.
v0.1.25.38 — disable evidence emission when unconfigured
Disable CyclesEvidence emission when the public identity is unconfigured (#207). Pairs with cycles-server-events v0.1.25.15.
Fixed
- When
EVIDENCE_SERVER_IDorEVIDENCE_SIGNING_SIGNER_DIDis blank,EvidenceEmitter.emit(...)now returnsnullbefore building a source record or pushing toevidence:pending— an intentionally non-evidence deployment no longer accumulates work for a signer that should be off. - Configured deployments are unchanged: null-strip payload, synchronous
evidence_idcompute, record stamp, queue push,cycles_evidenceref returned.
Additive / no wire change. mvn -B verify green.
v0.1.25.37 — link reservations to their evidence (include=evidence)
Link reservations to their CyclesEvidence envelope(s) — implements cycles-protocol v0.1.25.9 (#117).
The cycles_evidence ref previously rode only on the live reserve/commit/release response, so a reservation fetched later had no path back to its signed envelope. The server now persists each computed evidence ref onto the reservation and surfaces it via a new opt-in evidence projection.
Changes
evidenceprojection onReservationSummary/ReservationDetail: aReservationEvidencemap keyed by artifact type (reserve/commit/release) ofCyclesEvidenceRefs.include=evidencetoken onGET /v1/reservations(omitted from list rows by default, projection-only — not folded into the cursor/filter hash). Always present on the single-rowgetReservationwhen recorded.persistEvidenceRefdoes a fail-open follow-upHSETof<artifact>_evidence_id+_evidence_urlonto the reservation hash right after the ref is stamped (preserves the Lua-set terminal TTL).- Degrades gracefully:
nullwhen evidence emission is disabled or for pre-feature reservations.
Additive / non-breaking — clients that don't request include=evidence get byte-identical responses. Full mvn verify green across model/data/api; JaCoCo 95% gate met.
Downstream: cycles-dashboard renders one-click "View evidence" links per reservation (runcycles/cycles-dashboard#212).