Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion AUDIT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Cycles Protocol v0.1.25 — Server Implementation Audit

**Date:** 2026-05-21 (v0.1.25.20 — `from` / `to` ISO-8601 time-window filter on `GET /v1/reservations` per cycles-protocol revision 2026-05-21; closes runcycles/cycles-server#159. Two new query params on `listReservations`, both `string`/`format: date-time`, both inclusive bounds on `created_at_ms`, both bind to `created_at_ms` regardless of `sort_by`. Implemented in both the legacy SCAN-cursor and sorted paths. `FilterHasher.hash(...)` now folds `fromMs`/`toMs` into the canonical hash so sorted-path cursors invalidate on window change (the legacy Redis-SCAN cursor is not window-validated, matching how it already treats every other filter). Validation: malformed values → 400, `from > to` → 400 before any repository call, blank strings treated as unset, missing/unparseable `created_at` rows defensively excluded when either bound supplied. Pure additive wire change — all v0.1.25.x clients that don't send the params continue to work byte-for-byte. 538 tests pass (375 data + 163 api).),
**Date:** 2026-05-22 (v0.1.25.21 — `expires_from`/`expires_to` and `finalized_from`/`finalized_to` ISO-8601 time-window filters on `GET /v1/reservations` per `cycles-protocol-v0.yaml` revision 2026-05-22 (runcycles/cycles-protocol#98); closes runcycles/cycles-server#162. Four new query params mirroring the v0.1.25.20 `from`/`to` shape: `expires_*` binds to `expires_at_ms` (required field, every row), `finalized_*` binds to `finalized_at_ms` (populated only on COMMITTED/RELEASED; ACTIVE and EXPIRED normatively excluded). Three windows compose with AND. `finalized_at_ms` added as an optional field on `ReservationSummary` so clients filtering with `finalized_*` can see the timestamp without a follow-up `getReservation` — strict-schema-compatible because the field is `@JsonInclude(NON_NULL)`. `FilterHasher` extends with four more `Long` args (10 → 14) using independent gated emission per pair — preserves byte-exact back-compat for v0.1.25.18 cursors (golden `2f397ea0e8fb53b7`) AND v0.1.25.20 cursors with from/to set (golden `ad7204d521cfd133`). `RedisReservationRepository.listReservations` signature 14 → 18 args. Two new predicate helpers (`expiresAtInWindow`, `finalizedAtInWindow`) applied in both legacy SCAN-cursor and sorted paths. Validation: each new pair `from > to` → 400; malformed values → 400 with distinct per-param message; blank strings treated as unset. 557 tests pass (384 data + 173 api), +19 vs v0.1.25.20.),
2026-05-21 (v0.1.25.20 — `from` / `to` ISO-8601 time-window filter on `GET /v1/reservations` per cycles-protocol revision 2026-05-21; closes runcycles/cycles-server#159. Two new query params on `listReservations`, both `string`/`format: date-time`, both inclusive bounds on `created_at_ms`, both bind to `created_at_ms` regardless of `sort_by`. Implemented in both the legacy SCAN-cursor and sorted paths. `FilterHasher.hash(...)` now folds `fromMs`/`toMs` into the canonical hash so sorted-path cursors invalidate on window change (the legacy Redis-SCAN cursor is not window-validated, matching how it already treats every other filter). Validation: malformed values → 400, `from > to` → 400 before any repository call, blank strings treated as unset, missing/unparseable `created_at` rows defensively excluded when either bound supplied. Pure additive wire change — all v0.1.25.x clients that don't send the params continue to work byte-for-byte. 538 tests pass (375 data + 163 api).),
2026-05-21 (v0.1.25.19 — supply-chain CVE patch; re-pin `tomcat.version=10.1.55` in `cycles-protocol-service/pom.xml` to close 7 new CVEs flagged by Trivy against `tomcat-embed-core 10.1.54` (CRITICAL: CVE-2026-43512, CVE-2026-43515, CVE-2026-41293; HIGH: CVE-2026-43513, CVE-2026-42498, CVE-2026-41284; LOW: CVE-2026-43514 — all fixed in 10.1.55 / 11.0.22). Mirrors the v0.1.25.16 pattern; the override was dropped in v0.1.25.18 when SB 3.5.14's BOM caught up to 10.1.54, now re-added one patch higher because Trivy DB updates between 2026-05-11 (last green main run) and 2026-05-21 surfaced a new wave on the same artifact. Removable once Spring Boot ships with 10.1.55+ as its managed version. `commons-lang3.version=3.18.0` retained (CVE-2025-48924 still unfixed in SB 3.5.14's managed 3.17.0). No production code or test changes; all 537 protocol-service tests pass.),
2026-04-26 (v0.1.25.18 — dependency hygiene matching `cycles-server-events` v0.1.25.12: bump `spring-boot-starter-parent` 3.5.13 → 3.5.14 (patch with upstream security hardening — constant-time comparison for remote DevTools secret, `RandomValuePropertySource` SecureRandom, hostname verification applied consistently for Cassandra/RabbitMQ SSL, plus symlink-handling fixes); **drop `<tomcat.version>10.1.54</tomcat.version>` override** since Spring Boot 3.5.14's BOM now manages 10.1.54 directly (verified against `spring-boot-dependencies-3.5.14.pom`); commons-lang3 3.18.0 override retained — Spring Boot 3.5.14's BOM still manages 3.17.0. **Jedis 7.4.1 → 6.2.0** to align all three services on the same Redis client major (events at 6.2.0 since v0.1.25.12, admin at 6.2.0 in v0.1.25.41); all call sites use stable APIs (`Jedis`, `JedisPool`, `Pipeline`, `Response`, `ScanParams`, `ScanResult`, `JedisNoScriptException`) — no 7.x-only API usage. No code changes; all 152 tests pass.),
2026-04-19 (v0.1.25.17 — supply-chain CVE fix follow-up; pin `commons-lang3.version=3.18.0` to close CVE-2025-48924 (Trivy HIGH) on the `commons-lang3-3.17.0` jar that ships in the fat-jar image via `swagger-core-jakarta` (OpenAPI UI). Spring Boot 3.5.13's BOM manages commons-lang3 at 3.17.0 — override is removable once Spring Boot ships a managed version of 3.18.0+. All 152 tests pass),
Expand All @@ -26,6 +27,48 @@

---

### 2026-05-22 — v0.1.25.21: `expires_*` / `finalized_*` time-window filters on listReservations

Closes [#162](https://github.com/runcycles/cycles-server/issues/162). Spec landed in `cycles-protocol-v0.yaml` revision 2026-05-22 (PR runcycles/cycles-protocol#98); this is the matching runtime impl.

**Why the spec change.** The v0.1.25.20 `from`/`to` window covers "what happened in the last 24h" cleanly but binds to `created_at_ms`, which is unhelpful for the operational use case the original issue thread also called out: "cleanup routines on expired or abandoned reservations." A reservation created at T-7d that just expired this morning is invisible to a 24h `created_at` window — exactly what a sweeper is looking for. The new windows give sweepers a direct path: query the expiry timestamp, query the finalization timestamp.

**Three independent pairs.** Each window pair binds to its target field regardless of `sort_by`, matching the v0.1.25.20 sort-key-independence rule. The three pairs compose with AND semantics; a row must satisfy every supplied predicate. This keeps the contract predictable across the matrix of (window, sort_key) combinations — no per-key filter semantics to memorize.

**Finalized-row exclusion.** The spec makes the ACTIVE/EXPIRED exclusion normative: when either `finalized_*` bound is supplied, rows missing both `committed_at` and `released_at` MUST be excluded from results. The predicate naturally fails on field-absent rows; making the exclusion normative ensures conformant servers agree. Callers wanting EXPIRED-row windows should use `expires_*` instead (which works on every row since `expires_at_ms` is required).

**Schema addition: `finalized_at_ms` on `ReservationSummary`.** Pre-revision the field was declared only on `ReservationDetail` (with `additionalProperties: false` on the summary blocking servers from sending it). The filter would have been useful only via a follow-up `getReservation` per row. Adding the field to the summary makes the filter genuinely useful; strict-schema clients remain compatible because the field is optional (`@JsonInclude(NON_NULL)`). Old clients that don't know about the field get pre-revision responses byte-for-byte (the field is absent on ACTIVE rows, which is the common case for unfiltered list calls).

**Two execution paths.** `RedisReservationRepository.listReservations` retains its v0.1.25.12 dual-path shape. Two new helpers — `expiresAtInWindow(fields, fromMs, toMs)` and `finalizedAtInWindow(fields, fromMs, toMs)` — are applied as per-row predicates in both the legacy SCAN-cursor and sorted paths, immediately after the existing scope/status/tenant predicates and the v0.1.25.20 `createdAtInWindow`. Predicate bodies follow the same defensive shape: missing or unparseable hash field → row excluded when EITHER bound is supplied.

**`finalizedAtInWindow` field resolution.** Mirrors `buildReservationSummary`'s projection logic: read `committed_at` first (populates the timestamp for COMMITTED rows), fall through to `released_at` for RELEASED rows. Both absent → row excluded. The legacy `committed_at`/`released_at` Redis fields are the source of truth; the wire field `finalized_at_ms` is a projection. Both filter and serializer agree on the source.

**Cursor invalidation extends to all six bounds.** `FilterHasher.hash(...)` gains four trailing `Long` arguments (10 → 14). Each window pair emits its canonical block with **independent gating** — the v0.1.25.20 `from`/`to` block only emits when `fromMs || toMs != null`, the new `expires_*` block only when `expiresFromMs || expiresToMs != null`, and the `finalized_*` block likewise. This preserves byte-exact back-compat for **both** prior cursor generations:

- v0.1.25.18 cursor (no windows) → canonical `t=acme|...|ts=` → golden hash `2f397ea0e8fb53b7` (locked down in v0.1.25.20)
- v0.1.25.20 cursor (from/to only) → canonical `t=acme|...|ts=|fr=100|to=200` → golden hash `ad7204d521cfd133` (newly locked down in v0.1.25.21 to prevent a future refactor from accidentally unioning the gating)

**Validation choices** (mirror the v0.1.25.20 contract):

- ISO-8601 parsed via `Instant.parse(...)`; malformed → 400 with distinct `Invalid {param_name}` message identifying which parameter failed.
- `expires_from > expires_to` and `finalized_from > finalized_to` → 400 *before* the repository call.
- Blank-string values for any of the six bounds treated as unset.
- Equal bounds (point window) accepted on each pair.

**Internal Java signature changes, no wire impact beyond the schema addition.** `listReservations` 14 → 18 args; `listReservationsSorted` mirrors; `FilterHasher.hash` 10 → 14 args. All Java callers updated. The single wire-format change is the optional `finalized_at_ms` field on `ReservationSummary` — covered by the optional-property guarantee.

**Coverage.**

- `FilterHasherTest` (+3): expires_* values differ from base/from-to, finalized_* values differ from from-to/expires_*, v0.1.25.20 8-byte golden lockdown.
- `RedisReservationQueryTest` (+6) under `ExpiresAndFinalizedWindowFilter` nested class: legacy-path `expires_from` excludes-below, legacy-path `expires_to` excludes-above, legacy-path `finalized_from` excludes-ACTIVE-rows, `finalized_at` resolves from `released_at` when `committed_at` absent, all-three AND composition (created + expires + finalized), cursor mismatch on expires window change rejected with 400.
- `ReservationControllerTest` (+10) under `ListReservations` nested class: malformed-expires_from, malformed-expires_to, malformed-finalized_from, malformed-finalized_to, reversed-expires-window, reversed-finalized-window, expires propagation with `verify(...)` lock, finalized propagation with `verify(...)` lock, all-three combined with distinct epoch-ms per pair to catch slot mix-ups, blank-as-unset for new windows.

557 protocol-service tests pass (384 data + 173 api), +19 vs v0.1.25.20. JaCoCo 95% bundle gate met.

**Out of scope (intentionally).** The `time_field`-pivoted alternative parameter shape (one `from`/`to` plus a `time_field=created_at|expires_at|finalized_at` selector) was considered and rejected — it would have been an awkward retroactive change to the v0.1.25.20 shape and split the family-wide `from`/`to` convention. Three parallel pairs is the cleaner expansion.

---

### 2026-05-21 — v0.1.25.20: `from` / `to` time-window filter on listReservations

Closes [#159](https://github.com/runcycles/cycles-server/issues/159). Spec landed in `cycles-protocol-v0.yaml` revision 2026-05-21 (PR runcycles/cycles-protocol#97); this is the matching runtime impl.
Expand Down
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,43 @@ changes to request/response bodies or Lua-script semantics would require a
minor bump. "Internal signature changes" (e.g. Java method parameters) are
called out but are not breaking to API clients.

## [0.1.25.21] — 2026-05-22

`expires_from`/`expires_to` and `finalized_from`/`finalized_to` ISO-8601 time-window filters on `GET /v1/reservations`, implementing `cycles-protocol-v0.yaml` revision 2026-05-22 ([runcycles/cycles-protocol#98](https://github.com/runcycles/cycles-protocol/pull/98)). Closes [#162](https://github.com/runcycles/cycles-server/issues/162).

### Added

- **Four new query parameters on `listReservations`** mirroring the v0.1.25.20 `from`/`to` shape. All ISO 8601 `format: date-time`, all optional, all inclusive bounds:
- `expires_from` / `expires_to` — bound on `expires_at_ms` (required field; applies to every row regardless of status).
- `finalized_from` / `finalized_to` — bound on `finalized_at_ms` (populated only on COMMITTED/RELEASED; ACTIVE and EXPIRED rows are normatively excluded since the field is absent).
- The three window filters (`from`/`to` + `expires_*` + `finalized_*`) compose with AND semantics — a row must satisfy every supplied predicate to be returned.
- **`finalized_at_ms` on `ReservationSummary`.** Pre-revision the field was only on `ReservationDetail`, which meant clients filtering with `finalized_*` couldn't see the timestamp they were filtering on without a per-row `getReservation` call. The summary now carries the field with the same population semantics. Strict-schema clients remain compatible because the field is optional (`@JsonInclude(NON_NULL)`).

### Validation

- Each pair rejects `expires_from > expires_to` and `finalized_from > finalized_to` with HTTP 400 before any repository call.
- Malformed ISO-8601 → 400 with distinct `Invalid {param_name}` message identifying which parameter failed.
- Blank-string values for any of the six bounds treated as unset per the normative carve-out in the 2026-05-22 spec revision.

### Internal

- `RedisReservationRepository.listReservations(...)` signature gains trailing `Long expiresFromMs, Long expiresToMs, Long finalizedFromMs, Long finalizedToMs` (14 → 18 args). Private `listReservationsSorted(...)` mirrors. Two new predicate helpers: `expiresAtInWindow(fields, fromMs, toMs)` and `finalizedAtInWindow(fields, fromMs, toMs)`, applied in both legacy SCAN-cursor and sorted paths after the existing scope/status/tenant predicates.
- `finalizedAtInWindow` resolves the timestamp from `committed_at` OR `released_at` (whichever is set), matching `buildReservationSummary`'s projection logic. Both fields absent → row excluded per the normative ACTIVE/EXPIRED exclusion.
- `FilterHasher.hash(...)` gains four trailing `Long` arguments (10 → 14 args) with independent gated emission. Each window pair emits its `|ef=|et=` / `|ff=|ft=` block only when at least one of its bounds is non-null — preserves byte-exact back-compat for **both** v0.1.25.18 cursors (no window canonical) and v0.1.25.20 cursors (`|fr=|to=` canonical, no expires/finalized). Locked down by `FilterHasherTest.preservesV01_25_20HashWhenOnlyFromTo` (golden `ad7204d521cfd133`).
- `ReservationSummary.finalizedAtMs` projection added to `toSummary(...)` builder.

### Coverage

- 557 protocol-service tests pass (384 data + 173 api), up from 538 in v0.1.25.20 (+19 new):
- `FilterHasherTest`: +3 new (expires/finalized distinctness, finalized vs from/to distinctness, v0.1.25.20 8-byte golden lockdown).
- `RedisReservationQueryTest`: +6 new under `ExpiresAndFinalizedWindowFilter` nested class.
- `ReservationControllerTest`: +10 new under `ListReservations` nested class (4 malformed-*, 2 reversed-window, expires propagation, finalized propagation, all-three combined, blank-as-unset for new windows).
- JaCoCo 95% bundle gate met.

### Behavior change

None for existing callers. All four new params are optional; the gated FilterHasher emission preserves byte-exact cursor back-compat for both v0.1.25.18 and v0.1.25.20 sorted-path cursors. The single new response-body field on `ReservationSummary` is optional with `@JsonInclude(NON_NULL)`, so v0.1.25.20-shape responses go out byte-for-byte when no terminal-state rows are returned.

## [0.1.25.20] — 2026-05-21

`from` / `to` ISO-8601 time-window filter on `GET /v1/reservations`,
Expand Down
4 changes: 4 additions & 0 deletions cycles-protocol-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,10 @@ List reservations for the effective tenant. Optional recovery/debug endpoint. Re
| `sort_dir` | `asc` or `desc`. Defaults to `desc` when `sort_by` is provided. Ignored unless `sort_by` is set. |
| `from` | ISO 8601 date-time (v0.1.25.20+). Inclusive lower bound on `created_at_ms`. Always binds to `created_at_ms` regardless of `sort_by`. May be supplied alone (no upper bound) or with `to`. Blank string treated as unset. |
| `to` | ISO 8601 date-time (v0.1.25.20+). Inclusive upper bound on `created_at_ms`. Same binding and blank-as-unset rules as `from`. `from > to` returns `400 INVALID_REQUEST`. |
| `expires_from` | ISO 8601 date-time (v0.1.25.21+). Inclusive lower bound on `expires_at_ms`. Applies to every row regardless of status. May be supplied alone or with `expires_to`. Blank string treated as unset. |
| `expires_to` | ISO 8601 date-time (v0.1.25.21+). Inclusive upper bound on `expires_at_ms`. Same blank-as-unset rule. `expires_from > expires_to` returns `400 INVALID_REQUEST`. |
| `finalized_from` | ISO 8601 date-time (v0.1.25.21+). Inclusive lower bound on `finalized_at_ms`. Populated only on COMMITTED and RELEASED rows — ACTIVE and EXPIRED rows are normatively excluded when this is set. May be supplied alone or with `finalized_to`. Blank string treated as unset. |
| `finalized_to` | ISO 8601 date-time (v0.1.25.21+). Inclusive upper bound on `finalized_at_ms`. Same blank-as-unset rule and ACTIVE/EXPIRED exclusion. `finalized_from > finalized_to` returns `400 INVALID_REQUEST`. |

When `sort_by` or `sort_dir` is provided, the cursor encodes the
`(sort_by, sort_dir, filters)` tuple — reusing a sorted cursor after
Expand Down
Loading