feat(drafts): CyclesEvidence envelope v0.1 — signed, content-addressed lifecycle evidence#90
Conversation
1d89620 to
f622870
Compare
…d lifecycle evidence New schema-only OpenAPI 3.1 draft at drafts/cycles-evidence-v0.1.yaml defining a JCS-canonicalized, Ed25519-signed envelope for the four Cycles authorization lifecycle events (decide / reserve / commit / release). Content-addressed via sha256 over canonical bytes with signature emptied; signature derived over canonical bytes with evidence_id populated and signature emptied (id-then-signature ordering matches APS payment-rails/canonicalize.ts and Wave 1 accountability artifacts). Motivation: - Cycles runtime responses are sufficient for the immediate caller but not for ledger-independent audit consumers — cross-system verifiers (notably APS) need a single artifact they can fetch by content hash and verify offline. - Hoists reservation_id into the signed payload for commit/release artifacts, closing the linkage gap called out at aeoess/agent-governance-vocabulary#92 review round 4 (the wire response bodies do not echo reservation_id; it lives on the URL path). - Provides the canonicalization commitment promised at aeoess/agent-passport-system#25 (CyclesEvidenceRef join interface): RFC 8785 JCS + sha256 to match APS conventions. Status: DRAFT (v0.1) in drafts/. Will move to a normative repo-root spec (cycles-evidence-v0.2.yaml) once at least one production implementation ships and APS has integrated against the envelope shape end-to-end. Out of scope for v0.1: - Evidence retrieval HTTP API (URL path layout, auth scheme, replication policy stays with the server impl). - Signing-key rotation, JWKS publication, did:cycles registration. - Merkle-batched aggregated evidence (one envelope per event in v0.1). Schema mirrors of cycles-protocol-v0.yaml types (DecisionRequest, ReservationCreateRequest, etc.) are inlined for standalone validation; the normative spec move will replace them with cross-file refs. Spectral lint passes with 0 errors (3 warnings, all unavoidable for a schema-only draft: oas3-api-servers, oas3-unused-component on CyclesEvidence root, oneOf-incompatible additionalProperties on EvidencePayload). References: - aeoess/agent-passport-system#25 (CyclesEvidenceRef join interface) - aeoess/agent-governance-vocabulary#92 (Cycles signal-type crosswalk) - RFC 8785 (JCS)
f622870 to
cf31d6b
Compare
…fier
Ten signed, content-addressed CyclesEvidence envelopes covering the full
v0.1 surface, plus the deterministic generator and standalone verifier
that produced and verify them.
Surface coverage:
- 4 artifact_types: decide, reserve, commit, release
- 3 decision branches: ALLOW, ALLOW_WITH_CAPS, DENY
- All 4 Unit enum values: USD_MICROCENTS, TOKENS, RISK_POINTS, CREDITS
- Optional fields exercised: Action.tags, Balance.allocated,
ReleaseRequest.reason
- trace_id omission semantics (field absent in canonical bytes, NOT
empty string and NOT null — the distinction called out in the spec
normative note on omit/null/empty)
Generator + verifier:
- generate.py is deterministic: re-running produces byte-identical
output. Test signer derived from
sha256("cycles-evidence-v0.1-fixture-signer") so reviewers can
re-derive the Ed25519 keypair locally (pubkey:
ec52b49b81eb29ef6f62947cade245c715bf943b7ef2a5f2789288574466fc43).
Implements the v0.1 normative empty-string-sentinel +
id-then-signature algorithm.
- verify.py enforces every spec MUST clause: schema_version must be
understood (rejects unknown versions before further checks),
evidence_id sha256 byte-match against recomputed canonical bytes,
Ed25519 signature verification, artifact_type ↔ payload key
consistency (oneOf), and 32-hex trace_id pattern when present.
- Tamper-detection sanity: flipping one character in any signed field
trips both the evidence_id mismatch and the signature failure;
re-running generate.py restores from canonical sources.
Closes the test-plan checkbox on PR #90:
> "Implement a worked example: emit one envelope per artifact type
> from a reference Cycles server, compute evidence_id, sign, then
> re-verify; commit fixtures alongside the spec."
Test plan:
- cd drafts/fixtures/cycles-evidence-v0.1 && pip install -r
requirements.txt && python generate.py — writes cases/*.json with
byte-identical output across runs.
- python verify.py — 10/10 fixtures verify green; exit code 0.
|
Pushed
What landed (under
Reproduce locally: cd drafts/fixtures/cycles-evidence-v0.1
pip install -r requirements.txt
python generate.py # writes cases/*.json (byte-stable across runs)
python verify.py # 10/10 fixtures verify; exit 0Tamper sanity (covered in the README): flipping one character in any signed field trips both the One closure on a review comment: while auditing the spec against the generator I noticed the
|
…, namespace, mirror constraints) Three review findings on PR #90 surfaced real bugs. All addressed. 1. HIGH — live reserve denials had no valid evidence slot. cycles-protocol-v0.yaml §ReservationCreateResponse.decision is explicit: "For dry_run=true, decision MAY be DENY. For dry_run=false, insufficient budget MUST be expressed via 409 BUDGET_EXCEEDED (not decision=DENY)." The previous draft treated reserve `decision: DENY` as universally valid evidence, and fixture 03 emitted a non-dry reserve with decision: DENY — a wire shape the canonical protocol forbids. The most important denial path on the issue #25 integration thread ("Cycles denies → APS blocks/audits") had no evidence shape at all. Changes: - Extend ArtifactType with `error` (5th type). - Add ErrorPayload + ErrorResponseMirror schemas. ErrorPayload wraps a 4xx/5xx ErrorResponse from any of the four endpoints, hoisting `reservation_id` (when path-arg-bearing) into the signed payload and preserving the originating request body via a discriminated oneOf over the four request mirrors. - Tighten ReservePayload description with the normative dry_run + decision: DENY constraint, AND encode it schema-side as an if/then: validators reject non-dry DENY without custom logic. Confirmed: 11/11 fixtures pass, and a constructed non-dry-DENY envelope fails validation at the EvidencePayload oneOf. - Generator: rename case_03 → case_03_reserve_dry_run_deny with explicit dry_run: true; add case_11_reserve_live_budget_exceeded emitting a 409 BUDGET_EXCEEDED via the `error` artifact type. Stale 03-reserve-deny.json removed automatically by the generator (cleanup step added). 2. MEDIUM — stale APS literal namespace. ReleasePayload description still referenced `rail.budget_authority. release.v1` from the abandoned namespace, with the "open question" framing left over from a thread state that no longer holds. Issue #25 comments 4433715146 (aeoess) and 4433275511 (me) renamed the APS rail-literal namespace from `budget_authority` to `budget_reservation` and locked in three lifecycle literals: `rail.budget_reservation.{permit,release,denial}.v1`. Description rewritten to cite the rename and the confirmed literal without the open-question framing. 3. MEDIUM — Mirror schemas looser than the canonical source. The previous mirrors stripped constraints from the canonical: - Subject lost minProperties: 1, the anyOf for the six standard fields, maxLength: 128 on string fields, maxProperties: 16 on dimensions, and maxLength: 256 on dimensions values. - idempotency_key lost minLength: 1, maxLength: 256 on all four *RequestMirror schemas. - reason_code on DecisionResponseMirror and ReservationCreateResponseMirror lost maxLength: 128 (the canonical DecisionReasonCode bound). - retry_after_ms, ttl_ms, grace_period_ms, expires_at_ms lost minimum: 0. - Action.kind lost maxLength: 64; Action.name lost maxLength: 256; Action.tags lost maxItems: 10 and items maxLength: 64. - Caps.tool_allowlist/tool_denylist lost items maxLength: 256. - Amount.amount and SignedAmount.amount lost format: int64. This was a documentation gap — fixtures happened to satisfy the canonical constraints — but it misled adapter authors reading the draft about what's permitted on the wire. Changes: - All canonical constraints copied into the mirrors byte-for-byte against cycles-protocol-v0.yaml as of 2026-05-13. - New MIRROR CONTRACT block at the head of the mirrors section: drift from canonical is now an explicit v0.1 bug. Coverage after this commit: - 11 signed fixtures (was 10), exit 0 on python verify.py. - All 11 schema-valid under the tightened spec (Draft 2020-12). - The if/then ReservePayload rule rejects non-dry DENY at the JSON Schema layer. - npx spectral lint drafts/cycles-evidence-v0.1.yaml --fail-severity=error: 0 errors, 3 expected schema-only warnings (oas3-api-servers / oas3-unused-component / oneOf additionalProperties — same set as before this review pass). Test plan: - cd drafts/fixtures/cycles-evidence-v0.1 && python generate.py && python verify.py — 11/11 verified. - JSON Schema validate every fixture against the updated CyclesEvidence schema — 11/11 valid. - Construct a non-dry reserve with decision: DENY (stripped dry_run: true from fixture 03 in-memory) — schema rejects it. - npx spectral lint — 0 errors.
|
Review pass on Findings & resolutions1 — HIGH: live reserve denials had no valid evidence slot.
2 — MEDIUM: stale APS literal namespace. 3 — MEDIUM: Mirror schemas dropped canonical constraints. Subject lost Validationcd drafts/fixtures/cycles-evidence-v0.1
python generate.py # 11 fixtures + stale 03-reserve-deny.json auto-removed
python verify.py # 11/11 verified, exit 0
Fixture coverage now
Thanks for the thorough review — finding 1 in particular would have shipped a wire shape the canonical protocol forbids if it had merged as-is. |
…iscriminated error.request, mirror tightening, CREDITS prose) Three review findings on PR #90 round 2. All addressed, all empirically verified before commit. 1. MEDIUM — `error.request` oneOf rejected valid minimal requests. The four request mirrors share identical required-field sets (idempotency_key+subject+action+estimate for decide/reserve; idempotency_key for commit/release). A minimal `POST /v1/reservations` body therefore ambiguously matched both `DecisionRequestMirror` and `ReservationCreateRequestMirror` under JSON Schema `oneOf`, failing the "exactly one" rule. Empirically reproduced: stripping `ttl_ms` from fixture 11's request caused schema validation to fail under the previous `oneOf`. Fix: replace `oneOf` with an endpoint-discriminated `allOf` of `if`/ `then` at the `ErrorPayload` level. Each branch fires only when `endpoint` matches its `const`, routing `request` to the matching mirror. Validates as expected: - positive: minimal reserve request via error path → ACCEPTED - negative: commit-body shape under endpoint=POST /v1/reservations → REJECTED (the `endpoint`-mirror mismatch is caught by the correct branch) 2. MEDIUM — remaining mirror constraints still not fully copied. Reviewer caught five more divergences from cycles-protocol-v0.yaml that the round-1 mirror tightening missed: - `ttl_ms`: canonical is `minimum: 1000, maximum: 86400000` (cycles-protocol-v0.yaml:828-830). Mirror had `minimum: 0`, no max. - `grace_period_ms`: canonical is `minimum: 0, maximum: 60000` (cycles-protocol-v0.yaml:832-836). Mirror had no max. - `overage_policy`: canonical is `CommitOveragePolicy` enum `[REJECT, ALLOW_IF_AVAILABLE, ALLOW_WITH_OVERDRAFT]` (cycles-protocol-v0.yaml:765-794). Mirror had plain `type: string`. - `CommitResponseMirror.status`: canonical is enum `[COMMITTED]` (cycles-protocol-v0.yaml:1066-1068). Mirror had plain string. - `ReleaseResponseMirror.status`: canonical is enum `[RELEASED]` (cycles-protocol-v0.yaml:1099-1101). Mirror had plain string. - `ReleaseRequestMirror.reason`: canonical is `maxLength: 256` (cycles-protocol-v0.yaml:1091-1092). Mirror had no maxLength. All copied into the mirrors. Verified by negative tests at the schema layer: - ttl_ms=500 (below min) → REJECTED - ttl_ms=90000000 (above max) → REJECTED - overage_policy='NOT_A_REAL_POLICY' → REJECTED - commit response status='pending' → REJECTED 3. LOW — CREDITS unit-class prose in generator could mislead adapter authors. The previous comment on `case_10_reserve_credits_allow` said "some deployments treat them as consumption, others as authority". Even though that reflects an open position in aeoess/agent-passport-system #25 (comment 4421702699), it can read as a positive claim about APS unit_class mapping when the fixture is only exercising Cycles wire semantics. Rewritten to scope the comment strictly to the Cycles wire surface (CREDITS is a closed-enum Unit name preserved byte-for-byte) and defer the APS unit_class question explicitly to issue #25 and crosswalk/cycles.yaml (aeoess/agent-governance-vocabulary#92). Byte stability: no fixture canonical bytes change in this commit (only the spec and one generator comment edited). `evidence_id` and `signature` on all 11 fixtures are byte-identical to the prior commit. Validation summary: - python verify.py: 11/11 verified - JSON Schema validate all 11 fixtures against the tightened spec: 11/11 valid - F1 positive: minimal reserve request via error path now validates - F1 negative: commit-body under reserve endpoint correctly rejected - F2 negatives: out-of-range ttl_ms (both directions), bad overage_policy enum, and bad commit status all correctly rejected - Prior regression: non-dry reserve with decision: DENY still rejected by the if/then on ReservePayload - npx spectral lint drafts/cycles-evidence-v0.1.yaml --fail-severity=error: 0 errors, 3 expected schema-only warnings (unchanged set from prior commits)
|
Review round 2 — all three findings confirmed against canonical sources and addressed in 1 — MEDIUM:
|
| Field | Was | Now (per canonical line cited) |
|---|---|---|
ttl_ms |
minimum: 0, no max |
minimum: 1000, maximum: 86400000 (L828-830) |
grace_period_ms |
minimum: 0, no max |
minimum: 0, maximum: 60000 (L832-836) |
overage_policy |
type: string |
enum [REJECT, ALLOW_IF_AVAILABLE, ALLOW_WITH_OVERDRAFT] (L794) |
CommitResponseMirror.status |
type: string |
enum [COMMITTED] (L1068) |
ReleaseResponseMirror.status |
type: string |
enum [RELEASED] (L1101) |
ReleaseRequestMirror.reason |
type: string, no max |
maxLength: 256 (L1092) |
Verified at the schema layer:
ttl_ms=500→ rejected (below canonical min 1000)ttl_ms=90000000→ rejected (above canonical max 86400000)overage_policy='NOT_A_REAL_POLICY'→ rejected- commit response
status='pending'→ rejected
3 — LOW: CREDITS unit-class prose
Comment on case_10_reserve_credits_allow rewritten to scope strictly to the Cycles wire surface (CREDITS as closed-enum unit name preserved byte-for-byte) and defer the APS unit_class question explicitly to issue #25 and crosswalk/cycles.yaml (aeoess/agent-governance-vocabulary#92). No positive claim about APS unit_class mapping.
Byte stability
No fixture canonical bytes change in this commit (only the spec yaml and one generator comment edited). All 11 evidence_ids and signatures are byte-identical to the prior commit 17271c4. python generate.py shows zero diff on cases/*.json.
Validation
python verify.py: 11/11 verified- JSON Schema validation of all 11 fixtures against the tightened spec: 11/11 valid
- Six negative tests above all reject correctly
- Prior regression — non-dry reserve with
decision: DENY— still rejected by theif/thenonReservePayload npx spectral lint drafts/cycles-evidence-v0.1.yaml --fail-severity=error: 0 errors, 3 expected warnings (unchanged set)
(Branch needed a rebase before push — remote had picked up cd66835 merging main in for unrelated hygiene work. Rebase was clean, no conflicts since main's diff didn't touch CyclesEvidence files.)
…ype discriminator, commit/release reservation_id requirement, stale prose) Three review findings on PR #90 round 3. All addressed, all empirically verified before commit. Same rigor protocol as round 2 (negative tests for every new constraint). 1. MEDIUM — schema-only validators accepted artifact_type / payload key mismatches. The previous `EvidencePayload.oneOf` enforced that `payload` had exactly one of {decide, reserve, commit, release, error}, but did NOT tie that key to `artifact_type`. So an envelope with `artifact_type: reserve` and `payload.error` validated at the schema layer. The custom `verify.py` catches this, but any consumer using JSON Schema alone (an APS verifier, an offline auditor, a generic gateway) would silently accept the impossible envelope. Empirically reproduced: fixture 02 with payload swapped to fixture 11's `payload.error` → previous schema accepted it. Fix: top-level `allOf` of five `if`/`then` rules on `CyclesEvidence`, one per artifact_type, requiring `payload` to contain the matching key. The pairing now lives in the schema, not just in the custom verifier. Verified via cross-product: all 5 × 5 = 25 (artifact_type X, payload Y) combinations checked; 5 matching pairs accepted, 20 mismatch pairs rejected. 2. MEDIUM — `ErrorPayload.reservation_id` was optional even for commit/release endpoints, breaking authorization chain linkage. The `reservation_id` is the URL path argument of `POST /v1/reservations/{reservation_id}/{commit,release}`. For successful evidence (`CommitPayload`, `ReleasePayload`), `reservation_id` is REQUIRED — that's the whole point of the hoist discussed in the #92 round-4 review. But `ErrorPayload` left it optional unconditionally, so a 4xx/5xx error on those same paths could ship without the linkage. Empirically reproduced: error envelope with `endpoint: POST /v1/reservations/{reservation_id}/commit` and a valid `CommitRequestMirror`-shaped body but no `reservation_id` → previous schema accepted it. Fix: add a 5th branch to `ErrorPayload.allOf` requiring `reservation_id` when `endpoint` is the commit or release path. Endpoints with no path-arg reservation_id (`POST /v1/decisions`, `POST /v1/reservations`) are unaffected. Verified: commit/release endpoint without reservation_id → REJECTED; commit endpoint WITH reservation_id → VALID; non-commit/release endpoints without reservation_id remain VALID. Prose: ErrorPayload description updated to call out the conditional "required when endpoint is commit/release" rule, citing the same chain-completeness rationale as CommitPayload/ReleasePayload. 3. LOW — stale prose said "four properties" / "other three" but `error` is now a 5th branch. `EvidencePayload.description` updated to "five properties" / "other four", with a new sentence noting that the artifact_type ↔ payload-key pairing is enforced at the top-level CyclesEvidence schema via `allOf`/`if`/`then` (so schema-only validators reject mismatches without needing the custom verifier). Byte stability: no fixture canonical bytes change in this commit (only spec yaml edited; generator untouched). All 11 evidence_ids and signatures are byte-identical to the prior commit. Validation summary: - python verify.py: 11/11 verified - JSON Schema validate all 11 fixtures against the tightened spec: 11/11 valid - F1 cross-product matrix: 20/20 artifact_type/payload mismatches rejected (5 matching pairs remain valid) - F2 negatives: commit-endpoint and release-endpoint ErrorPayload without reservation_id both REJECTED - F2 positive: commit-endpoint ErrorPayload WITH reservation_id VALID - All prior regression negatives still trip (non-dry DENY, commit-body under reserve endpoint, ttl_ms below canonical min) - npx spectral lint drafts/cycles-evidence-v0.1.yaml --fail-severity=error: 0 errors, 3 expected warnings (unchanged set)
|
Review round 3 — all three findings confirmed empirically before fix, addressed in 1 — MEDIUM: schema-only validators accepted
|
… sweep of "MUST be absent / Present only when" rules
One reviewer finding + six proactive sweep findings on the same
mirror-drift class. Same rigor protocol as rounds 2-3: empirically
reproduce, fix, paired negative tests for every constraint.
The reviewer flagged that the round-3 fix only enforced the "MUST be
present" side of the commit/release reservation_id rule, not the
"MUST be absent" side for non-path endpoints. While verifying that
finding I grepped the canonical for "MUST be absent" and found six
more constraints in the same class (caps-vs-decision and
dry_run-vs-reservation_id rules from canonical L752 / L981 / L997
/ L1404), all of which the mirrors were silently dropping. All seven
rejected invalid envelopes empirically before this commit; all seven
now reject them.
REVIEWER-FLAGGED (1)
A. ErrorPayload.reservation_id MUST be absent when endpoint is
`POST /v1/decisions` or `POST /v1/reservations` (those endpoints
take no reservation_id path argument). Previous schema enforced
the commit/release REQUIRED side but not this ABSENT side, so a
stray reservation_id on a decisions/reservations error
validated and would mislead audit consumers.
PROACTIVE SWEEP (6)
Six more "MUST be absent" / "Present only when" rules from
cycles-protocol-v0.yaml that the mirrors weren't enforcing. Same
class of mirror drift the reviewer caught in rounds 1, 2, and 4 —
doing the broader pass here breaks the find-more-each-round pattern.
B. DecisionResponseMirror: `caps` MUST be present when
decision=ALLOW_WITH_CAPS, MUST be absent otherwise (canonical
L752). Encoded as mirror-level if/then/else.
C. ReservationCreateResponseMirror: same caps rule (canonical
L997). Same mirror-level if/then/else.
D. ReservePayload: dry_run=true → reservation_id MUST be absent
on response (canonical L981, L1404). Added to ReservePayload
allOf (rule lives there because it spans request and response).
E. ReservePayload: dry_run=true → expires_at_ms MUST be absent
on response (canonical L1404). Combined with D into a single
conditional branch.
F. ReservePayload: dry_run NOT true (false or absent) →
reservation_id MUST be present on response (canonical L981).
The inverse of D. Combined with the round-1 rule forbidding
non-dry DENY, this means decision is guaranteed ALLOW or
ALLOW_WITH_CAPS in the non-dry branch — both of which require
reservation_id per canonical.
G. ReservePayload: same expires_at_ms required side. Combined
with F into a single conditional branch.
Validation summary:
- 11/11 fixtures still valid (positive)
- 10 paired negative tests covering A through G, all REJECT:
* reservations endpoint + stray reservation_id
* decisions endpoint + stray reservation_id
* decide ALLOW + spurious caps
* decide ALLOW_WITH_CAPS + missing caps
* reserve ALLOW + spurious caps
* reserve ALLOW_WITH_CAPS + missing caps
* dry_run=true + reservation_id present
* dry_run=true + expires_at_ms present
* non-dry ALLOW + missing reservation_id
* non-dry ALLOW + missing expires_at_ms
- All prior-round regressions still trip:
* artifact_type / payload mismatch (round 3)
* commit endpoint + no reservation_id (round 3)
* non-dry DENY (round 1)
* ttl_ms out-of-range (round 2)
- npx spectral lint drafts/cycles-evidence-v0.1.yaml
--fail-severity=error: 0 errors, 3 expected schema-only warnings
(unchanged set)
Byte stability: no fixture canonical bytes change in this commit
(only spec yaml edited; generator untouched). All 11 evidence_ids
and signatures are byte-identical to `3d8b3c8`.
|
Review round 4 — reviewer flagged 1 finding (rule A below). Proactively swept the canonical for sibling "MUST be absent / Present only when" rules and found 6 more in the same class. All 7 fixed in REVIEWER-FLAGGED (1)A — Fix added as a 6th branch to - if:
properties:
endpoint:
enum:
- "POST /v1/decisions"
- "POST /v1/reservations"
required: [endpoint]
then:
not:
required: [reservation_id]PROACTIVE SWEEP (6)
B and C are mirror-level Validation
Byte stabilityNo fixture canonical bytes change in this commit (only spec yaml edited; generator untouched). All 11 |
…ame typo, ErrorCode enum closure, expires_at_ms over-tightening)
Three findings, all empirically verified before fix.
1. HIGH — endpoint name typo: /v1/decisions → /v1/decide.
The canonical Cycles spec defines the decide endpoint at
`/v1/decide` (cycles-protocol-v0.yaml:1342). The draft used
`/v1/decisions` everywhere — in the ArtifactType→endpoint mapping
prose, the ErrorPayload endpoint enum, the four endpoint-discriminated
allOf branches (request-mirror routing + reservation_id absent rule),
and the DecisionRequestMirror / DecisionResponseMirror descriptions.
12 occurrences renamed. No fixture caught this earlier because no
existing fixture exercised an error envelope on the decide endpoint;
fixture 12 (added in this commit) does, closing the coverage gap.
Verified: a synthetic POST /v1/decide error envelope now validates;
the old POST /v1/decisions value is now rejected as off-enum.
2. MEDIUM — ErrorResponseMirror.error open string, contradicting the
mirror contract.
The MIRROR CONTRACT block at the top of the mirrors section is
explicit: "Mirror schemas copy field names, types, required-lists,
enum values, AND structural constraints." The previous
ErrorResponseMirror.error was `type: string` with a description
listing the 15 canonical ErrorCode values, plus loose prose
suggesting consumers "treat unknown codes as terminal error states"
— directly contradicting the contract.
Tightened to the 15-value closed enum copied byte-for-byte from
canonical L429-L446. Future ErrorCode additions in v0 minor versions
trigger a re-cut of this mirror, which is the normal mirror-drift
process documented in the MIRROR CONTRACT.
Verified: error="FUTURE_CODE" rejected; error="invalid_request"
(lowercase) rejected.
3. MEDIUM/clarify — Round-4 rule G over-tightened by requiring
expires_at_ms on non-dry responses.
Canonical L981 explicitly says reservation_id is "Present if
decision is ALLOW or ALLOW_WITH_CAPS and dry_run is false" (a
positive presence rule). For expires_at_ms, the canonical L1404
only requires its ABSENCE on dry_run; the canonical schema's
required list never includes it, and there is no positive-presence
prose for non-dry. The round-4 rule G required expires_at_ms on
non-dry, which rejected envelopes the canonical schema accepts —
over-tightening.
Relaxed rule 3 on ReservePayload.allOf to require only
reservation_id (not expires_at_ms) in the non-dry branch. The
dry-run absent rule (which DOES cover expires_at_ms per L1404)
is unchanged. Updated prose explains the asymmetry.
Verified:
- F3 POS: non-dry ALLOW without expires_at_ms now validates
- F3 REG: non-dry ALLOW without reservation_id still rejected
- F3 REG: dry_run=true + expires_at_ms present still rejected
ADDED FIXTURE 12 — error on /v1/decide endpoint.
The first 11 fixtures had no decide-error coverage, which is exactly
why F1 went undetected for five review rounds. Fixture 12 emits a
403 FORBIDDEN on POST /v1/decide with a DecisionRequest body. This
proves the corrected endpoint name is consistent with the
endpoint-discriminated request validation end-to-end.
Validation summary:
- 12/12 fixtures verify
- 12/12 fixtures validate against the tightened schema
- F1: POST /v1/decide validates, POST /v1/decisions rejected
- F2: unknown ErrorCode rejected (FUTURE_CODE and lowercase
invalid_request both fail)
- F3 positive: non-dry ALLOW without expires_at_ms validates
- F3 regression: dry_run=true + expires_at_ms still rejected;
non-dry ALLOW without reservation_id still rejected
- Prior-round regressions still trip: stray reservation_id on
decide/reservations endpoints; ALLOW_WITH_CAPS without caps;
artifact_type/payload mismatch; non-dry DENY
- npx spectral lint drafts/cycles-evidence-v0.1.yaml
--fail-severity=error: 0 errors, 3 expected schema-only warnings
(unchanged set)
Byte stability: fixtures 1-11 are byte-identical to dba1fe6.
Fixture 12 is new. Generator is deterministic and reproducible from
the same test signer seed.
|
Review round 5 — three findings, all confirmed against the canonical, fixed in 1 — HIGH:
|
…quest.metrics (round 6)
Reviewer caught one finding: `CommitRequestMirror.metrics` was an
arbitrary object (`type: object, additionalProperties: true`) but the
canonical `CommitRequest.metrics` references the constrained
`StandardMetrics` schema (cycles-protocol-v0.yaml L1055-L1056 →
L1008-L1034 with additionalProperties: false and 5 specific fields).
Empirically reproduced: `metrics: { "bogus_metric": -1 }` validated
under the previous schema while the canonical rejects it.
Per the rigor protocol, also ran a sweep for sibling `additionalProperties:
true` slots on the off-chance there were more. Five total in mirrors —
`DecisionRequestMirror.metadata`, `ReservationCreateRequestMirror.metadata`,
`CommitRequestMirror.metrics`, `CommitRequestMirror.metadata`, and
`ErrorResponseMirror.details` — but only `metrics` is divergent from
canonical. The four `metadata`/`details` slots are genuinely
`additionalProperties: true` in the canonical (lines 480, 729, 860,
1057). No additional drift found.
Fix:
- Added `StandardMetrics` schema under "Common nested types"
section. Copies the canonical exactly: 5 fields
(tokens_input/tokens_output/latency_ms with `minimum: 0`,
model_version with `maxLength: 128`, custom as the
`additionalProperties: true` escape hatch) plus the top-level
`additionalProperties: false`.
- Updated `CommitRequestMirror.metrics` from
`{type: object, additionalProperties: true}` to
`$ref: "#/components/schemas/StandardMetrics"`.
Coverage gap closed with fixture 13:
- The reason no prior fixture exercised this constraint is that
fixture 05 (the only commit fixture before this round) didn't
populate `metrics`. Same coverage gap pattern as fixture 12
(round 5, `/v1/decide` typo).
- Fixture 13 (`13-commit-with-metrics.json`) is a commit envelope
with all five StandardMetrics fields populated, including the
`custom` escape hatch carrying deployment-specific keys
(`cache_hit_ratio`, `retry_count`).
Validation:
- 13/13 fixtures verify (python verify.py)
- 13/13 fixtures validate against the tightened schema
- F NEG: bogus top-level metric key `{ "bogus_metric": -1 }` →
rejected (reviewer's exact case)
- F NEG: `tokens_input: -1` → rejected
- F NEG: `model_version` 129 chars → rejected
- F NEG: `latency_ms: -100` → rejected
- F POS: `custom` escape hatch carrying arbitrary nested content
→ valid (confirms the canonical escape hatch works)
- Prior-round regressions still trip: error=FUTURE_CODE,
/v1/decisions stale endpoint, non-dry DENY, ALLOW_WITH_CAPS
without caps
- npx spectral lint: 0 errors, 3 expected schema-only warnings
(unchanged set)
Byte stability: fixtures 1-12 byte-identical to b5ecb44. Fixture 13
is new.
|
Review round 6 — finding confirmed against canonical, fixed in Finding (MEDIUM) —
|
| Slot | Canonical line | Verdict |
|---|---|---|
DecisionRequestMirror.metadata |
L729 | genuinely loose in canonical ✓ |
ReservationCreateRequestMirror.metadata |
L860 | genuinely loose in canonical ✓ |
CommitRequestMirror.metrics |
L1055 | divergent — fixed |
CommitRequestMirror.metadata |
L1057 | genuinely loose in canonical ✓ |
ErrorResponseMirror.details |
L480 | genuinely loose in canonical ✓ |
Only metrics was divergent; the four metadata/details slots are genuinely additionalProperties: true in the canonical. No additional drift.
Coverage gap closed — fixture 13
The reason no prior fixture caught this: fixture 05 (only commit fixture before round 6) didn't populate metrics. Same coverage-gap pattern as fixture 12 in round 5 (/v1/decide endpoint). Added 13-commit-with-metrics.json — a commit envelope with all five StandardMetrics fields populated, including the custom escape hatch carrying cache_hit_ratio and retry_count.
Validation
- 13/13 fixtures verify and schema-validate
- F NEG: reviewer's exact case (
{ "bogus_metric": -1 }) → rejected - F NEG:
tokens_input: -1→ rejected - F NEG:
model_versionat 129 chars → rejected - F NEG:
latency_ms: -100→ rejected - F POS:
customescape hatch with arbitrary nested content → valid (canonical escape hatch works as designed) - Prior regressions still trip:
error=FUTURE_CODE; stale/v1/decisions; non-dry DENY; ALLOW_WITH_CAPS without caps npx spectral lint: 0 errors, 3 expected schema-only warnings
Byte stability
Fixtures 1-12 byte-identical to b5ecb44. Fixture 13 is new.
Self-note
The two recent rounds (5 + 6) both surfaced bugs whose root cause is fixture coverage gaps, not schema design. Pattern: when adding a new constraint or enum value, I should immediately add a fixture exercising it. Already encoded as rule 5 in my review-rigor memory; this round added rule 6 (cross-check literal strings against canonical via grep). The coverage rule continues to pay back — round 6's bug would have been invisible without fixture 13.
… fixture (round 7) README line called StandardMetrics a "closed-enum mirror". It is a constrained object schema with additionalProperties: false and 5 typed fields plus a `custom` escape hatch — not an enum. Reviewer flagged the documentation inconsistency. Updated to "constrained StandardMetrics mirror (object schema with `additionalProperties: false` and per-field constraints; not an enum)" — eliminates the imprecise wording and makes the actual shape explicit for adapter authors reading the fixture coverage table. Swept for sibling occurrences of "closed-enum": only one other use in `generate.py:332` describes the canonical UnitEnum (USD_MICROCENTS, TOKENS, CREDITS, RISK_POINTS) — that IS a closed enum, so the wording is correct there. Documentation-only. No schema, generator, fixture-byte, or verification logic changed. All 13 fixtures byte-identical to 66ef936.
|
Round 7 — single doc fix in Swept for sibling occurrences of "closed-enum": Documentation-only. All 13 fixtures byte-identical to Also: thanks for the all-clear on behavioral/spec compliance ("No remaining behavioral/spec-compliance issues found"). After seven review rounds — six finding rounds and this doc round — the spec, mirrors, fixtures, and verifier are clean against Ready for whatever's next on the path to merge. |
New draft at `drafts/cycles-aps-denial-mapping-v0.1.md` specifying the contract for `mapCyclesDenialToFoundation()` — the function the APS-side Cycles adapter at `src/v2/payment-rails/cycles/index.ts` (in `aeoess/agent-passport-system`) will implement to translate Cycles denial signals into APS `PaymentDenial` Tier-1 `denial_reason` values. Sister artifact to `#90` (CyclesEvidence v0.1). #90 defines how Cycles denials get signed and content-addressed; this draft defines how those denials are projected into the APS `PaymentDenial`'s closed 6-value Tier-1 enum, with the richer Cycles-specific detail preserved Tier-2 in the `cycles.denial_detail` namespace. What's in scope (v0.1.25.x): - Mapping table 1 — Cycles `ErrorCode` → APS Tier-1 (15 closed values from `cycles-protocol-v0.yaml` L429-L446, each with canonical-cited HTTP status code or explicit "(inferred)" flag) - Mapping table 2 — Cycles `DecisionReasonCode` → APS Tier-1 (6 known v0.1.25 base values; open enum per canonical L487 with documented unknown-value fallback to `rail_error`) - Tier-2 `cycles.denial_detail` namespace schema covering both ErrorCode-sourced and DecisionReasonCode-sourced denials with standardized field naming (`code`, `source`, `http_status`, `message`, `request_id`, `trace_id`) - Forward-compat note documenting `requires_owner_confirmation` as the one APS Tier-1 value with no Cycles counterpart - TypeScript reference implementation (15-entry `ERROR_CODE_TO_TIER1` + 6-entry `DECISION_REASON_TO_TIER1` const tables + the `mapCyclesDenialToFoundation` function body), directly droppable into the APS adapter when it opens - Round-trip verification section mapping the three denial-path CyclesEvidence fixtures (03 dry-run-deny, 11 live-budget-exceeded, 12 decide-forbidden) to the expected Tier-1 outcomes — these are the golden test cases the adapter PR will use Scoped out (v0.2 promotion criteria): - `cycles-protocol-extensions-v0.1.26.yaml` adds 3 codes (`ACTION_QUOTA_EXCEEDED`, `ACTION_KIND_DENIED`, `ACTION_KIND_NOT_ALLOWED`) and the `DenyDetail` structure. The extension spec is published but not yet implemented in any Cycles deployment, so documenting its mapping now would create phantom rows no fixture can verify against. v0.2 of this doc will add three rows to each table with cross-path-consistent Tier-1 mappings, plus a `deny_detail` field on the evidence schema's response mirrors, plus at least one fixture exercising the extension end-to-end. Validation: - 15 `ErrorCode` rows in mapping table 1, all with specific HTTP status codes (canonical-cited where the spec pins them; inferred where it doesn't, with `(inferred)` flag) - 6 `DecisionReasonCode` rows in mapping table 2 - 15 + 6 entries in matching TypeScript const tables - 0 stray `cycles.denial_detail.error` or `cycles.denial_detail.reason_code` field-name references (standardized on `cycles.denial_detail.code`) - JSON example parses cleanly (no TypeScript union syntax) - All cross-repo line citations verified against current files via `gh api`: `aeoess/agent-passport-system/src/v2/payment-rails/types.ts` L92 (`PaymentReceipt`), L137 (`PaymentDenial`), L340 (`emitReceipt`), L344 (`emitDenial`); `hooks.ts` L87-L94 (`VALID_DENIAL_REASONS`), L381 + L606 + L629 (the three `INVALID_DENIAL_REASON` rejection points) - `python drafts/fixtures/cycles-evidence-v0.1/verify.py`: 13/13 fixtures still verify (unchanged from #90 merge) - `git diff --check origin/main...HEAD`: passed Review history preserved at #93 (5 review rounds with progressively tighter rigor: initial draft + self-audit on cross-repo citations → 4 reviewer findings on doc consistency → 4 more findings on v0.1.26-vs-v0.1.25.x scope → 3 findings against stale snapshot (already-fixed) → 2 findings on HTTP status codes and one stale "9 known values" reference → 1 finding on `PaymentReceipt` vs `PaymentDenial` type confusion). Each round expanded the `feedback_spec_review_rigor` memory rule set; the final rule count is 9, covering canonical-source verification, negative tests, deployment reality checks, and column-level literal-value cross-checking.
Extends the scaffold from the prior commit with the full sign/verify surface for all three Cycles artifact types (permit / release / denial) and 19 tests covering happy-path round-trips, tamper detection, accountability-shape invariants, TTL expiry, expected-signer mismatch, DID URI sync-path, and the three golden CyclesEvidence fixtures from runcycles/cycles-protocol#90. What lands: - Sign functions (mirror mpp pattern, field-omission canonical bytes, UUID-based receipt_id): signCyclesPermitReceipt → CyclesPermitReceipt (claim_type permit) signCyclesReleaseReceipt → CyclesReleaseReceipt (claim_type release) signCyclesDenial → CyclesDenial (claim_type denial) - Sync verify functions: verifyCyclesPermitReceipt(receipt, options) → CyclesVerifyResult verifyCyclesReleaseReceipt(receipt, options) → CyclesVerifyResult verifyCyclesDenial(denial, options) → CyclesVerifyResult - Async DID-resolving verify functions (matches mpp's verifyMppReceiptWithDID + verifyMppDenialWithDID): verifyCyclesPermitReceiptWithDID(receipt, options) verifyCyclesReleaseReceiptWithDID(receipt, options) verifyCyclesDenialWithDID(denial, options) - Sign-input shapes in types.ts: SignCyclesPermitReceiptInput SignCyclesReleaseReceiptInput SignCyclesDenialInput VerifyCyclesOptions CyclesResolveDidDocument (DID document resolver type, mirrors MppResolveDidDocument) - Default scope_of_claim assertions per artifact type: defaultCyclesPermitReceiptScope — "aps:cycles.permit:emit" defaultCyclesReleaseReceiptScope — "aps:cycles.release:emit" defaultCyclesDenialScope — "aps:cycles.deny:emit" Each carries the right does_not_assert disclaimers for that artifact (permit doesn't assert commit; release doesn't assert action was cancelled; denial doesn't assert rail unavailability or agent malice). - Shared _verifyReceiptCore helper checking: * claim_type matches expected literal * receipt_id, signer, agent_id, delegation_ref, action_ref present * rail_name === 'cycles' (structural check; INVALID_RAIL_NAME) * cycles_evidence.cycles_evidence_url and cycles_evidence.cycles_evidence_id_sha256 present * options.expected_signer matches signer (if provided) * issued_at parses; not past TTL (default 24h; EXPIRED reason) * timestamp === issued_at (accountability-shape invariant) * scope_of_claim.asserts is a non-empty string * DID URI signer triggers DID_RESOLVER_MISSING (caller should use the async WithDID path) * Ed25519 signature verifies against the signer's hex pubkey - Verify-reason enum (CyclesVerifyReason) extended with two new reasons surfaced by self-review: EXPIRED — TTL violation (was SIGNATURE_INVALID before self-review caught the bug) INVALID_RAIL_NAME — rail_name not 'cycles' (new structural check) Self-review caught (and fixed) before commit: * EXPIRED reason was undefined in the enum; TTL path was emitting SIGNATURE_INVALID with a misleading detail message * rail_name === 'cycles' wasn't validated * timestamp === issued_at and scope_of_claim.asserts non-empty weren't validated (mpp pattern; missed in the scaffold) Test coverage (19 tests, all green): * 3 golden-fixture mapping tests (03-reserve-dry-run-deny, 11-reserve-live-budget-exceeded, 12-decide-live-forbidden) — copied from runcycles/cycles-protocol/drafts/fixtures/cycles-evidence-v0.1/ cases/ to tests/v2/payment-rails/cycles-fixtures/ * 1 unknown-ErrorCode → rail_error fallback test * 4 permit-receipt tests (round-trip, tampered reservation_id, missing cycles_evidence, wrong claim_type) * 2 release-receipt tests (round-trip with reason, tampered amount) * 3 denial tests (ErrorCode-sourced round-trip, DecisionReasonCode- sourced round-trip, tampered cycles.denial_detail.code) * 1 expected_signer mismatch test * 1 sync DID URI → DID_RESOLVER_MISSING test * 1 TTL → EXPIRED test * 1 tampered rail_name → INVALID_RAIL_NAME test * 2 accountability-invariant tests (timestamp != issued_at, empty scope_of_claim.asserts) Regression sanity: * npm run build clean (tsc + chmod) * npm run test:quick 15/15 pass (unchanged) * mpp tests 41/41 pass (unchanged — no shared code modified) Still TODO for v0.1 (explicit in file header): * cycles_evidence_id_sha256 join-integrity check against a fetched CyclesEvidence envelope (load-bearing offline-audit guarantee). VerifyCyclesOptions has no envelope slot today; future option add will pass the envelope in and recompute the sha256. * preAuthorizeCyclesReserve gateway hook into PaymentRail interface (pending design discussion in PR review).
…gn/verify TODO) Lays the directory + denial-mapping function for the APS ↔ Cycles budget-authority rail at `src/v2/payment-rails/cycles/`. Closes the fifth step in the integration sequencing agreed at #25 comment 4429584316; the prior four (vocabulary crosswalks, CyclesEvidence spec, denial-mapping spec, SDK verifier acceptance for the rail.budget_reservation.*.v1 literals) have all merged. What lands in this commit: - `cycles/types.ts` — APS claim_type literal constants (rail.budget_reservation.{permit,release,denial}.v1, the three landed in #27), `CyclesEvidenceRef` minimal join shape (cycles_evidence_url + cycles_evidence_id_sha256 + action_ref + delegation_ref per #25 comment 4422627045), minimal read-only `CyclesEvidenceView` mirror over the canonical cycles-evidence-v0.1.yaml envelope shape, and skeleton interfaces for `CyclesPermitReceipt` / `CyclesReleaseReceipt` / `CyclesDenial` / `CyclesDenialDetail` / `CyclesVerifyReason` / `CyclesVerifyResult`. - `cycles/index.ts` — `mapCyclesDenialToFoundation()` fully implemented per the merged spec at runcycles/cycles-protocol drafts/cycles-aps-denial-mapping-v0.1.md (commit 9b0fb5e). Two const tables ported byte-for-byte: * ERROR_CODE_TO_TIER1 — 15 v0.1.25 base ErrorCode values (canonical cycles-protocol-v0.yaml L429-L446) * DECISION_REASON_TO_TIER1 — 6 v0.1.25 base DecisionReasonCode values (canonical L487-L545) Unknown DecisionReasonCode values gracefully degrade to rail_error per canonical L503-L505; raw code preserved Tier-2 byte-for-byte in `cycles.denial_detail.code`. What is NOT in this commit (explicit TODO comments in index.ts): - signCyclesPermitReceipt / signCyclesReleaseReceipt / signCyclesDenial — Ed25519 emission paths over JCS-canon receipt bodies. Will mirror the mpp pattern (sign with signature='', canonicalize, sha256→receipt_id, re-canonicalize, sign, hex). - verifyCyclesPermitReceipt / verifyCyclesReleaseReceipt / verifyCyclesDenial — symmetric verification, plus cycles_evidence_id_sha256 join-integrity check against the fetched CyclesEvidence envelope (the load-bearing offline-audit guarantee). - Pre-authorization hook (preAuthorizeCyclesReserve) — the gateway- side hook into the existing PaymentRail interface. Pending decision on whether it belongs in v0.1 or can ship in a follow-up. - Fixture-test wiring — the three golden fixtures from cycles-protocol drafts/fixtures/cycles-evidence-v0.1/cases/ (03, 11, 12) will land alongside sign/verify in the next commit so the denial-mapper has wire-level round-trip coverage. v0.1.26 extension codes (ACTION_QUOTA_EXCEEDED / ACTION_KIND_DENIED / ACTION_KIND_NOT_ALLOWED) and the DenyDetail structure are OUT OF SCOPE for v0.1 per the merged mapping spec. The extension spec exists in cycles-protocol but is not yet implemented in any Cycles deployment; mapping it now would create phantom rows no fixture can verify against. v0.2 promotion criterion documented in the merged denial-mapping spec. Validation (all empirical, before commit): - 5/5 cited commit hashes verified via `gh api` against the right repos: 9b0fb5e (cycles-protocol #93), 61186a2 (cycles-protocol #90), 3ccc17f (agent-governance-vocabulary #92), 161b1d4 (agent-governance-vocabulary #91), 4abd9df (this repo, #27). - ERROR_CODE_TO_TIER1 and DECISION_REASON_TO_TIER1 byte-match the merged spec exactly (15/15 + 6/6 entries, set-compared on parsed TS). - All 5 Tier-1 values used (no_commerce_scope, spend_limit_exceeded, wallet_revoked, time_window_violation, rail_error) are members of the closed APS `DenialReason` enum at src/v2/payment-rails/types.ts. - Compiled scaffold runs end-to-end against the three golden fixtures from runcycles/cycles-protocol — 3/3 produce predicted Tier-1 + Tier-2 outcomes. - npm run build clean; npm run test:quick 15/15 pass. Status: DRAFT until sign/verify + pre-auth hook + fixture tests land. Refs: - #25 (integration thread) - #27 (SDK PR adding rail.budget_reservation literals, merged 4abd9df) - aeoess/agent-governance-vocabulary#91 (budget_reservation crosswalk, merged 161b1d4) - aeoess/agent-governance-vocabulary#92 (cycles.yaml crosswalk, merged 3ccc17f) - runcycles/cycles-protocol#90 (CyclesEvidence envelope, merged 61186a2) - runcycles/cycles-protocol#93 (denial-mapping spec, merged 9b0fb5e)
Summary
New schema-only OpenAPI 3.1 draft at
drafts/cycles-evidence-v0.1.yamldefining a JCS-canonicalized, Ed25519-signed envelope for the four Cycles authorization lifecycle events (decide/reserve/commit/release).signatureemptied.evidence_idpopulated andsignatureemptied (id-then-signature ordering matches APSpayment-rails/canonicalize.tsand Wave 1 accountability artifacts — so a verifier that already knows how to verify an APSPaymentReceiptcan verify aCyclesEvidenceenvelope with the same primitives, only the canonical-bytes input differs).artifact_type.Why
The Cycles runtime API responses (
DecisionResponse,ReservationCreateResponse,CommitResponse,ReleaseResponse) are sufficient for the immediate caller with a live connection to the Cycles server. They are not sufficient for cross-system, ledger-independent audit consumers (notably APS, https://github.com/aeoess/agent-passport-system) who need a single artifact they can fetch by content hash and verify offline.This draft also closes two concrete gaps the Cycles signal-type crosswalk at aeoess/agent-governance-vocabulary#92 surfaced during review:
reservation_idlinkage. On the wire,commit_reservationandrelease_reservationtakereservation_idas a URL path argument; the response bodies do not echo it. An evidence consumer reading just the JSON cannot recover the authorization → settlement chain. The CyclesEvidence envelope hoistsreservation_idinto the signed payload forcommitandreleaseartifacts, so the linkage is visible from evidence alone.replay_classpromotion path. The crosswalk currently declaresreplay_class: decision_replaybecause Cycles' wire JSON requires the server's ledger state for verification. Once this envelope is normative and emitted by reference implementations,replay_classis promotable back tofull_replay— replay becomes possible from recorded evidence alone.Status
DRAFT (v0.1). Lives under
drafts/for review and external feedback. Will move to a numbered spec file at repo root (cycles-evidence-v0.2.yaml) once:What's in scope (v0.1)
schema_version,artifact_type,server_id,signer_did,issued_at_ms,payload,evidence_id,signature.decide/reserve/commit/release, each pairing the Cycles request and response so evidence is self-contained.evidence_idandsignatureset to"".evidence_idpopulated andsignatureset to"".What's out of scope (v0.1, deliberately deferred)
did:cyclesmethod registration. These land alongside the normative move.system.expireemitted by the server's expiry sweep).Implementation notes
cycles-protocol-v0.yamltypes (DecisionRequest,ReservationCreateRequest, etc.) are inlined under*Mirrorschemas so this file validates standalone. When promoted to normative, these should be replaced with cross-file$refs intocycles-protocol-v0.yamlfor single-source-of-truth.model_fields[*].is_required()), the same verification trail used to fix four rounds of review feedback on feat: crosswalk/cycles.yaml v0.1 — signal-type rows for Cycles budget authority aeoess/agent-governance-vocabulary#92.drafts/aeoess-crosswalk.yaml(Cycles ↔ APS receipt vocabulary mapping) is in-flight on a separate working copy and intentionally not part of this commit. The two drafts are orthogonal: this PR specifies the envelope shape; that draft specifies how Cycles vocabulary maps to APS receipt fields. Both can land independently.Validation
make lint-sourcesdoes not lintdrafts/(only the canonical source specs at repo root). Running spectral directly:Result: 0 errors, 3 warnings. The three warnings are unavoidable for a schema-only draft:
oas3-api-servers— file declares noservers:because there are no endpoints (the envelope wraps responses from endpoints already defined incycles-protocol-v0.yaml).oas3-unused-componentonCyclesEvidence— spectral can't trace usage without paths.protocol-schemas-no-additional-propertiesonEvidencePayload— incompatible with theoneOfdiscriminator; each variant carries its ownadditionalProperties: false.References
CyclesEvidenceRefminimal join shape this envelope plugs into.reservation_id-linkage gap and thereplay_classpromotion path.Test plan
reservation_idhoisting oncommit/releasepayloads addresses the linkage gap from feat: crosswalk/cycles.yaml v0.1 — signal-type rows for Cycles budget authority aeoess/agent-governance-vocabulary#92 review round 4.payment-rails/canonicalize.tsso a verifier can reuse primitives.evidence_id, sign, then re-verify; commit fixtures alongside the spec.CyclesEvidenceRef-keyed verification and a reference Cycles server emits envelopes, promotereplay_classon feat: crosswalk/cycles.yaml v0.1 — signal-type rows for Cycles budget authority aeoess/agent-governance-vocabulary#92 (or its successor) fromdecision_replaytofull_replay.