Skip to content

Commit 3cc44a7

Browse files
authored
Merge pull request #207 from runcycles/codex/disable-evidence-emission-when-unconfigured
[codex] Disable evidence emission when unconfigured
2 parents 1e2b098 + 47f83e2 commit 3cc44a7

6 files changed

Lines changed: 66 additions & 28 deletions

File tree

AUDIT.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99

1010
All four `docker-compose*.yml` (base, `prod`, `full-stack`, `full-stack.prod`) gain a shared `x-logging` anchor (`json-file`, `max-size: 10m`, `max-file: 5`) referenced by every service (redis, cycles-server, cycles-admin, cycles-events). Previously no compose file declared a logging driver, so containers inherited Docker's default UNBOUNDED `json-file` logs — a slow disk-exhaustion path on long-running deployments (a stack left up for days grows each container's `*-json.log` without limit). The anchor caps every container at 5×10 MB = 50 MB with rotation. Runtime/deployment config only — no image, server-code, or wire change, so no version bump or release; the cap takes effect on containers (re)created from these files. `docker compose config` validates clean on all four.
1111

12+
### 2026-06-23 — v0.1.25.38: disable evidence emission when identity is unconfigured
13+
14+
Aligns runtime producer behavior with the event-tier disabled mode. Previously `EvidenceEmitter.emit` returned no `cycles_evidence` ref when `EVIDENCE_SERVER_ID` / `EVIDENCE_SIGNING_SIGNER_DID` were blank, but it still built and LPUSH'd a source record without `evidence_id` to `evidence:pending`. That made an intentionally non-evidence deployment accumulate work for a signer that should be off.
15+
16+
`EvidenceEmitter.emit` now fail-opens earlier: if either public identity value is blank, it returns `null` before null-stripping payloads, computing ids, or touching `EvidenceQueueRepository`. Configured deployments retain the existing path exactly: payload null-stripping, synchronous `evidence_id` computation, record stamping, queue push, and response ref. `EvidenceEmitterTest` updates the unconfigured case to assert no Redis push and no failure metric, and the configured cases now set identity explicitly. Data-module focused test: `mvn -B -pl cycles-protocol-service-data -am -Dtest=EvidenceEmitterTest -Dsurefire.failIfNoSpecifiedTests=false test`. Version bump: `cycles-protocol-service/pom.xml` `<revision>``0.1.25.38`.
17+
1218
### 2026-06-22 — v0.1.25.37: link reservations to their evidence via `include=evidence`
1319

1420
Implements cycles-protocol v0.1.25.9 (runcycles/cycles-protocol#117). The `cycles_evidence` ref previously rode only on the live reserve/commit/release response, so a reservation fetched later (e.g. by the admin dashboard) had no path back to its signed envelope — you had to have captured the `evidence_id` at the moment of the call. Now the server persists each computed ref onto the reservation and surfaces it via a new `evidence` projection.

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@ changes to request/response bodies or Lua-script semantics would require a
1414
minor bump. "Internal signature changes" (e.g. Java method parameters) are
1515
called out but are not breaking to API clients.
1616

17+
## [0.1.25.38] — 2026-06-23
18+
19+
### Fixed
20+
21+
- **Unconfigured CyclesEvidence no longer queues source records.** When either
22+
`EVIDENCE_SERVER_ID` or `EVIDENCE_SIGNING_SIGNER_DID` is blank,
23+
`EvidenceEmitter.emit(...)` now returns `null` before building a source record
24+
or pushing to `evidence:pending`.
25+
- Configured deployments are unchanged: the emitter still null-strips the
26+
evidence payload, computes `evidence_id` synchronously, stamps it onto the
27+
queued record, and returns `cycles_evidence` for the response.
28+
29+
### Documentation
30+
31+
- Updated the evidence configuration comments to state that missing public
32+
identity disables evidence emission completely.
33+
34+
### Validation
35+
36+
- `mvn -B -pl cycles-protocol-service-data -am -Dtest=EvidenceEmitterTest -Dsurefire.failIfNoSpecifiedTests=false test`
37+
passes.
38+
1739
## [0.1.25.21] — 2026-05-22
1840

1941
`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).

cycles-protocol-service/cycles-protocol-service-api/src/main/resources/application.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ cycles.evidence.store.key-prefix=${EVIDENCE_STORE_KEY_PREFIX:evidence:envelope:}
7878
# signer-did is PUBLIC (the hex Ed25519 public key) — never the private key.
7979
# Property names + env vars are SHARED with the worker (cycles.evidence.server-id /
8080
# EVIDENCE_SERVER_ID and cycles.evidence.signing.signer-did / EVIDENCE_SIGNING_SIGNER_DID)
81-
# so one env var configures both services. When unset, evidence is still queued but
82-
# no evidence_id is computed or returned.
81+
# so one env var configures both services. When either is unset, evidence is off:
82+
# no source record is queued and no evidence_id is computed or returned.
8383
cycles.evidence.server-id=${EVIDENCE_SERVER_ID:}
8484
cycles.evidence.signing.signer-did=${EVIDENCE_SIGNING_SIGNER_DID:}
8585
# Signer JWK Set publication (getEvidenceJwks, GET /v1/.well-known/cycles-jwks.json).

cycles-protocol-service/cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/service/EvidenceEmitter.java

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
* identity), store and serve.
2121
*
2222
* <p>This server stamps the operational facts ({@code artifact_type},
23-
* {@code issued_at_ms}, {@code trace_id}, payload) AND — when the public server
24-
* identity ({@code cycles.evidence.server-id} + {@code cycles.evidence.signer-did})
25-
* is configured — computes the {@code evidence_id} SYNCHRONOUSLY via
23+
* {@code issued_at_ms}, {@code trace_id}, payload) when the public server
24+
* identity ({@code cycles.evidence.server-id} +
25+
* {@code cycles.evidence.signing.signer-did}) is configured. It also computes
26+
* the {@code evidence_id} SYNCHRONOUSLY via
2627
* {@link EvidenceIdComputer} and stamps it on the record. The {@code evidence_id}
2728
* is a pure function of the envelope contents (no private key needed), so it can
2829
* be returned on the lifecycle response ({@link EvidenceRef}) for a caller to
@@ -36,7 +37,7 @@
3637
* carry {@code reservation_id}, and {@code error} carries
3738
* {@code {endpoint, http_status, request, response}}.
3839
*
39-
* <p>DURABILITY: the enqueue is SYNCHRONOUS — the source record is LPUSH'd
40+
* <p>DURABILITY: when evidence is configured, the enqueue is SYNCHRONOUS — the source record is LPUSH'd
4041
* before the lifecycle response returns, so a successful operation cannot return
4142
* without its evidence being durably queued. The enqueue targets the same Redis
4243
* as the ledger write that just committed, so it succeeds whenever the operation
@@ -108,9 +109,9 @@ private ObjectMapper evidencePayloadMapper() {
108109
private String signerDid;
109110

110111
/**
111-
* Synchronously enqueue an evidence-source record for a lifecycle artifact
112-
* and, when the public server identity is configured, compute its
113-
* {@code evidence_id} and return a {@link EvidenceRef} for the caller to
112+
* When the public server identity is configured, synchronously enqueue an
113+
* evidence-source record for a lifecycle artifact, compute its
114+
* {@code evidence_id}, and return a {@link EvidenceRef} for the caller to
114115
* surface on the response. Call AFTER the ledger write commits and BEFORE
115116
* returning the response.
116117
*
@@ -122,10 +123,15 @@ private ObjectMapper evidencePayloadMapper() {
122123
* {@code payload.<artifactType>} (see class javadoc)
123124
* @return the evidence reference ({@code evidence_id} + {@code cycles_evidence_url})
124125
* when the id could be computed, or {@code null} if the server
125-
* identity is unconfigured or emission failed (fail-open — never throws)
126+
* identity is unconfigured (no record queued) or emission failed
127+
* (fail-open — never throws)
126128
*/
127129
public EvidenceRef emit(String artifactType, long issuedAtMs, String traceId, Object payloadBody) {
128130
try {
131+
if (!identityConfigured()) {
132+
return null;
133+
}
134+
129135
// Null-strip the payload ONCE into a tree, and use that same tree for
130136
// BOTH the content-id computation and the queued record, so the id the
131137
// worker recomputes over the stored payload matches byte-for-byte.
@@ -139,18 +145,15 @@ public EvidenceRef emit(String artifactType, long issuedAtMs, String traceId, Ob
139145
}
140146
record.put("payload", cleanPayload);
141147

142-
EvidenceRef ref = null;
143-
if (identityConfigured()) {
144-
// Compute the content id over the SAME envelope the worker will
145-
// build, BEFORE the record is serialized — and never over a
146-
// response already carrying cycles_evidence (the caller stamps
147-
// the ref on the response only AFTER this returns), so the
148-
// attested payload stays free of a self-reference.
149-
String evidenceId = evidenceIdComputer.compute(
150-
artifactType, serverId, signerDid, issuedAtMs, traceId, cleanPayload);
151-
record.put("evidence_id", evidenceId);
152-
ref = new EvidenceRef(evidenceId, evidenceUrl(evidenceId));
153-
}
148+
// Compute the content id over the SAME envelope the worker will
149+
// build, BEFORE the record is serialized — and never over a
150+
// response already carrying cycles_evidence (the caller stamps
151+
// the ref on the response only AFTER this returns), so the
152+
// attested payload stays free of a self-reference.
153+
String evidenceId = evidenceIdComputer.compute(
154+
artifactType, serverId, signerDid, issuedAtMs, traceId, cleanPayload);
155+
record.put("evidence_id", evidenceId);
156+
EvidenceRef ref = new EvidenceRef(evidenceId, evidenceUrl(evidenceId));
154157

155158
repository.push(objectMapper.writeValueAsString(record));
156159
return ref;

cycles-protocol-service/cycles-protocol-service-data/src/test/java/io/runcycles/protocol/data/service/EvidenceEmitterTest.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import static org.mockito.ArgumentMatchers.anyString;
1717
import static org.mockito.Mockito.doThrow;
1818
import static org.mockito.Mockito.mock;
19+
import static org.mockito.Mockito.never;
1920
import static org.mockito.Mockito.verify;
2021

2122
class EvidenceEmitterTest {
@@ -46,6 +47,8 @@ private void configureIdentity() throws Exception {
4647

4748
@Test
4849
void enqueuesSynchronouslyWithArtifactTypeTraceAndPayloadBody() throws Exception {
50+
configureIdentity();
51+
4952
emitter.emit("reserve", 1810000000100L, "trace-abc",
5053
Map.of("request", Map.of("idempotency_key", "k1"),
5154
"response", Map.of("decision", "ALLOW")));
@@ -63,6 +66,8 @@ void enqueuesSynchronouslyWithArtifactTypeTraceAndPayloadBody() throws Exception
6366

6467
@Test
6568
void stripsNullValuedPropertiesFromTheEvidencePayload() throws Exception {
69+
configureIdentity();
70+
6671
// The evidence mirrors are additionalProperties:false with non-nullable typed
6772
// fields, so an unset request field serialized as null (e.g. ttl_ms / metadata)
6873
// would make the envelope fail mirror validation. Null-strip it.
@@ -112,6 +117,8 @@ void evidenceIdIsComputedOverTheNullStrippedPayload() throws Exception {
112117

113118
@Test
114119
void omitsTraceIdWhenBlank() throws Exception {
120+
configureIdentity();
121+
115122
emitter.emit("commit", 123L, " ", Map.of("request", Map.of("a", 1), "response", Map.of("b", 2)));
116123

117124
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
@@ -120,15 +127,14 @@ void omitsTraceIdWhenBlank() throws Exception {
120127
}
121128

122129
@Test
123-
void returnsNullAndOmitsEvidenceIdWhenIdentityUnconfigured() throws Exception {
130+
void returnsNullAndDoesNotQueueWhenIdentityUnconfigured() {
124131
// default wire() leaves server-id/signer-did blank
125132
EvidenceEmitter.EvidenceRef ref = emitter.emit("reserve", 1L, null,
126133
Map.of("request", Map.of(), "response", Map.of("decision", "ALLOW")));
127134

128135
assertThat(ref).isNull();
129-
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
130-
verify(repository).push(captor.capture());
131-
assertThat(mapper.readTree(captor.getValue()).has("evidence_id")).isFalse();
136+
verify(repository, never()).push(anyString());
137+
verify(metrics, never()).recordEvidenceEmitFailed(anyString());
132138
}
133139

134140
@Test
@@ -168,7 +174,8 @@ void evidenceUrlJoinsCleanlyWhenServerIdHasTrailingSlash() throws Exception {
168174
}
169175

170176
@Test
171-
void failsOpenAndMetersWhenPushThrows() {
177+
void failsOpenAndMetersWhenPushThrows() throws Exception {
178+
configureIdentity();
172179
doThrow(new RuntimeException("redis down")).when(repository).push(anyString());
173180

174181
// a push failure must NOT propagate (the ledger write already committed)

cycles-protocol-service/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<module>cycles-protocol-service-api</module>
1919
</modules>
2020
<properties>
21-
<revision>0.1.25.37</revision>
21+
<revision>0.1.25.38</revision>
2222
<java.version>21</java.version>
2323
<maven.compiler.source>21</maven.compiler.source>
2424
<maven.compiler.target>21</maven.compiler.target>

0 commit comments

Comments
 (0)