Skip to main content

runx_contracts/
receipt.rs

1// rust-style-allow: large-file - receipt contracts keep the signed envelope,
2// lineage, and effect-finality schemas together for cross-language schema
3// generation.
4//! Governance receipt contracts: the flat `runx.receipt.v1` shape, seals,
5//! fanout sync, lineage, and signatures.
6//!
7//! One flat artifact, each top-level key answering one question: integrity
8//! (envelope), dedup (idempotency), what ran (subject), what allowed it
9//! (authority), the inbound triggers (`signals[]`), the reasoning
10//! (`decisions[]`), what was done (`acts[]`), the outcome (seal), and graph/resume
11//! lineage. The post-run verdict is a `review`/`verification` act in `acts[]`
12//! (or a follow-up receipt linked by `lineage`), never a side contract. The
13//! reasoning and the full acts (intent, success criteria, criterion
14//! bindings) are INLINE: that is simultaneously the proof, the training signal,
15//! and the inspection narrative. Only the bulky per-act execution I/O (the
16//! agent-context envelope: instructions/inputs/output) is referenced via
17//! `acts[].context_ref` + `artifact_refs` and hydrated by projections.
18//! Verification is computed at read time, never part of the signed body.
19use serde::{Deserialize, Serialize};
20
21use crate::schema::{IsoDateTime, NonEmptyString, RunxSchema};
22use crate::{
23    ActForm, AuthorityAttenuation, AuthorityTerm, Closure, ClosureDisposition, CriterionBinding,
24    Decision, HashAlgorithm, Intent, JsonObject, Reference, RevisionDetails, VerificationDetails,
25};
26
27/// Logical schema name for the governance receipt.
28pub const RECEIPT_SCHEMA: &str = "runx.receipt.v1";
29
30/// Logical schema name reserved for follow-on receipts that record deferred
31/// effect finality. A finality receipt is emitted as a new artifact; sealed
32/// receipts are never mutated after the fact.
33pub const EFFECT_FINALITY_RECEIPT_SCHEMA: &str = "runx.effect_finality_receipt.v1";
34
35/// The canonicalization byte contract this receipt's digest commits under.
36pub const RECEIPT_CANONICALIZATION: &str = "runx.receipt.c14n.v1";
37
38#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
39pub enum ReceiptSchema {
40    #[serde(rename = "runx.receipt.v1")]
41    V1,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
45pub enum EffectFinalityReceiptSchema {
46    #[serde(rename = "runx.effect_finality_receipt.v1")]
47    V1,
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
51#[serde(rename_all = "snake_case")]
52pub enum EffectFinalityPhase {
53    Provisional,
54    InFlight,
55    Sealed,
56    Failed,
57    Reversed,
58}
59
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
61#[serde(deny_unknown_fields)]
62#[runx_schema(id = "runx.effect_finality_receipt.v1")]
63pub struct EffectFinalityReceipt {
64    pub schema: EffectFinalityReceiptSchema,
65    pub id: NonEmptyString,
66    pub created_at: IsoDateTime,
67    pub family: NonEmptyString,
68    pub phase: EffectFinalityPhase,
69    pub original_receipt_ref: Reference,
70    pub criterion_id: NonEmptyString,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub proof_ref: Option<Reference>,
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub evidence_refs: Vec<Reference>,
75    #[serde(default, skip_serializing_if = "Vec::is_empty")]
76    pub norm_refs: Vec<NonEmptyString>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub confirmation_depth: Option<u64>,
79    #[serde(default, skip_serializing_if = "JsonObject::is_empty")]
80    pub payload: JsonObject,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
84#[serde(rename_all = "snake_case")]
85pub enum FanoutReceiptStrategy {
86    All,
87    Any,
88    Quorum,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
92#[serde(rename_all = "snake_case")]
93pub enum FanoutReceiptDecision {
94    Proceed,
95    Halt,
96    Pause,
97    Escalate,
98}
99
100#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
101#[serde(deny_unknown_fields)]
102pub struct FanoutReceiptSyncPoint {
103    pub group_id: NonEmptyString,
104    pub strategy: FanoutReceiptStrategy,
105    pub decision: FanoutReceiptDecision,
106    pub rule_fired: NonEmptyString,
107    pub reason: NonEmptyString,
108    pub branch_count: usize,
109    pub success_count: usize,
110    pub failure_count: usize,
111    pub required_successes: usize,
112    #[serde(default)]
113    pub branch_receipts: Vec<NonEmptyString>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub gate: Option<JsonObject>,
116}
117
118/// Scoped byte commitment; unifies the old hash_commitments + enforcement.std*_hash.
119#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
120#[serde(rename_all = "snake_case")]
121pub enum ReceiptCommitmentScope {
122    Input,
123    Output,
124    Stdout,
125    Stderr,
126    Error,
127}
128
129#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
130#[serde(deny_unknown_fields)]
131pub struct ReceiptCommitment {
132    pub scope: ReceiptCommitmentScope,
133    pub algorithm: HashAlgorithm,
134    pub value: NonEmptyString,
135    pub canonicalization: NonEmptyString,
136}
137
138/// Canonical receipt subject kinds. The wire form on `Subject.kind` is an
139/// open `NonEmptyString` so receipts emitted by new subject categories (for
140/// example, tool build or hosted provider publication) do not require a
141/// contract edit.
142pub mod receipt_subject_kind {
143    /// A single skill invocation.
144    pub const SKILL: &str = "skill";
145    /// A graph execution composed of multiple acts.
146    pub const GRAPH: &str = "graph";
147}
148
149/// The input signal for training and inspection: where the run's input came
150/// from, a human-readable preview, and a content hash for integrity.
151#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
152#[serde(deny_unknown_fields)]
153pub struct ReceiptInputContext {
154    pub source: NonEmptyString,
155    pub preview: String,
156    pub value_hash: NonEmptyString,
157}
158
159#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
160#[serde(deny_unknown_fields)]
161pub struct Subject {
162    /// Open subject kind identifier (e.g. `receipt_subject_kind::SKILL`). Any
163    /// non-empty string is accepted; new subject categories do not need a
164    /// contract edit.
165    pub kind: NonEmptyString,
166    #[serde(rename = "ref")]
167    pub reference: Reference,
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub input_context: Option<ReceiptInputContext>,
170    #[serde(default)]
171    pub commitments: Vec<ReceiptCommitment>,
172}
173
174/// Enforcement profile is hashed; the granted authority stays readable.
175#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
176#[serde(deny_unknown_fields)]
177pub struct ReceiptEnforcement {
178    pub profile_hash: NonEmptyString,
179    #[serde(default)]
180    pub redaction_refs: Vec<Reference>,
181    #[serde(default)]
182    pub setup_refs: Vec<Reference>,
183    #[serde(default)]
184    pub teardown_refs: Vec<Reference>,
185}
186
187#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
188#[serde(deny_unknown_fields)]
189pub struct ReceiptAuthority {
190    pub actor_ref: Reference,
191    #[serde(default)]
192    pub grant_refs: Vec<Reference>,
193    #[serde(default)]
194    pub scope_refs: Vec<Reference>,
195    #[serde(default)]
196    pub authority_proof_refs: Vec<Reference>,
197    pub attenuation: AuthorityAttenuation,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub mandate_ref: Option<Reference>,
200    #[serde(default)]
201    pub terms: Vec<AuthorityTerm>,
202    pub enforcement: ReceiptEnforcement,
203}
204
205#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
206#[serde(deny_unknown_fields)]
207pub struct ReceiptIdempotency {
208    pub intent_key: NonEmptyString,
209    pub trigger_fingerprint: NonEmptyString,
210    pub content_hash: NonEmptyString,
211}
212
213/// Runner provenance for agent acts (drives the trainable-export projection).
214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
215#[serde(deny_unknown_fields)]
216pub struct RunnerProvenance {
217    pub provider: Option<String>,
218    pub model: Option<String>,
219    pub prompt_version: Option<String>,
220}
221
222/// What was done, rich and inline. The act's semantic core (intent, success
223/// criteria, criterion bindings, outcome) stays in the signed body; only the
224/// bulky execution I/O (the agent-context envelope: instructions/inputs/output
225/// and tool calls) is referenced via `context_ref` and hydrated by projections.
226#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
227#[serde(deny_unknown_fields)]
228pub struct ReceiptAct {
229    pub id: NonEmptyString,
230    pub form: ActForm,
231    pub intent: Intent,
232    pub summary: NonEmptyString,
233    #[serde(default)]
234    pub criterion_bindings: Vec<CriterionBinding>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub by: Option<RunnerProvenance>,
237    #[serde(default)]
238    pub source_refs: Vec<Reference>,
239    #[serde(default)]
240    pub target_refs: Vec<Reference>,
241    #[serde(default)]
242    pub artifact_refs: Vec<Reference>,
243    /// The agent-context envelope (instructions/inputs/output/tool-calls) is
244    /// referenced here and hydrated by the trainable/inspection projections.
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub context_ref: Option<Reference>,
247    pub closure: Closure,
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub revision: Option<RevisionDetails>,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub verification: Option<VerificationDetails>,
252}
253
254/// Exactly one seal. `deferred` expresses a suspended (waiting/delegated) run.
255#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
256#[serde(deny_unknown_fields)]
257pub struct Seal {
258    pub disposition: ClosureDisposition,
259    pub reason_code: NonEmptyString,
260    pub summary: NonEmptyString,
261    pub closed_at: IsoDateTime,
262    /// The last time the run was observed (advances for `deferred`/`monitor`
263    /// runs awaiting a follow-up verdict); equals `closed_at` for terminal seals.
264    pub last_observed_at: IsoDateTime,
265    #[serde(default)]
266    pub criteria: Vec<CriterionBinding>,
267}
268
269#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, RunxSchema)]
270#[serde(deny_unknown_fields)]
271pub struct Lineage {
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub parent: Option<Reference>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub previous: Option<Reference>,
276    #[serde(default)]
277    pub children: Vec<Reference>,
278    #[serde(default)]
279    pub sync: Vec<FanoutReceiptSyncPoint>,
280    // Open resolution request when seal.disposition == "deferred".
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub resume_ref: Option<Reference>,
283}
284
285#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
286#[serde(rename_all = "snake_case")]
287pub enum ReceiptIssuerType {
288    Local,
289    Hosted,
290    Ci,
291    Verifier,
292}
293
294#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
295#[serde(deny_unknown_fields)]
296pub struct ReceiptIssuer {
297    #[serde(rename = "type")]
298    pub issuer_type: ReceiptIssuerType,
299    pub kid: NonEmptyString,
300    pub public_key_sha256: NonEmptyString,
301}
302
303#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
304#[serde(rename_all = "PascalCase")]
305pub enum SignatureAlgorithm {
306    Ed25519,
307}
308
309#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
310#[serde(deny_unknown_fields)]
311pub struct ReceiptSignature {
312    pub alg: SignatureAlgorithm,
313    pub value: NonEmptyString,
314}
315
316/// The single signed governance receipt: `runx.receipt.v1`.
317///
318/// `decisions[]` (the reasoning, with `proposed_intent` + `justification`) and
319/// `acts[]` (intent, success criteria, criterion bindings) are inline: the proof
320/// and the training signal are the same artifact. `metadata` is a runtime-local
321/// read aid (legacy skill name, source type, actor labels for local projection)
322/// and is NOT part of the canonical signed body (the canonicalizer strips it).
323/// It is non-authoritative and must never be the source of a trust-bearing
324/// identity label. Display identity comes from signed fields:
325/// `subject.kind`, `subject.ref`, `issuer`, `authority.actor_ref`, and
326/// `acts[].by`.
327#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
328#[serde(deny_unknown_fields)]
329#[runx_schema(id = "runx.receipt.v1")]
330pub struct Receipt {
331    pub schema: ReceiptSchema,
332    pub id: NonEmptyString,
333    pub created_at: IsoDateTime,
334    pub canonicalization: NonEmptyString,
335    pub issuer: ReceiptIssuer,
336    pub signature: ReceiptSignature,
337    pub digest: NonEmptyString,
338    pub idempotency: ReceiptIdempotency,
339    pub subject: Subject,
340    pub authority: ReceiptAuthority,
341    /// Inbound triggers for this run: `runx:signal:` references whose
342    /// authenticity/trust/body live in the signal artifact.
343    #[serde(default)]
344    pub signals: Vec<Reference>,
345    #[serde(default)]
346    pub decisions: Vec<Decision>,
347    #[serde(default)]
348    pub acts: Vec<ReceiptAct>,
349    pub seal: Seal,
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub lineage: Option<Lineage>,
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub metadata: Option<JsonObject>,
354}