-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcycles-protocol-v0.yaml
More file actions
2996 lines (2835 loc) · 153 KB
/
Copy pathcycles-protocol-v0.yaml
File metadata and controls
2996 lines (2835 loc) · 153 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.16
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.
PAYMENT-RAIL TERMINOLOGY BOUNDARY (NORMATIVE):
- A Cycles reservation holds spending authority in a budget ledger; it does not hold funds
on a card, bank, blockchain, or other payment rail.
- A Cycles commit finalizes recorded economic exposure; it does not authorize, capture,
charge, or settle a payment on a payment rail.
- A Cycles release returns unused budget authority; it does not void or refund a payment.
- Implementations that move money MUST execute and reconcile payment-rail operations separately
and MUST NOT represent a Cycles lifecycle transition as proof that funds moved or were reversed.
AUTH & TENANCY (NORMATIVE):
- Requests are authenticated via X-Cycles-API-Key, EXCEPT endpoints that
explicitly declare `security: []`, which are PUBLIC and require no API key.
Two such endpoints exist, both in the CyclesEvidence surface:
`GET /v1/evidence/{evidence_id}` (envelope retrieval — its `evidence_id`
is an unguessable content-hash capability and the envelope is
content-addressed and signed) and `GET /v1/.well-known/cycles-jwks.json`
(the signer's public JWK Set — public keys only, the standard posture for
a verification key set). See each operation's description for the rationale.
- 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).
- Closed owning tenant MUST return HTTP 409 with error=TENANT_CLOSED on the persisting
mutation surface — reservation create (POST /v1/reservations with dry_run
absent or false), commit, release, extend, AND POST /v1/events (createEvent) — when the
owning tenant's status is CLOSED and the CLOSED flip is durable
(added to the ErrorCode enum in revision 2026-07-10, mirroring the governance spec's
code of the same name). POST /v1/events is a persisting BUDGET DEBIT (post-only
accounting that directly mutates remaining/spent on every budgeted derived scope and
MAY accrue overdraft debt under overage_policy=ALLOW_WITH_OVERDRAFT, with concurrency
semantics as for commit) — it is on the same runtime-plane persisting surface as the
reservation mutations and MUST be guarded identically. This is the runtime-plane half
of the terminal-owner mutation guard in cycles-governance-admin-v0.1.25.yaml (CASCADE
SEMANTICS Rule 2); that section enumerates "any reservation create/commit/release/extend"
on the runtime plane, and createEvent is the remaining runtime-plane persisting budget
mutation subject to the same Mode B invariant, so the runtime plane guards it on the
same basis. Rationale: the close
cascade revokes the tenant's API keys, so a closed tenant usually surfaces on this
plane as 401 UNAUTHORIZED — but Mode B invariant (a) of that cascade requires that a
mutation observed AFTER the CLOSED flip MUST NOT succeed even in the window before
keys are revoked; this binding closes that race on the runtime plane.
POST /v1/events has no dry_run or decide mode — it always persists — so on a FRESH
(non-replay) request the closed-tenant outcome there is the 409 TENANT_CLOSED of the
persisting surface; it never returns a decision=DENY. Everything below that the
persisting surface inherits — fail-closed on a malformed/undeterminable tenant record
(500 INTERNAL_ERROR), the not-applicable case when no governance plane exists, and the
idempotent same-key replay exception (a same-key replay of a pre-close event returns the
original stored 201 response, taking precedence over the guard exactly as on the
reservation surface) — applies to createEvent unchanged.
Non-persisting evaluations: POST /v1/reservations with dry_run=true and POST /v1/decide
MUST NOT produce HTTP 409 TENANT_CLOSED for a closed owning tenant. A fresh (non-replay)
evaluation MUST instead reflect the closed tenant as-if-live: decision=DENY with
reason_code=TENANT_CLOSED. (Same-key replays of pre-close evaluations return the
original stored response per the IDEMPOTENCY section — replay precedence applies here
exactly as on the persisting surface.) Rationale: dry-run and /decide outcomes are attestations of
what live execution would do (and MAY be captured as signed evidence per
cycles-evidence-v0.2.yaml); an evaluation that ignores a durable CLOSED flip would
attest ALLOW for a request whose live execution MUST fail.
Guard evaluation (both surfaces): a tenant record with status CLOSED triggers the
guard (409 on the persisting surface, decision=DENY on the non-persisting surface); a
tenant record that exists but whose status cannot be determined (malformed or corrupt
record) MUST fail closed with HTTP 500 INTERNAL_ERROR — on the non-persisting surface
too, because the server cannot attest against corrupt governance state; a subject
tenant with no tenant record is not guarded (there is no status to observe).
Precedence: for non-replay mutations on a closed tenant's reservations,
TENANT_CLOSED takes precedence over the reservation-state errors
(RESERVATION_FINALIZED, RESERVATION_EXPIRED) — Rule 2 rejects "regardless of
that child's own current status". Idempotent replays are the exception: a
same-key replay of a mutation that succeeded BEFORE the close retains replay
precedence and MUST return the original stored response payload per the
IDEMPOTENCY section (consistent with Rule 2's invariant (b) — the cascade is
idempotent and does not rewrite already-finalized outcomes).
Cross-plane applicability: a deployment that operates a governance plane (tenant
records exist) MUST enforce this guard — either by making the owning tenant's
CLOSED status observable to the runtime plane, or by enforcing an equivalent
post-flip mutation guard at a central enforcement point in front of these
operations. The requirement is behavioral (the 409 TENANT_CLOSED rejection),
not architectural: choosing not to wire tenant status through to the runtime
plane does NOT exempt a deployment. Only deployments with NO governance plane
at all (no tenant records exist anywhere in the deployment) have no tenant
status to enforce — the rule is not applicable to them.
Non-mutating reservation reads (GET /v1/reservations, GET /v1/reservations/{id})
MUST NOT be rejected with TENANT_CLOSED: they remain available (subject to normal
auth) on reservations of a CLOSED tenant for post-close audit, mirroring Rule 2's
read-access rule.
- 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;
getReservation: any reservation whose status is EXPIRED — see that operation's
EXPIRY note. Clarified in revision 2026-07-03; previously the parenthetical
enumerated only the mutation endpoints, leaving the GET case ambiguous.)
- 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.
429 responses carry error=LIMIT_EXCEEDED (added to the ErrorCode enum in revision 2026-07-04, mirroring the
governance spec's code of the same name) plus the Retry-After and X-RateLimit-Reset headers.
- 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 outcome and payload (including any server-generated identifiers such as reservation_id), except
for volatile response observations defined below.
- remaining_ttl_ms on a replayed createReservation or extendReservation success is a volatile response
observation, not part of the attested CyclesEvidence payload. When emitted, the server MUST recompute it
while constructing the replay response as
max(0, original expires_at_ms - current authoritative server time); it MUST NOT replay the originally
stored remaining_ttl_ms value. It MUST be 0 if the reservation is no longer ACTIVE and MAY
conservatively understate current lead if a later, separately keyed extension moved expiry outward.
All other fields replay verbatim as the original outcome.
- 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|admin_on_behalf_of|system|scheduler, key_id?, source_ip? }
(admin_on_behalf_of: admin key exercising a tenant-scoped dual-auth
operation; added in revision 2026-07-04, mirroring the governance
spec's Event.actor.type enum)
* 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 INITIALLY DISPATCHED in
order; cross-tenant ordering NOT guaranteed. Clarified in revision
2026-07-04: the ordering guarantee applies to first delivery
attempts only. A failed delivery re-enters the queue after its
retry backoff, so retried deliveries MAY arrive after later events
for the same tenant — an unavoidable consequence of combining
per-delivery retry with non-blocking dispatch. Consumers MUST NOT
assume strict arrival ordering across retry boundaries; reconstruct
order from the event envelope's `timestamp` (and `correlation_id`
chains) rather than arrival order.
* 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
- name: Evidence
description: Public retrieval of signed CyclesEvidence envelopes by content id
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
Cache-Control:
description: >-
Caching directive. For content-addressed responses (e.g. getEvidence)
the server SHOULD return `public, immutable` — the body never changes.
schema:
type: string
Retry-After:
description: >-
On 429, the non-negative delta-seconds a client SHOULD wait before
retrying (optional in v0). This API intentionally supports only the
delta-seconds form of HTTP Retry-After; the HTTP-date form is invalid
for this contract because converting it to a safe monotonic delay
would require wall-clock assumptions that lease scheduling forbids.
schema:
type: integer
format: int64
minimum: 0
X-Cycles-Tenant:
description: Effective tenant identifier derived from auth context (optional in v0)
schema:
type: string
Date:
description: >-
Standard HTTP origination timestamp (RFC 9110 §6.6.1, IMF-fixdate per
§5.6.7). Declared here — OPTIONAL — purely as documentation of standard
HTTP behavior on the reservation responses. It plays NO role in the
HEARTBEAT GUIDANCE on extendReservation and MUST NOT be used for lease
arithmetic or heartbeat scheduling: Date has whole-second resolution,
is a best-effort timestamp that may be generated at any point during
message origination or replaced by intermediaries, and a server may
derive `expires_at_ms` and Date from DIFFERENT clocks (e.g. a data
store's clock vs the HTTP container's), so `expires_at_ms − Date` is
not a same-frame measurement.
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 outcome and payload (including any server-generated identifiers such as reservation_id), except
that remaining_ttl_ms on a replayed createReservation or extendReservation success is a volatile response
observation and is not part of the attested CyclesEvidence payload. When emitted, it MUST be recomputed
from the original expires_at_ms and the current authoritative server time, never copied from the stored
response; it MUST be 0 if the reservation is no longer ACTIVE. All other fields replay verbatim.
schema:
$ref: '#/components/schemas/IdempotencyKey'
ReservationId:
name: reservation_id
in: path
required: true
schema:
type: string
minLength: 1
maxLength: 128
EvidenceId:
name: evidence_id
in: path
required: true
description: sha256 content hash of the CyclesEvidence envelope (64 lowercase hex chars).
schema:
type: string
pattern: '^[0-9a-f]{64}$'
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:
CyclesEvidenceEnvelope:
type: object
additionalProperties: true
description: >-
A signed CyclesEvidence envelope returned by `getEvidence`. The
NORMATIVE definition — field semantics and the JCS content-hash +
Ed25519 signature derivation — lives in
`cycles-evidence-v0.2.yaml`; this schema mirrors its required
shape for the serving endpoint. The envelope is content-addressed
(`evidence_id` = sha256 of the JCS-canonical bytes with `evidence_id`
and `signature` emptied) and self-verifying, so a consumer can check
it offline without trusting this server.
required:
- schema_version
- artifact_type
- server_id
- signer_did
- issued_at_ms
- payload
- evidence_id
- signature
properties:
schema_version:
type: string
const: cycles-evidence/v0.1
artifact_type:
type: string
enum: [decide, reserve, commit, release, error]
server_id:
type: string
description: Stable URI of the issuing Cycles server.
signer_did:
type: string
description: Ed25519 public key (v0.1 hex) the signature verifies against.
issued_at_ms:
type: integer
format: int64
trace_id:
type: string
payload:
type: object
description: One key matching artifact_type (decide/reserve/commit/release/error).
evidence_id:
type: string
pattern: '^[0-9a-f]{64}$'
signature:
type: string
pattern: '^[0-9a-f]{128}$'
CyclesEvidenceJwks:
type: object
additionalProperties: true
description: >-
The issuing server's signer key set, returned by `getEvidenceJwks`. An
RFC 7517 JWK Set (extra top-level members allowed and ignored). The
NORMATIVE resolution algorithm — `did:cycles` binding, the
validity-window key selection (pick the key whose
`[cycles_nbf_ms, cycles_exp_ms)` covers the envelope's `issued_at_ms`,
never "the current key"), the deterministic-selection rules, and the
verification dispositions — lives in `cycles-evidence-v0.2.yaml`;
this schema mirrors its required shape for the serving endpoint.
required: [keys]
properties:
keys:
type: array
items: {$ref: '#/components/schemas/CyclesEvidenceJwk'}
CyclesEvidenceJwk:
type: object
additionalProperties: true
description: >-
One Ed25519 signing key with a Cycles validity window. RFC 7517 /
RFC 8037 (OKP/Ed25519) JWK plus the `cycles_*` members. A retired key
MUST remain in the set so envelopes signed before a rotation still
verify. See `cycles-evidence-v0.2.yaml` for full semantics.
required: [kty, crv, x, kid, cycles_nbf_ms]
properties:
kty:
type: string
const: OKP
crv:
type: string
const: Ed25519
alg:
type: string
const: EdDSA
description: >-
Optional (RFC 7517 §4.4); if present MUST be `EdDSA`. A
present-but-different `alg` makes the JWK invalid (excluded).
x:
type: string
description: >-
base64url (unpadded) of the raw 32-byte Ed25519 public key. Equals
`base64url(hex-decode(signer_did))` for a raw-hex `signer_did`.
kid:
type: string
description: >-
Stable key id; equals the `#<kid>` fragment of a `did:cycles`
`signer_did`.
cycles_nbf_ms:
type: integer
format: int64
description: Valid-from, epoch ms, INCLUSIVE.
cycles_exp_ms:
type: [integer, "null"]
format: int64
description: Valid-until, epoch ms, EXCLUSIVE. Absent/null ⇒ active (open-ended).
status:
type: string
enum: [active, retired]
description: >-
Advisory only. Selection is by validity WINDOW, never by `status`.
CyclesEvidenceRef:
type: object
additionalProperties: false
description: >-
Reference to the CyclesEvidence envelope emitted for an operation. The
`evidence_id` is the sha256 content hash of the JCS-canonical envelope
and is computed SYNCHRONOUSLY at decision time, so it is returned on the
operation response even though the envelope is Ed25519-signed and stored
ASYNCHRONOUSLY. A caller (e.g. an APS gateway) binds its own signed
receipt to this evidence by recording `evidence_id`, then fetches the
envelope at `cycles_evidence_url` (see `getEvidence`) to verify it
offline. Because signing/storage is async, the envelope MAY be served
shortly after this response — consumers SHOULD treat a transient `404`
from `getEvidence` as not-yet-available and retry.
TRANSPORT METADATA, NOT ATTESTED: `cycles_evidence` is added to the
response for the caller's convenience and is NOT part of the evidence
the envelope attests. `remaining_ttl_ms` on ReservationCreateResponse
is likewise a volatile transport observation rather than attested
decision data, so a same-key replay can safely recompute it. The
`evidence_id` is computed over the operation response WITHOUT either
transport-only field — the envelope's `payload.<artifact>.response`
mirror (`cycles-evidence-v0.2.yaml`) keeps `additionalProperties:
false` and omits them. Implementations MUST compute `evidence_id`
before stamping transport-only fields onto the response, and MUST NOT
include either field in the attested payload.
required: [evidence_id, cycles_evidence_url]
properties:
evidence_id:
type: string
pattern: '^[0-9a-f]{64}$'
description: sha256 content id of the emitted envelope; resolve via getEvidence.
cycles_evidence_url:
type: string
format: uri
description: >-
Absolute URL of the signed envelope, formed as
`{server_id}/evidence/{evidence_id}`. Note `server_id` is already the
canonical deployment base INCLUDING the `/v1` prefix (e.g.
`https://cycles.example.com/v1`), so the join adds only
`/evidence/{evidence_id}` — it MUST NOT re-add `/v1`.
ReservationEvidence:
type: object
additionalProperties: false
description: >-
Map of artifact type to the CyclesEvidence reference emitted for that
operation on a reservation. Lets a consumer jump from a reservation
straight to its signed envelope(s) via `getEvidence` without having
captured the `evidence_id` off the original reserve / commit / release
response. Keys are the reservation-applicable artifact types: a
reservation has at most a `reserve` entry plus one terminal entry
(`commit` XOR `release`). TRANSPORT METADATA, NOT ATTESTED (see
`CyclesEvidenceRef`) — each entry is recorded after its artifact's
`evidence_id` was computed and is not part of any attested payload.
properties:
reserve:
$ref: '#/components/schemas/CyclesEvidenceRef'
description: Evidence emitted for the reserve operation, if any.
commit:
$ref: '#/components/schemas/CyclesEvidenceRef'
description: Evidence emitted for the commit operation (COMMITTED reservations).
release:
$ref: '#/components/schemas/CyclesEvidenceRef'
description: Evidence emitted for the release operation (RELEASED reservations).
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
- LIMIT_EXCEEDED
- TENANT_CLOSED
- 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
cycles_evidence:
$ref: '#/components/schemas/CyclesEvidenceRef'
description: >-
Reference to the CyclesEvidence envelope emitted for this error
(artifact_type `error`). Present when the server emitted an `error`
CyclesEvidence record for this response — most importantly the
non-dry reserve denials that surface as HTTP 409 `BUDGET_EXCEEDED`
(and the other live denial codes), which are the highest-signal
evidence an APS receipt can bind to. A denied caller reads
`evidence_id` to bind its own signed receipt to this denial, then
fetches the envelope at `cycles_evidence_url` (see `getEvidence`).
Absent when evidence emission is disabled on the server, or for
errors raised before evidence could be emitted (e.g. request
validation / auth failures). TRANSPORT METADATA, NOT ATTESTED — as
with the success responses, `evidence_id` is computed over the
`ErrorResponse` body WITHOUT this field (the `error` artifact's
`payload.error.response` mirror omits it); see `CyclesEvidenceRef`.
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).
- TENANT_CLOSED — the owning tenant's status is CLOSED (deployments with a
governance plane; added in revision 2026-07-10). Populated on /decide and
dry_run=true evaluations; the persisting mutation surface reports the same
condition as HTTP 409 error=TENANT_CLOSED instead (see the closed-tenant
binding in ERROR SEMANTICS).
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.
CHARSET (NORMATIVE, revision 2026-07-03):
- Standard-field values (tenant/workspace/app/workflow/agent/toolset)
SHOULD match ^[a-zA-Z0-9_.-]+$. Canonical scope identifiers and scope
paths use ":" and "/" as structural delimiters, so delimiter,
whitespace, and control characters have no stable canonical encoding.
- Servers MAY reject standard-field values outside this pattern with
400 INVALID_REQUEST (the reference implementation does). Clients
MUST NOT rely on out-of-pattern values being accepted, and portable
clients SHOULD restrict themselves to the pattern above.
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: