-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcycles-protocol-v0.yaml
More file actions
2076 lines (1936 loc) · 95.1 KB
/
Copy pathcycles-protocol-v0.yaml
File metadata and controls
2076 lines (1936 loc) · 95.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
openapi: 3.1.0
info:
title: Cycles Budget Authority API
version: 0.1.25
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0
summary: v0 protocol for deterministic budget governance (reserve/commit) with optional decide + balance queries, plus debt/overdraft support with soft-limit reconciliation.
x-changelog:
url: ./changelogs/cycles-protocol-v0.md
format: keep-a-changelog
description: |-
PURPOSE (v0):
- Provide a minimal, language-agnostic protocol to enforce deterministic spend exposure for agent runtimes
via concurrency-safe reservations and idempotent commits.
- Include optional integration endpoints: /decide (soft landing) and /balances (operator visibility).
NON-GOALS (v0) (NORMATIVE):
- Budget establishment and funding operations are out of scope for v0.
v0 defines the reservation/commit/release enforcement plane and balance reporting only.
- v0 provides no API for budget CRUD (create/update/delete), allocation setting, credit/deposit, or debit/withdrawal.
Implementations MAY provide these via an operator/admin plane or a separate API; future versions may standardize them.
- A reservation lifecycle is denominated in exactly one unit (single-unit reserve/commit/release).
Multi-unit atomic reservation/settlement is a v1+ concern.
AUTH & TENANCY (NORMATIVE):
- Requests are authenticated via X-Cycles-API-Key.
- Server determines an "effective tenant" from the API key (or other auth context).
- Subject.tenant is a budgeting dimension and MUST be validated against the effective tenant.
If mismatched, server MUST return 403 FORBIDDEN.
- Reservation ownership MUST be enforced: every reservation is bound to the effective tenant at creation.
Any subsequent GET/commit/release for a reservation that exists but is owned by a different tenant
MUST return 403 FORBIDDEN.
- Balance visibility MUST be tenant-scoped: the server MUST only return balances within the effective tenant.
If a request attempts to query another tenant (e.g., tenant filter mismatches), server MUST return 403 FORBIDDEN.
EVOLUTION CONTRACT:
- This API starts at v0.1.0 with /v1 paths to avoid future client churn.
- v1+ evolution MUST be backward-compatible by default: new fields are additive, existing field meanings MUST NOT change.
- Breaking changes (e.g., new required fields, semantic changes) require a new major API path (e.g., /v2).
CORE INVARIANTS:
- Reserve is atomic across all derived scopes.
- Commit and release are idempotent.
- No double-charge on retries (idempotency key enforced).
ERROR SEMANTICS (NORMATIVE):
- Budget denials MUST return HTTP 409 with error=BUDGET_EXCEEDED.
- Overdraft limit exceeded MUST return HTTP 409 with error=OVERDRAFT_LIMIT_EXCEEDED in two cases:
1. During commit: when overage_policy=ALLOW_WITH_OVERDRAFT and (current_debt + delta) > overdraft_limit at commit time
2. During reservation: when the scope is in over-limit state (debt > overdraft_limit due to prior concurrent commits)
- Outstanding debt blocking reservation MUST return HTTP 409 with error=DEBT_OUTSTANDING
(when debt > 0 and new reservation is attempted).
- Finalized reservations MUST return HTTP 409 with error=RESERVATION_FINALIZED.
- Expired reservations MUST return HTTP 410 with error=RESERVATION_EXPIRED.
(commit/release: beyond expires_at_ms + grace_period_ms; extend: beyond expires_at_ms).
- Reservations that never existed MUST return HTTP 404 with error=NOT_FOUND.
- HTTP 429 is reserved for server-side throttling/rate limiting (optional in v0), not deterministic budget exhaustion.
- Unit mismatch MUST return HTTP 400 with error=UNIT_MISMATCH in any of these cases:
(a) reserve — estimate.unit does not match any budget stored for the derived scopes,
but at least one of those scopes has a budget in a different unit;
(b) commit — actual.unit differs from the reservation's estimate.unit;
(c) event — actual.unit does not match the budget stored for the target scope;
(d) decide — estimate.unit does not match any budget stored for the derived scopes,
but at least one of those scopes has a budget in a different unit. This is an
exception to /decide's general "return decision=DENY (200) without 4xx" pattern,
which applies only to budget-state conditions (debt, overdraft, insufficient
remaining), not request-validity errors like a wrong unit.
When the cause is a wrong unit (rather than the absence of any budget at the scope),
servers SHOULD populate the error response's `details` object with:
- `scope` — the canonical scope identifier where the mismatch was detected
- `requested_unit` — the unit supplied by the client
- `expected_units` — array of units for which a budget does exist at that scope
so clients can self-correct without a separate lookup. HTTP 404 with error=NOT_FOUND
is reserved for the case where the target scope has no budget in ANY unit (the
runtime plane uses the single NOT_FOUND code for all resource-not-found conditions;
the message field carries the specific reason, e.g. "Budget not found for provided
scope: ...").
- For expiry comparisons, “now” refers to server time (not client-provided time).
- When is_over_limit=true, server MUST return 409 OVERDRAFT_LIMIT_EXCEEDED for new reservations.
This takes precedence over DEBT_OUTSTANDING even when debt > 0.
OVERDRAFT RECONCILIATION (NORMATIVE):
- When concurrent commits cause debt > overdraft_limit on a scope, the server MUST mark that scope as "over-limit" (is_over_limit=true).
- Over-limit scopes MUST reject ALL new reservation attempts with 409 OVERDRAFT_LIMIT_EXCEEDED until debt is reduced below overdraft_limit.
- Operators reconcile over-limit scopes via budget funding operations (out-of-scope for this API).
When debt is repaid below overdraft_limit, is_over_limit automatically returns to false.
- Servers SHOULD provide monitoring/alerting when scopes enter over-limit state:
* Log events with scope identifier, current debt, and overdraft_limit
* Optionally emit webhooks or notifications to operators
* Optionally expose metrics endpoint showing over-limit scope count
- Clients SHOULD handle 409 OVERDRAFT_LIMIT_EXCEEDED on reservation as a signal to wait/retry with exponential backoff, or escalate to operators.
IDEMPOTENCY (NORMATIVE):
- If X-Idempotency-Key header is present and body.idempotency_key is present, they MUST match.
- Server MUST enforce idempotency per (effective tenant, endpoint, idempotency_key).
- On replay of an idempotent request that previously succeeded, server MUST return the original successful
response payload (including any server-generated identifiers such as reservation_id).
- If the same key is reused with a different request payload, server MUST return 409 IDEMPOTENCY_MISMATCH.
- Servers SHOULD compare idempotency payloads using a canonical JSON representation
(e.g., RFC 8785 JSON Canonicalization Scheme) or an equivalent stable serialization.
SCOPE DERIVATION (NORMATIVE):
- Server derives canonical scope identifiers and a canonical scope_path from Subject fields.
- Canonical ordering is: tenant → workspace → app → workflow → agent → toolset.
- Only explicitly provided subject levels are included in scope paths; intermediate gaps are skipped (not filled with "default").
- Scopes without budgets are skipped during enforcement; at least one derived scope MUST have a budget.
- affected_scopes returned by the server MUST be in that canonical order.
RESERVATION LEASING (GUIDANCE):
- To mitigate "zombie reservations" (client crash after reserve), SDKs SHOULD:
* keep ttl_ms short (typically 10s–30s),
* include modest estimation buffers when using overage_policy=REJECT,
* reserve in small initial leases and increase gradually ("slow start") for long or bursty operations,
* prefer chunked reserve/commit cycles for long-running actions rather than a single large reservation.
OVERDRAFT MONITORING (GUIDANCE):
- Implementations SHOULD provide visibility into over-limit states:
* Dashboard showing scopes with is_over_limit=true
* Alerts when debt exceeds overdraft_limit
* Time-series metrics: debt_utilization = debt / overdraft_limit
- Recommended alerting thresholds:
* Warning at 80% of overdraft_limit
* Critical at 100% (over-limit state)
- Recommended operator runbook:
1. Investigate which reservations caused the over-limit state
2. Determine if overdraft_limit should be increased (normal variance) or if this represents anomalous consumption (incident)
3. Fund the scope to repay debt below limit
4. Monitor that is_over_limit returns to false
5. Resume operations automatically
CORRELATION AND TRACING (NORMATIVE, cross-plane):
This section defines the cross-surface correlation contract for the entire Cycles
spec family. It is normative for every Cycles server operation on every plane
(runtime, governance-admin, action-kinds, and any extension that layers onto
these bases). Companion specs SHOULD carry a brief pointer to this section and
MUST NOT restate a conflicting contract.
Three-tier correlation model:
* request_id — one HTTP request grain. Set by the server. Echoed on
X-Request-Id response header, on ErrorResponse, and on every event / audit
entry that is causally downstream of the request (including entries emitted
from queued or deferred work spawned by the request).
* trace_id — logical-operation grain. W3C Trace Context-compatible. Accepted
from inbound headers or generated by the server. Echoed on X-Cycles-Trace-Id
response header and propagated to events, audit entries, and outbound
webhook deliveries.
* correlation_id — event-stream cluster grain. Set by the server as a
deterministic hash over (tenant_id, scope, action_kind_or_risk_class,
window, window_key) to JOIN threshold-alert → trip → reset chains and
observed_denied ↔ reservation.denied pairs. Scoped to the event stream only.
Inbound header precedence (server extracts trace_id by the first matching rule):
1. `traceparent` header, if present AND parses as a valid W3C Trace Context
value (version 00, non-all-zero trace-id, non-all-zero span-id) → use its
trace-id (the leftmost 32-hex segment).
2. Else `X-Cycles-Trace-Id` header, if present AND matches `^[0-9a-f]{32}$`
AND is not all-zero → use its value directly.
3. Else server generates a new trace_id: 16 random bytes encoded as 32
lowercase hex characters. The all-zero value is invalid per W3C Trace
Context §3.2.2.3 and MUST be re-rolled.
Header validation and precedence rules:
* A malformed `traceparent` OR malformed `X-Cycles-Trace-Id` MUST be treated
as absent for that header; the server falls through to the next rule. The
server MUST NOT reject a request for a malformed correlation header.
* If both `traceparent` and `X-Cycles-Trace-Id` are present, both valid, but
their trace-ids DISAGREE, `traceparent` wins (OpenTelemetry interop takes
precedence over the flat convenience header). The server MAY log this
condition for diagnostics but MUST NOT reject the request.
Outbound response contract (every plane, every response):
* Servers MUST echo `X-Cycles-Trace-Id` on every response (2xx, 4xx, 5xx).
The header is declared as `X-Cycles-Trace-Id` in this document's
`components.headers` and re-declared in the companion spec's own
`components.headers` for OpenAPI tooling conformance.
* ErrorResponse bodies MUST carry `trace_id` on every conformant error.
* Events and audit-log entries causally downstream of the request MUST
carry `trace_id`. See each companion spec's Event / AuditLogEntry schema.
Propagation contract:
* The server propagates trace_id onto: the audit-log entry for the request
(one per authenticated request that hits the governance plane), every
event emitted as a side effect of the request (runtime or governance),
and every outbound webhook delivery. Propagation across thread, queue, or
process boundaries is REQUIRED; loss at the request-thread boundary is
non-compliant.
* Outbound webhook deliveries carry `X-Cycles-Trace-Id` AND `traceparent`
headers constructed as documented in the WEBHOOK EVENT GUIDANCE section
below (including the trace-flags preservation rule).
Format: `^[0-9a-f]{32}$` — 32 lowercase hex characters (128-bit trace ID).
Backward compatibility:
* `trace_id` is declared as an OPTIONAL property on ErrorResponse, Event,
and AuditLogEntry schemas (no wire-contract break). Servers conformant
with this section MUST populate it; clients MUST tolerate its absence on
entries emitted by older servers.
* Adding `X-Cycles-Trace-Id` as a response header is additive; clients that
do not read the header are unaffected.
* Accepting `traceparent` / `X-Cycles-Trace-Id` as inbound request headers
is additive; clients that do not send them are unaffected.
WEBHOOK EVENT GUIDANCE (GUIDANCE):
Implementations MAY emit webhook events when runtime operations produce observable state changes.
This enables operators and tenant applications to react to budget state transitions in real-time
without polling. The webhook delivery system is separate from the protocol endpoints — it does not
add new API paths to the runtime server.
Event types emitted by the runtime server:
* reservation.denied — Reserve or decide returned DENY (budget exceeded, overdraft limit, frozen, etc.)
* reservation.commit_overage — Commit actual amount exceeded estimated amount
* reservation.expired — Reservation TTL expired without commit or release (via background sweeper)
* budget.exhausted — Remaining budget reached 0 after a reservation or event
* budget.debt_incurred — Commit created new debt via ALLOW_WITH_OVERDRAFT policy
* budget.over_limit_entered — is_over_limit flipped to true (debt > overdraft_limit)
* budget.over_limit_exited — Debt repaid below overdraft_limit (via admin funding operations)
* budget.threshold_crossed — Utilization crossed a configured threshold (e.g., 80%, 95%)
* budget.burn_rate_anomaly — Spend rate exceeded baseline by configured multiplier
Event types emitted by the admin/operator server:
* tenant.created/updated/suspended/reactivated/closed — Tenant lifecycle
* budget.created/updated/funded/debited/reset/debt_repaid/frozen/unfrozen/closed — Budget lifecycle
* api_key.created/revoked/expired/permissions_changed/auth_failed — API key lifecycle
* policy.created/updated/deleted — Policy lifecycle
* system.store_connection_lost/restored, system.high_latency — System health
* system.webhook_delivery_failed — Meta-alert for persistent delivery failures
Standard event payload schema (JSON):
* event_id (string, required) — Globally unique (e.g., "evt_01abc..."). Use for deduplication.
* event_type (string, required) — Dotted format: "{category}.{action}" (e.g., "reservation.denied")
* category (string, required) — One of: budget, reservation, tenant, api_key, policy, system
* timestamp (string, date-time, required) — ISO 8601 UTC
* tenant_id (string, required) — Tenant context. System events use "__system__".
* scope (string, optional) — Full scope path affected (e.g., "tenant:acme/agent:bot")
* actor (object, optional) — Who caused the event: { type: admin|api_key|system|scheduler, key_id?, source_ip? }
* source (string, required) — Service that emitted: "cycles-server", "cycles-admin", "expiry-sweeper"
* data (object, optional) — Event-specific payload (varies by event_type)
* correlation_id (string, optional) — Links related events for chain reconstruction
* request_id (string, optional) — X-Request-Id from the originating HTTP request.
MUST be populated on every event causally downstream of an HTTP request,
including events emitted from queued, deferred, or otherwise-async work spawned
by that request. MAY be absent on internal sweeper/expiry-generated events that
have no originating HTTP request. See CORRELATION AND TRACING section below.
* trace_id (string, optional, pattern ^[0-9a-f]{32}$) — W3C Trace Context trace-id
for the logical operation. Populated on every event produced by a server that
conforms to the CORRELATION AND TRACING contract below.
* metadata (object, optional) — Operator-defined key-value pairs
Webhook delivery protocol:
* Delivery method: HTTP POST to subscriber's URL with JSON event payload as body
* Delivery semantics: At-least-once. Consumers MUST deduplicate using event_id.
* Ordering: Events for the same tenant are dispatched in order. Cross-tenant ordering NOT guaranteed.
* Non-blocking: Webhook delivery MUST NOT block the operation that produced the event.
Required HTTP headers on webhook delivery:
* Content-Type: application/json
* X-Cycles-Event-Id: {event_id} — For deduplication
* X-Cycles-Event-Type: {event_type} — For routing
* X-Cycles-Signature: sha256={hex} — HMAC-SHA256 of raw request body using subscription's signing secret
* X-Cycles-Trace-Id: {trace_id} — W3C Trace Context trace-id (32-hex) for the
logical operation that produced this event. Always required; the server always
has a trace_id per the CORRELATION AND TRACING fallback-generate rule.
* traceparent: 00-{trace_id}-{fresh-span-id-16-hex}-{trace-flags} — W3C Trace
Context version 00 header. Always required. trace_id MUST equal the value in
X-Cycles-Trace-Id. span-id MUST be freshly generated for the outbound delivery
(NOT reused from inbound). trace-flags rules:
- If the inbound request to Cycles carried a valid `traceparent`, the server
MUST preserve the inbound trace-flags byte on the outbound `traceparent`
(so a `sampled=0` upstream is not silently flipped to `sampled=1`).
- If the trace was derived from `X-Cycles-Trace-Id` (no inbound W3C
`traceparent`) OR generated fresh by the server, the server uses a default
trace-flags value of `01` (sampled).
The `trace_id` field also appears in the event envelope body so subscribers
unfamiliar with W3C Trace Context can still correlate via the JSON payload.
* User-Agent: {service-name}/{version}
* Custom headers from subscription configuration (e.g., Authorization)
Signature verification (X-Cycles-Signature):
* Algorithm: HMAC-SHA256
* Input: Raw JSON request body (bytes, not parsed)
* Key: Subscription's signing_secret (UTF-8 encoded)
* Format: "sha256=" + lowercase hex encoding of HMAC digest
* Consumers SHOULD verify the signature before processing the event.
* Use constant-time comparison (e.g., hmac.compare_digest) to prevent timing attacks.
Retry and failure handling:
* On non-2xx response: exponential backoff retry (default: 5 retries, 1s/2s/4s/8s/16s, max 60s)
* After all retries exhausted: delivery marked FAILED, system.webhook_delivery_failed event emitted
* After N consecutive failures (default 10): subscription auto-disabled (status → DISABLED)
* Disabled subscriptions can be re-enabled via admin API (resets failure counter)
Retention:
* Event records: 90 days hot storage (recommended). TTL enforced via Redis EXPIRE.
* Delivery records: 14 days (operational debugging data).
* ZSET index entries: Trimmed hourly by background cleanup job.
* Stale deliveries: Deliveries older than 24h (configurable) are auto-failed on pickup
to prevent delivering ancient webhooks after prolonged service outage.
Extensibility:
* New event types MAY be added in future versions without a breaking change.
* Consumers MUST ignore unrecognized event types gracefully.
* Custom event types MUST use a "custom." prefix (e.g., "custom.billing.invoice_sent").
servers:
- url: https://api.cycles.local
description: Replace with your implementation endpoint
tags:
- name: Decisions
description: Optional preflight checks (no reservation created)
- name: Reservations
description: Reservation, commit, release, and (optional) get-by-id operations
- name: Balances
description: Query balances for operator visibility
- name: Events
description: Optional post-only accounting for non-estimable actions
security:
- ApiKeyAuth: []
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-Cycles-API-Key
# Added 2026-04-13 for admin-on-behalf-of access on a small set of
# ops-critical reservation endpoints (list / get / release). Same
# header the governance-admin spec already uses, so admin operators
# can authenticate against the runtime plane with one key.
AdminKeyAuth:
type: apiKey
in: header
name: X-Admin-API-Key
headers:
X-Request-Id:
description: Unique request identifier for debugging
schema:
type: string
X-Cycles-Trace-Id:
description: >-
W3C Trace Context-compatible correlation identifier echoed on every
response (2xx, 4xx, 5xx) on every plane. 32 lowercase hex characters
(128-bit trace ID, matching the W3C Trace Context trace-id field).
Links the request, its audit entry, all side-effect events, and any
outbound webhook deliveries produced by the request. See the
"CORRELATION AND TRACING" section of this document's `info.description`
for inbound-header precedence, fallback-generation rules, and
cross-surface propagation contract.
schema:
type: string
pattern: ^[0-9a-f]{32}$
X-RateLimit-Remaining:
description: Number of requests remaining in current window (optional in v0)
schema:
type: integer
X-RateLimit-Reset:
description: Unix timestamp (seconds) when rate limit resets (optional in v0)
schema:
type: integer
format: int64
X-Cycles-Tenant:
description: Effective tenant identifier derived from auth context (optional in v0)
schema:
type: string
parameters:
IdempotencyKeyHeader:
name: X-Idempotency-Key
in: header
required: false
description: >-
Optional idempotency key header. If both header and body idempotency_key are provided, they MUST match.
Server MUST enforce idempotency per endpoint by (effective tenant, endpoint, idempotency_key).
On replay of an idempotent request that previously succeeded, server MUST return the original successful
response payload (including any server-generated identifiers such as reservation_id).
schema:
$ref: '#/components/schemas/IdempotencyKey'
ReservationId:
name: reservation_id
in: path
required: true
schema:
type: string
minLength: 1
maxLength: 128
Limit:
name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 200
default: 50
description: Maximum number of results to return
Cursor:
name: cursor
in: query
required: false
schema:
type: string
description: Opaque cursor from previous response
responses:
ErrorResponse:
description: Error response
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
X-Cycles-Trace-Id:
$ref: '#/components/headers/X-Cycles-Trace-Id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
schemas:
IdempotencyKey:
type: string
minLength: 1
maxLength: 256
ErrorCode:
type: string
enum:
- INVALID_REQUEST
- UNAUTHORIZED
- FORBIDDEN
- NOT_FOUND
- BUDGET_EXCEEDED
- BUDGET_FROZEN
- BUDGET_CLOSED
- RESERVATION_EXPIRED
- RESERVATION_FINALIZED
- IDEMPOTENCY_MISMATCH
- UNIT_MISMATCH
- OVERDRAFT_LIMIT_EXCEEDED
- DEBT_OUTSTANDING
- MAX_EXTENSIONS_EXCEEDED
- INTERNAL_ERROR
ErrorResponse:
type: object
required: [error, message, request_id]
additionalProperties: false
example:
error: BUDGET_EXCEEDED
message: Insufficient remaining budget for scope tenant:acme-corp
request_id: req_abc123def456
trace_id: 4bf92f3577b34da6a3ce929d0e0e4736
properties:
error:
$ref: '#/components/schemas/ErrorCode'
message:
type: string
request_id:
type: string
trace_id:
type: string
pattern: ^[0-9a-f]{32}$
description: >-
W3C Trace Context-compatible correlation identifier. 32 lowercase
hex characters (128-bit trace ID, matching the W3C Trace Context
trace-id field in the `traceparent` header). Links this error
response to the request that triggered it and to any events /
audit entries emitted before the error was returned. Optional on
the wire for forward compatibility with historical responses
emitted by servers older than the first release that populated
this field, but servers conformant with the CORRELATION AND
TRACING contract MUST populate it on every error response. See
the "CORRELATION AND TRACING" section of this document's
`info.description` for header formats and propagation rules.
details:
type: object
additionalProperties: true
DecisionEnum:
type: string
enum: [ALLOW, ALLOW_WITH_CAPS, DENY]
DecisionReasonCode:
type: string
maxLength: 128
description: >-
Stable machine-readable reason for why a /decide call or a reservation
returned decision=DENY. Populated on DecisionResponse.reason_code and on
ReservationCreateResponse.reason_code. The human-readable `message` field
on the parent response (when present) may carry additional context.
EXTENSIBILITY (NORMATIVE):
DecisionReasonCode is intentionally an OPEN string (not a closed enum)
so that:
(a) future minor versions of this base spec can add new values without
a breaking change, and
(b) companion extension specs (e.g., cycles-protocol-extensions-v0.1.26.yaml)
can define additional reason codes without modifying the base schema.
Clients MUST gracefully handle unknown values — log them and map to
generic DENY handling (i.e., "the request was denied; treat as a terminal
failure even if we don't recognize the specific reason").
KNOWN VALUES (v0.1.25 base):
- BUDGET_EXCEEDED — remaining amount insufficient on at least one derived scope
(evaluated against the requested estimate.amount).
- BUDGET_FROZEN — a derived scope has a budget in FROZEN status
(operator-set, no mutations allowed).
- BUDGET_CLOSED — a derived scope has a budget in CLOSED status
(permanently closed).
- BUDGET_NOT_FOUND — no budget exists at any derived scope in the requested unit.
On non-dry reserve and /v1/events paths this same condition surfaces as HTTP
404 with error=NOT_FOUND instead.
- OVERDRAFT_LIMIT_EXCEEDED — either (a) debt + delta > overdraft_limit on commit,
or (b) the scope is in over-limit state (is_over_limit=true) and no new
reservations are permitted until reconciled.
- DEBT_OUTSTANDING — a derived scope has debt > 0 and overdraft_limit == 0
(no policy permits further debt accrual).
KNOWN VALUES (v0.1.26 runtime extension):
- ACTION_QUOTA_EXCEEDED — a per-kind or risk-class action quota rule was
exceeded for the target scope and window.
- ACTION_KIND_DENIED — the action kind is in the matching policy's
denied_action_kinds list.
- ACTION_KIND_NOT_ALLOWED — the matching policy has a non-empty
allowed_action_kinds list and the action kind is not in it.
See cycles-protocol-extensions-v0.1.26.yaml for full semantics,
evaluation order, and DenyDetail structure (populated alongside
reason_code for v0.1.26 denials).
Distinct from the `ErrorCode` enum: reason codes appear only on 200 OK
responses with decision=DENY; ErrorCodes appear only on 4xx/5xx responses
with an `error` field. Some labels overlap (e.g. BUDGET_EXCEEDED appears
in both sets) because the same underlying condition is reported two ways
depending on the endpoint: /decide and dry_run surface it as a non-4xx
DENY decision, while non-dry reserve surfaces it as a 409 error.
SDK GENERATION GUIDANCE:
Codegen tools SHOULD generate a string type (not a strict enum) and
expose the known values above as constants or string literal union
members for ergonomic use. SDK code MUST NOT reject unknown values
at the deserialization boundary.
UnitEnum:
type: string
description: >
Standard units.
USD_MICROCENTS preserves precision for per-call and batched accounting (int64).
- 1 USD_MICROCENTS = 10^-6 cents = 10^-8 dollars
- 1 USD = 100 cents = 10^8 USD_MICROCENTS
- Max int64 ≈ 9.22e18 USD_MICROCENTS ≈ $92.2B
TOKENS are integer token counts.
CREDITS/RISK_POINTS are generic integer units (optional in v0 implementations).
enum: [USD_MICROCENTS, TOKENS, CREDITS, RISK_POINTS]
SortDirection:
type: string
enum: [asc, desc]
default: desc
description: >-
Sort direction for list endpoints. Used with per-endpoint
`sort_by` parameters (introduced in revision 2026-04-16 on
listReservations). When `sort_by` is provided but `sort_dir`
is omitted, servers MUST default to "desc". Servers that do
not recognize the parameter MUST ignore it without error
(additive-parameter guarantee). This schema is carried
inline in each spec of the family (protocol + governance);
the companion-specs publication model does not use cross-spec
$ref.
Amount:
type: object
required: [unit, amount]
additionalProperties: false
example:
unit: USD_MICROCENTS
amount: 500000
properties:
unit:
$ref: '#/components/schemas/UnitEnum'
amount:
type: integer
format: int64
minimum: 0
SignedAmount:
type: object
required: [unit, amount]
additionalProperties: false
description: >-
Like Amount, but allows negative values. Used for Balance.remaining which can be negative in overdraft scenarios.
example:
unit: USD_MICROCENTS
amount: -120000
properties:
unit:
$ref: '#/components/schemas/UnitEnum'
amount:
type: integer
format: int64
description: >-
Signed integer amount. Can be negative when representing remaining balance in overdraft state
(debt > allocated - spent - reserved).
Subject:
type: object
description: >
Dimension bag for hierarchical budgets. At least one standard field (tenant, workspace, app, workflow, agent, or toolset) MUST be provided.
A subject containing only `dimensions` is invalid; server MUST return 400 INVALID_REQUEST.
Hierarchy: tenant → workspace → app → workflow → agent → toolset.
EXTENSIBILITY (v0):
- dimensions is an optional, user-defined map for alternative taxonomies
(e.g., cost_center/department/project) and policy/reporting.
- v0 servers MAY ignore dimensions for budgeting decisions; if they do, they still MUST accept and round-trip it.
- Keys SHOULD be lowercase and match ^[a-z0-9_.-]+$ for stable canonicalization; values are opaque strings.
additionalProperties: false
minProperties: 1
anyOf:
- required: [tenant]
- required: [workspace]
- required: [app]
- required: [workflow]
- required: [agent]
- required: [toolset]
properties:
tenant:
type: string
maxLength: 128
workspace:
type: string
maxLength: 128
app:
type: string
maxLength: 128
workflow:
type: string
maxLength: 128
agent:
type: string
maxLength: 128
toolset:
type: string
maxLength: 128
dimensions:
type: object
description: Optional custom dimensions for enterprise taxonomies and policy/reporting.
additionalProperties:
type: string
maxLength: 256
maxProperties: 16
Action:
type: object
required: [kind, name]
additionalProperties: false
properties:
kind:
type: string
maxLength: 64
description: >
Action type identifier. Recommended format: <category>.<operation>
Examples: llm.completion, llm.embedding, tool.search, tool.calculator,
db.query, db.write, http.get, http.post, file.upload, message.email, webhook.outbound
name:
type: string
maxLength: 256
description: Provider/model/tool identifier (e.g., "openai:gpt-4o-mini", "web.search")
tags:
type: array
maxItems: 10
items:
type: string
maxLength: 64
description: Optional policy tags (e.g., ["prod","customer-facing"])
Caps:
type: object
description: >
Optional soft-landing constraints returned by /decide or reservation ALLOW_WITH_CAPS.
v0 intentionally keeps caps simple and concrete (no condition language).
PRECEDENCE RULES:
- If tool_allowlist is non-empty, ONLY those tools are allowed (denylist ignored).
- Otherwise, if tool_denylist is non-empty, all tools EXCEPT those are allowed.
- If both are empty/null, no tool restrictions apply.
- Tool names are case-sensitive and match Action.name exactly.
additionalProperties: false
properties:
max_tokens:
type: integer
minimum: 0
max_steps_remaining:
type: integer
minimum: 0
tool_allowlist:
type: array
items:
type: string
maxLength: 256
tool_denylist:
type: array
items:
type: string
maxLength: 256
cooldown_ms:
type: integer
minimum: 0
# ---- Decide (optional) ----
DecisionRequest:
type: object
required: [idempotency_key, subject, action, estimate]
additionalProperties: false
properties:
idempotency_key:
$ref: '#/components/schemas/IdempotencyKey'
subject:
$ref: '#/components/schemas/Subject'
action:
$ref: '#/components/schemas/Action'
estimate:
$ref: '#/components/schemas/Amount'
metadata:
type: object
additionalProperties: true
DecisionResponse:
type: object
required: [decision]
additionalProperties: false
example:
decision: ALLOW
affected_scopes:
- "tenant:acme-corp"
- "tenant:acme-corp/workspace:prod"
properties:
decision:
$ref: '#/components/schemas/DecisionEnum'
description: >
ALLOW indicates sufficient budget exists. ALLOW_WITH_CAPS indicates budget exists
but soft constraints apply. DENY indicates insufficient budget or policy block.
Note: /decide does not create a reservation; a subsequent reservation call may
still fail if concurrent activity depletes budget between the two calls.
caps:
$ref: '#/components/schemas/Caps'
description: Present only when decision=ALLOW_WITH_CAPS; MUST be absent otherwise.
reason_code:
$ref: '#/components/schemas/DecisionReasonCode'
retry_after_ms:
type: integer
minimum: 0
affected_scopes:
type: array
items:
type: string
description: Canonical scope identifiers impacted by this decision, in canonical order.
# ---- Reservations (core) ----
CommitOveragePolicy:
type: string
description: >
How server handles commits where actual > reserved.
REJECT: reject commit (client must reserve a buffer or over-estimate).
Recommended: add 10-20% buffer to estimates when using REJECT.
If the action already happened externally, this creates an unaccounted gap.
ALLOW_IF_AVAILABLE (default): commit always succeeds when actual > reserved.
If remaining budget supports the full delta, charge it atomically across all derived scopes.
Otherwise, cap the delta to available remaining (minimum across all affected scopes, floor 0),
charge estimate + capped_delta, and set is_over_limit=true on scopes where the full delta
could not be covered. This blocks future reservations until reconciled.
Never creates debt. Never rejects a commit — the action already happened.
ALLOW_WITH_OVERDRAFT: if remaining budget supports the delta, commit normally.
Otherwise, check if (debt + delta) <= overdraft_limit across all affected scopes.
If yes: commit succeeds, add delta to debt, remaining can go negative.
If no: commit fails with 409 OVERDRAFT_LIMIT_EXCEEDED.
This policy ensures the ledger always reflects reality even when budget is exhausted at commit time.
CONCURRENCY (NORMATIVE):
- The overdraft_limit check is performed per-commit and is NOT atomic across concurrent commits to the same scope.
- Multiple commits may each individually pass the (debt + delta) <= limit check but collectively cause debt to exceed overdraft_limit.
- This is acceptable: all commits succeed (actions already happened), and the scope enters "over-limit" state,
blocking future reservations until reconciled.
- Servers SHOULD use optimistic locking or compare-and-swap on debt updates to ensure individual commit atomicity,
but cross-commit atomicity is not required.
enum: [REJECT, ALLOW_IF_AVAILABLE, ALLOW_WITH_OVERDRAFT]
ReservationStatus:
type: string
description: Reservation lifecycle state (v1+ may add additional terminal states).
enum: [ACTIVE, COMMITTED, RELEASED, EXPIRED]
ReservationCreateRequest:
type: object
required: [idempotency_key, subject, action, estimate]
additionalProperties: false
example:
idempotency_key: "idem_20260412_run42_step1"
subject:
tenant: acme-corp
workspace: prod
agent: summarizer
action:
kind: llm.completion
name: summarize-document
estimate:
unit: USD_MICROCENTS
amount: 500000
ttl_ms: 30000
properties:
idempotency_key:
$ref: '#/components/schemas/IdempotencyKey'
subject:
$ref: '#/components/schemas/Subject'
action:
$ref: '#/components/schemas/Action'
estimate:
$ref: '#/components/schemas/Amount'
ttl_ms:
type: integer
minimum: 1000
maximum: 86400000
default: 60000
grace_period_ms:
type: integer
minimum: 0
maximum: 60000
default: 5000
description: >
Grace window after TTL for in-flight commits. Default 5s.
OPERATIONAL GUIDANCE:
- For high-latency actions (streaming LLM, slow APIs), consider setting higher (10-30s).
INTEROP / SAFETY RATIONALE:
- The 60s maximum is an interoperability ceiling to limit long-lived "in-flight" commit windows and reduce
zombie-reservation and abuse surface (grace extends the time that a crashed client can keep budget locked).
LONG-RUNNING WORKFLOWS:
- For multi-minute operations, clients SHOULD keep ttl_ms relatively short and use /v1/reservations/{reservation_id}/extend
as a heartbeat, and/or use chunked reserve/commit cycles rather than relying on large grace_period_ms.
overage_policy:
$ref: '#/components/schemas/CommitOveragePolicy'
default: ALLOW_IF_AVAILABLE
dry_run:
type: boolean
default: false
description: >
Shadow-mode evaluation. If true, the server MUST evaluate the reservation request and return
decision/caps/affected_scopes as if it would reserve, but MUST NOT modify balances, persist a
reservation, or require commit/release. Intended for safe rollout and testing of full reserve-path logic.
metadata:
type: object
additionalProperties: true
Balance:
type: object
required: [scope, scope_path, remaining]
additionalProperties: false
example:
scope: "tenant:acme-corp/workspace:prod"
scope_path: "tenant:acme-corp/workspace:prod"
remaining:
unit: USD_MICROCENTS
amount: 4577000
reserved:
unit: USD_MICROCENTS
amount: 500000
spent:
unit: USD_MICROCENTS
amount: 423000
allocated:
unit: USD_MICROCENTS
amount: 5500000
description: >-
Ledger state for a single (scope, unit) balance.
UNIT CONSISTENCY (NORMATIVE):
- All Amount fields within a Balance (remaining/reserved/spent/allocated/debt/overdraft_limit), if present, MUST share the same unit.
- Servers MUST NOT emit a Balance with mixed units; clients MAY treat such responses as invalid.
LEDGER INVARIANT (NORMATIVE):
- If allocated, spent, reserved, and debt are all present, then remaining MUST satisfy:
remaining.amount = allocated.amount - spent.amount - reserved.amount - debt.amount (same unit).
- Note that remaining.amount can be negative when debt exceeds available budget.
- If allocated is absent, remaining is authoritative and clients MUST treat it as opaque (i.e., MUST NOT derive it from other fields).
- Servers MUST NOT emit a Balance where the invariant is violated when all fields are present.
DEBT SEMANTICS (NORMATIVE):
- debt represents actual consumption that occurred when insufficient budget was available to cover a commit overage.
- debt is created only when overage_policy=ALLOW_WITH_OVERDRAFT and (remaining + released_reservation) < actual at commit time.
- When debt > 0, new reservations MUST be rejected with 409 DEBT_OUTSTANDING (unless explicitly allowed by policy).
- debt MUST be repaid via budget funding operations (out-of-scope for this API) before new reservations are permitted.
- When budget is added to a scope with debt > 0, debt MUST be repaid first, then remaining funds added to remaining.
OVERDRAFT LIMIT (NORMATIVE):
- overdraft_limit defines the target maximum debt for this scope.
- If overdraft_limit is absent or amount=0, no overdraft is permitted (behaves as ALLOW_IF_AVAILABLE).
- The overdraft_limit is enforced per-commit but is NOT atomic across concurrent commits.
Multiple concurrent commits MAY cause cumulative debt to briefly exceed overdraft_limit.
- Individual commits are rejected with 409 OVERDRAFT_LIMIT_EXCEEDED only if (current_debt + delta) > overdraft_limit
at the moment of that specific commit.
- When debt > overdraft_limit after reconciliation (due to concurrent commits), the scope enters an "over-limit" state:
* The server MUST set is_over_limit=true on the Balance
* ALL new reservations against that scope MUST be rejected with 409 OVERDRAFT_LIMIT_EXCEEDED (not DEBT_OUTSTANDING)
* Existing active reservations MAY be committed or released normally
* The server SHOULD emit alerts/events for operator intervention
- Once debt is repaid below overdraft_limit, is_over_limit returns to false and normal reservation operation resumes.
properties:
scope:
type: string
description: Canonical scope identifier (server-derived), e.g. "tenant:t1", "workflow:run123"
scope_path:
type: string
description: Canonical hierarchical path (server-derived)
remaining:
$ref: '#/components/schemas/SignedAmount'
description: >-
Available budget for new reservations. Can be negative when debt exceeds allocated budget.
Formula: remaining = allocated - spent - reserved - debt
reserved:
$ref: '#/components/schemas/Amount'
description: Amount currently locked by active reservations
spent:
$ref: '#/components/schemas/Amount'
description: Amount successfully committed through reservations
debt:
$ref: '#/components/schemas/Amount'
description: >-
Overdraft amount from commits where actual > reserved and insufficient budget existed to cover the delta.
Represents consumption that happened but couldn't be paid from available budget at commit time.
Must be repaid before new reservations are allowed (unless policy permits otherwise).
allocated:
$ref: '#/components/schemas/Amount'
description: Optional total budget cap if a fixed allocation exists.
overdraft_limit:
$ref: '#/components/schemas/Amount'
description: >-
Optional maximum debt allowed for this scope. If absent or amount=0, no overdraft is permitted.
When present and amount > 0, commits can succeed by creating debt up to this limit.
is_over_limit:
type: boolean
description: >-
True when debt > overdraft_limit due to concurrent commits creating cumulative debt beyond the limit,
or when ALLOW_IF_AVAILABLE could not cover the full overage delta and capped the charge.
When true, ALL new reservations against this scope are blocked with 409 OVERDRAFT_LIMIT_EXCEEDED
until reconciled (debt repaid below overdraft_limit, or is_over_limit cleared by operator).
Defaults to false when absent.
ReservationCreateResponse:
type: object
required: [decision, affected_scopes]
additionalProperties: false
example:
decision: ALLOW
reservation_id: "rsv_a1b2c3d4"
reserved:
unit: USD_MICROCENTS
amount: 500000
expires_at_ms: 1712956830000
scope_path: "tenant:acme-corp/workspace:prod/agent:summarizer"
affected_scopes:
- "tenant:acme-corp"
- "tenant:acme-corp/workspace:prod"
- "tenant:acme-corp/workspace:prod/agent:summarizer"
properties:
decision:
$ref: '#/components/schemas/DecisionEnum'
description: >
For dry_run=true, decision MAY be DENY. For dry_run=false, insufficient budget MUST be expressed via 409 BUDGET_EXCEEDED (not decision=DENY).
reservation_id:
type: string
description: Present if decision is ALLOW or ALLOW_WITH_CAPS and dry_run is false. MUST be absent when dry_run is true.
reserved:
$ref: '#/components/schemas/Amount'
expires_at_ms:
type: integer
format: int64
scope_path:
type: string
description: Canonical scope path (server-derived)
affected_scopes:
type: array
items:
type: string
description: Canonical scope identifiers impacted by this reservation, in canonical order.
caps:
$ref: '#/components/schemas/Caps'
description: Present only when decision=ALLOW_WITH_CAPS; MUST be absent otherwise.
balances:
type: array
items: