Skip to main content

axon/pcc/
proof_term.rs

1//! §Fase 51.a — Proof-Carrying Code: the portable proof object.
2//!
3//! A [`ProofTerm`] is the serializable artifact a producer (the axon
4//! compiler) emits alongside compiled code, certifying that a declared
5//! property holds. A consumer runs the INDEPENDENT checker
6//! ([`crate::pcc::checker`]) to verify it — WITHOUT trusting the
7//! producer (D51.2). The term travels as JSON, the same delivery
8//! surface as the SBOM / in-toto statements in [`crate::esk::attestation`],
9//! but unlike those it is a *proof* the consumer re-checks, not an
10//! attestation the consumer trusts.
11//!
12//! ## D51.1 — representation
13//!
14//! - [`PropertyClass`] — closed enum of property kinds. §51.a ships
15//!   exactly [`PropertyClass::ComplianceCoverage`]; §51.b-e extend it.
16//! - `artifact_digest` — SHA-256 hex of the canonical IR JSON the proof
17//!   is ABOUT. Binds the proof to a specific artifact: a proof for
18//!   program A cannot be replayed against program B (the checker
19//!   recomputes the digest and rejects a mismatch).
20//! - [`Witness`] — the property-specific derivation the checker
21//!   re-verifies against the artifact.
22//! - `axon_version` — producer version. Diagnostic only: the checker
23//!   does NOT trust it (it re-derives the property regardless).
24
25use serde::{Deserialize, Serialize};
26
27/// The closed catalog of properties a [`ProofTerm`] can certify.
28///
29/// §Fase 51.a ships [`Self::ComplianceCoverage`]. The §51.b-e classes
30/// (`EffectRowSoundness`, `CapabilityIsolation`, `ResourceBounds`,
31/// `ShieldHaltGuarantee`) land as the proof-term language generalizes
32/// (D51.4 — "universal" is the architecture, shipped one class at a
33/// time). Adding a variant here requires a matching witness variant +
34/// checker arm — the §51.a drift gate pins this lockstep.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub enum PropertyClass {
37    /// Every regulatory class an apx/axonendpoint declares in
38    /// `compliance:` is (a) a known class in the closed
39    /// [`crate::esk::compliance`] registry and (b) backed by a present,
40    /// resolvable shield (`shield_ref` non-empty AND that shield exists
41    /// in the program IR). Catches phantom compliance classes (a
42    /// typo'd `HIPPA`) and compliance-claimed-without-enforcement
43    /// (declaring GDPR with no attached shield).
44    ComplianceCoverage,
45    /// §51.b — every entry in a tool's `effects: <...>` row is
46    /// well-formed: its base is in the closed effect catalog
47    /// ([`crate::pcc::effects::EFFECT_BASES`]); `stream` / `trust`
48    /// carry a qualifier (`stream`'s in the backpressure catalog); and
49    /// `pure` is exclusive (a tool cannot be both `pure` and
50    /// effectful). Catches phantom effects (typo'd `network`), bare
51    /// `stream` without a backpressure policy, and pure/impure
52    /// contradictions.
53    EffectRowSoundness,
54    /// §51.c — every capability gate an `axonstore` declares (its
55    /// Pillar IV `capability` slug, §Fase 35.j) is a well-formed §32.g
56    /// capability scope (matches the closed grammar
57    /// `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`, via the OSS
58    /// `axon_frontend::parser::is_valid_capability_slug`). A malformed
59    /// gate is a Pillar IV defect: it can never match a properly-formed
60    /// bearer capability, so the store is either locked out or — worse,
61    /// if a consumer treats "unparseable gate" as "no gate" — silently
62    /// bypassed.
63    ///
64    /// **Scope (honest):** this is the gate-integrity half. The
65    /// containment half — an apx's reachable store gates ⊆ its declared
66    /// `requires:` set — needs the endpoint `requires_capabilities`
67    /// (AST / enterprise deploy metadata, NOT lowered to the frontend
68    /// IR) + flow→store reachability, and is deferred to §51.x
69    /// (enterprise PCC consumption, where `requires` lives).
70    CapabilityIsolation,
71    /// §51.d — declared resource bounds are within sane limits:
72    /// an apx/axonendpoint's `retries` is in `[0, MAX_RETRIES]`
73    /// (negative is nonsensical; above the ceiling is a retry storm),
74    /// and a `socket` carrying a DECLARED `backpressure: credit(k)`
75    /// has `k >= 1` (a credit window of 0 deadlocks the §Fase 41.b
76    /// typed-resource gate). Unspecified socket credit is a legitimate
77    /// type state and is NOT refuted. `timeout` is out of scope by
78    /// design — it is a closed duration enum (`{5s,15s,30s,60s}`)
79    /// already bounded at parse time, not an unbounded-resource risk.
80    ResourceBounds,
81    /// §51.e — a shield's breach policy provides a real guarantee:
82    /// `on_breach` is a recognized policy (closed catalog — catches a
83    /// typo'd `hault`, which the parser does NOT reject since it reads
84    /// `on_breach` as a bare identifier), and a shield declaring
85    /// `on_breach: halt` actually scans something (a halt shield with
86    /// an empty `scan: []` can never detect a breach, so the halt never
87    /// fires — a vacuous guarantee / security theater).
88    ShieldHaltGuarantee,
89    /// §51.x — the CONTAINMENT half of capability isolation (the
90    /// deferral §51.c flagged): every capability gate on a store the
91    /// apx's `execute_flow` REACHES is covered by the endpoint's
92    /// declared `requires:` scopes. Otherwise the flow touches a store
93    /// gated by capability `G` the endpoint does not declare requiring
94    /// — a request satisfying the endpoint's declared requires could
95    /// still reach a store it is not authorized for (capability leak /
96    /// privilege escalation). Reachability is a SOUND
97    /// over-approximation: every statically-reachable store op (both
98    /// conditional branches + the loop body) is counted. An
99    /// unresolvable `execute_flow` is REFUTED (cannot certify
100    /// containment for a flow the artifact does not contain).
101    CapabilityContainment,
102    /// §58.i — every structured `use <Tool>(k = v, …)` call satisfies the
103    /// called tool's declared `parameters:` input schema: no argument
104    /// names a parameter the tool does not declare; no argument is
105    /// supplied twice; every required (non-optional) parameter is
106    /// supplied; and every UNAMBIGUOUS-LITERAL argument's type aligns
107    /// with the declared type. This makes the §58.d CT-2 caller-blame
108    /// check an INDEPENDENTLY-VERIFIABLE proof — the tool schema rides
109    /// the bundle and the verifier re-derives the call's soundness from
110    /// the artifact, never trusting the compiler that ran the type-check
111    /// (D1). Literal-type alignment is CONSERVATIVE (only Int/Float/Bool
112    /// literals; bare identifiers + `${…}` interpolations are
113    /// runtime-resolved and skipped — zero false positives), so the
114    /// structural facts (unknown / duplicate / missing) are always
115    /// certified and the literal-type facts are certified where decidable
116    /// statically. A schema-less tool (no `parameters:`) and the legacy
117    /// `use <Tool> on <arg>` form carry no contract → no proof (D5).
118    ToolCallSoundness,
119}
120
121/// §51.e — the closed breach-policy catalog. Mirror of
122/// `axon_frontend::type_checker::VALID_ON_BREACH_POLICIES` (private
123/// const). The checker's own statement of the spec (D51.2). Cross-crate
124/// drift gate deferred to §51.f alongside the effect catalog.
125pub const VALID_BREACH_POLICIES: &[&str] =
126    &["deflect", "escalate", "halt", "quarantine", "sanitize_and_retry"];
127
128/// §51.d — the retry-storm ceiling. A declared `retries` above this is
129/// almost certainly a defect (an unbounded-ish retry storm), not a
130/// legitimate config. Generous on purpose so legitimate high-retry
131/// configs are not false-positived; the negative-retries case is the
132/// unambiguous defect this bound primarily guards.
133pub const MAX_RETRIES: i64 = 100;
134
135impl PropertyClass {
136    /// Stable wire slug for the property class.
137    pub fn slug(&self) -> &'static str {
138        match self {
139            PropertyClass::ComplianceCoverage => "compliance_coverage",
140            PropertyClass::EffectRowSoundness => "effect_row_soundness",
141            PropertyClass::CapabilityIsolation => "capability_isolation",
142            PropertyClass::ResourceBounds => "resource_bounds",
143            PropertyClass::ShieldHaltGuarantee => "shield_halt_guarantee",
144            PropertyClass::CapabilityContainment => "capability_containment",
145            PropertyClass::ToolCallSoundness => "tool_call_soundness",
146        }
147    }
148}
149
150/// §51.a — witness for [`PropertyClass::ComplianceCoverage`].
151///
152/// The derivation the producer recorded. The checker RE-DERIVES every
153/// field from the artifact and rejects the proof if the witness
154/// disagrees (D51.2 — a forged witness is caught because the checker
155/// recomputes, it does not believe the claim).
156///
157/// The property certified: the shield attached to a compliance-bearing
158/// apx **actually covers** every regulatory class the apx declares —
159/// `covers(provided, required) == ∅` (the existing
160/// [`crate::esk::compliance::covers`] predicate), with no phantom
161/// classes and a present, resolvable shield.
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163pub struct ComplianceCoverageWitness {
164    /// The apx / axonendpoint this proof is about.
165    pub endpoint_name: String,
166    /// The regulatory classes the endpoint declared, sorted + deduped
167    /// (canonical so the checker's re-derivation compares equal).
168    pub required_classes: Vec<String>,
169    /// The endpoint's `shield_ref` (empty string = no shield declared).
170    pub shield_ref: String,
171    /// Whether `shield_ref` is non-empty AND resolves to a shield
172    /// present in the program IR.
173    pub shield_present: bool,
174    /// The regulatory classes the resolved shield PROVIDES (its
175    /// `compliance:` set), sorted + deduped. Empty when no shield
176    /// resolves.
177    pub provided_classes: Vec<String>,
178    /// The subset of `required_classes` that are NOT in the closed
179    /// regulatory registry (phantom compliance claims). Empty for a
180    /// verifying proof.
181    pub unknown_classes: Vec<String>,
182    /// The subset of `required_classes` the shield does NOT provide
183    /// (`required \ provided` — the coverage gap), sorted. Empty for a
184    /// verifying proof.
185    pub uncovered_classes: Vec<String>,
186}
187
188/// §51.b — witness for [`PropertyClass::EffectRowSoundness`].
189///
190/// The derivation for one tool's declared effect row. The checker
191/// re-derives every field from the tool's IR and rejects a disagreement
192/// as forgery (D51.2).
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194pub struct EffectRowSoundnessWitness {
195    /// The tool this proof is about.
196    pub tool_name: String,
197    /// The tool's declared effect-row entries, sorted + deduped
198    /// (canonical so the checker's re-derivation compares equal). Each
199    /// entry is `base` or `base:qualifier`.
200    pub declared_effects: Vec<String>,
201    /// Entries whose base effect is NOT in the closed catalog (phantom
202    /// effects). Empty for a verifying proof.
203    pub unknown_bases: Vec<String>,
204    /// Qualifier-required bases (`stream` / `trust`) declared WITHOUT a
205    /// qualifier (bare `stream` / `trust`). Empty for a verifying proof.
206    pub missing_qualifier: Vec<String>,
207    /// `stream:<q>` entries whose qualifier is not a valid backpressure
208    /// policy. Empty for a verifying proof.
209    pub invalid_stream_qualifier: Vec<String>,
210    /// True when `pure` appears alongside any other effect (a tool
211    /// cannot be both pure and effectful). False for a verifying proof.
212    pub purity_violation: bool,
213}
214
215/// §51.c — witness for [`PropertyClass::CapabilityIsolation`].
216///
217/// The derivation for one `axonstore`'s capability gate. The checker
218/// re-reads the store's `capability` from the IR and re-runs the §32.g
219/// grammar validator; a forged witness is rejected because the
220/// recomputation disagrees (D51.2).
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub struct CapabilityIsolationWitness {
223    /// The `axonstore` this proof is about.
224    pub store_name: String,
225    /// The store's declared Pillar IV capability gate slug.
226    pub capability: String,
227    /// True when `capability` is non-empty AND does NOT match the
228    /// §32.g capability-scope grammar. False for a verifying proof.
229    pub malformed: bool,
230}
231
232/// §51.d — witness for [`PropertyClass::ResourceBounds`]. One subject
233/// per proof: an endpoint's retry bound OR a socket's credit window.
234/// Tagged by `subject` so the JSON is self-describing.
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236#[serde(tag = "subject")]
237pub enum ResourceBoundsWitness {
238    /// An apx/axonendpoint's declared `retries`.
239    EndpointRetry {
240        endpoint_name: String,
241        retries: i64,
242        /// True when `retries` is in `[0, MAX_RETRIES]`.
243        in_bounds: bool,
244    },
245    /// A socket's DECLARED `backpressure: credit(k)` window. Generated
246    /// only when the socket declares a credit (unspecified is not
247    /// witnessed — it is a legitimate type state, not a bound to
248    /// certify).
249    SocketCredit {
250        socket_name: String,
251        credit: i64,
252        /// True when `credit >= 1` (a 0 window deadlocks per §41.b).
253        positive: bool,
254    },
255}
256
257/// §51.e — witness for [`PropertyClass::ShieldHaltGuarantee`].
258///
259/// The derivation for one shield's breach policy. The checker re-reads
260/// the shield's `on_breach` + `scan` from the IR and recomputes both
261/// facts; a forged witness is rejected because the recomputation
262/// disagrees (D51.2).
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
264pub struct ShieldHaltGuaranteeWitness {
265    /// The shield this proof is about.
266    pub shield_name: String,
267    /// The shield's declared `on_breach` policy.
268    pub on_breach: String,
269    /// True when `on_breach` is in the closed breach-policy catalog.
270    pub known_policy: bool,
271    /// Count of declared `scan` categories (for the vacuous-halt check).
272    pub scan_count: usize,
273    /// True when `on_breach == "halt"` AND `scan` is empty — the halt
274    /// can never fire (no scan ⟹ no breach ⟹ no halt). False for a
275    /// verifying proof.
276    pub vacuous_halt: bool,
277}
278
279/// §51.x — witness for [`PropertyClass::CapabilityContainment`].
280///
281/// The derivation for one endpoint's reachable-store-gate containment.
282/// The checker re-resolves the `execute_flow`, re-walks its reachable
283/// store ops, re-resolves each store's capability gate, and recomputes
284/// the uncovered set; a forged witness is rejected because the
285/// recomputation disagrees (D51.2).
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287pub struct CapabilityContainmentWitness {
288    /// The apx / axonendpoint this proof is about.
289    pub endpoint_name: String,
290    /// The flow the endpoint executes.
291    pub execute_flow: String,
292    /// Whether `execute_flow` resolves to a flow present in the IR.
293    pub flow_resolved: bool,
294    /// The capability scopes the endpoint declares (`requires:`),
295    /// sorted + deduped.
296    pub declared_requires: Vec<String>,
297    /// The capability gates of the stores the flow REACHES (each
298    /// reached store's non-empty `capability`), sorted + deduped.
299    pub reached_gates: Vec<String>,
300    /// `reached_gates \ declared_requires` — gates the flow reaches but
301    /// the endpoint does not declare requiring. Empty for a verifying
302    /// proof.
303    pub uncovered_gates: Vec<String>,
304}
305
306/// §58.i — witness for [`PropertyClass::ToolCallSoundness`].
307///
308/// The derivation for ONE structured `use <Tool>(k = v, …)` call site.
309/// The checker re-walks the named flow, locates the call at `call_index`
310/// (deterministic walk order over the same digest-bound IR), re-reads
311/// the called tool's `parameters:` schema, and recomputes every fact; a
312/// forged witness (e.g. hiding an unknown argument) is rejected because
313/// the recomputation disagrees (D51.2).
314#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
315pub struct ToolCallSoundnessWitness {
316    /// The flow containing the call.
317    pub flow_name: String,
318    /// Ordinal of this call among ALL named-arg `use` calls in the flow,
319    /// in deterministic walk order (recursing into conditional branches +
320    /// for-in bodies). Locates the exact call site so the checker
321    /// re-derives the SAME one (two calls to the same tool in one flow
322    /// are distinguished).
323    pub call_index: usize,
324    /// The tool the call invokes.
325    pub tool_name: String,
326    /// The argument names supplied at the call, in SOURCE ORDER (so a
327    /// duplicate is visible). Re-derived from the IR call.
328    pub arg_names: Vec<String>,
329    /// The called tool's declared parameter names, sorted + deduped
330    /// (context + forgery surface). Empty if the tool is undeclared.
331    pub declared_params: Vec<String>,
332    /// Whether the called tool is declared with a NON-EMPTY `parameters:`
333    /// schema. A generated proof always has this `true` (a schema-less /
334    /// undeclared tool carries no contract → no proof).
335    pub schema_present: bool,
336    /// Args naming a parameter the tool does not declare, sorted +
337    /// deduped. Empty for a verifying proof.
338    pub unknown_args: Vec<String>,
339    /// Argument names supplied more than once, sorted + deduped. Empty
340    /// for a verifying proof.
341    pub duplicate_args: Vec<String>,
342    /// Required (non-optional) parameters not supplied, sorted. Empty for
343    /// a verifying proof.
344    pub missing_required: Vec<String>,
345    /// Unambiguous-literal args whose type does not align with the
346    /// declared type, each `name:expected:got`, sorted. Empty for a
347    /// verifying proof. (Bare identifiers / `${…}` interpolations are
348    /// runtime-resolved → not inferred → never listed.)
349    pub type_mismatches: Vec<String>,
350}
351
352/// The property-specific witness. Tagged so the JSON is self-describing
353/// + a future class adds a variant without ambiguity.
354#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
355#[serde(tag = "kind")]
356pub enum Witness {
357    ComplianceCoverage(ComplianceCoverageWitness),
358    EffectRowSoundness(EffectRowSoundnessWitness),
359    CapabilityIsolation(CapabilityIsolationWitness),
360    ResourceBounds(ResourceBoundsWitness),
361    ShieldHaltGuarantee(ShieldHaltGuaranteeWitness),
362    CapabilityContainment(CapabilityContainmentWitness),
363    ToolCallSoundness(ToolCallSoundnessWitness),
364}
365
366impl Witness {
367    /// §51.f — the subject (endpoint / tool / store / socket / shield)
368    /// the witness is about, for human-readable CLI output. Total.
369    pub fn subject_name(&self) -> &str {
370        match self {
371            Witness::ComplianceCoverage(w) => &w.endpoint_name,
372            Witness::EffectRowSoundness(w) => &w.tool_name,
373            Witness::CapabilityIsolation(w) => &w.store_name,
374            Witness::ResourceBounds(ResourceBoundsWitness::EndpointRetry {
375                endpoint_name,
376                ..
377            }) => endpoint_name,
378            Witness::ResourceBounds(ResourceBoundsWitness::SocketCredit {
379                socket_name,
380                ..
381            }) => socket_name,
382            Witness::ShieldHaltGuarantee(w) => &w.shield_name,
383            Witness::CapabilityContainment(w) => &w.endpoint_name,
384            Witness::ToolCallSoundness(w) => &w.tool_name,
385        }
386    }
387}
388
389/// The portable proof object (D51.1). Serializes to JSON; travels with
390/// the artifact; the independent checker verifies it.
391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
392pub struct ProofTerm {
393    /// The property class this term certifies.
394    pub property: PropertyClass,
395    /// SHA-256 hex of the canonical IR JSON the proof is about (binds
396    /// the proof to a specific artifact).
397    pub artifact_digest: String,
398    /// The derivation the checker re-verifies.
399    pub witness: Witness,
400    /// Producer version (diagnostic; NOT trusted by the checker).
401    pub axon_version: String,
402}
403
404/// §51.f — the portable proof bundle the `axon pcc prove` CLI emits +
405/// `axon pcc verify` consumes. Carries every proof generated for an
406/// artifact plus the artifact digest they all bind to (a quick
407/// sanity field — the per-proof `artifact_digest` is the authoritative
408/// binding the checker re-verifies).
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410pub struct ProofBundle {
411    /// Producer version that generated the bundle.
412    pub axon_version: String,
413    /// SHA-256 hex digest of the artifact all proofs bind to.
414    pub artifact_digest: String,
415    /// Every generated proof (across all property classes).
416    pub proofs: Vec<ProofTerm>,
417}