Skip to main content

subc_protocol/
manifest.rs

1//! Capability manifest schema for subc modules.
2//!
3//! All v1 modules are supervised singletons: one long-lived process per
4//! per-user machine. The manifest intentionally has **no `cardinality` field**.
5//! subc routes by module kind plus channel, while any finer demultiplexing
6//! (for example, AFT's per-project actor map) remains internal to the singleton
7//! module.
8
9use std::{collections::HashSet, fmt};
10
11use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
12use serde_json::Value;
13
14use crate::PROTOCOL_VERSION;
15
16/// A module's full declared participation in the subc mesh.
17///
18/// Construct via [`ModuleManifest::builder()`], never a struct literal. Adding a
19/// field to this struct would break every direct construction site; builder methods
20/// are additive, so constructors written against an older revision continue
21/// compiling when later fields land.
22#[derive(Serialize, Debug, Clone, PartialEq)]
23#[non_exhaustive]
24pub struct ModuleManifest {
25    pub module_id: String,
26    pub module_version: String,
27    pub protocol_ver: u8,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub trust_tier: Option<TrustTier>,
30    /// Whether this registered module is ready to receive new route binds.
31    ///
32    /// Absence means ready, preserving the behavior of manifests emitted before
33    /// this field existed. This is a best-effort admission hint, not an invariant:
34    /// the daemon reads readiness separately from reserving a relay, so modules
35    /// must still tolerate an `on_bind` while not ready.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub ready: Option<bool>,
38    /// Existing role declarations; capability grammar claims deliberately live in
39    /// the separate [`CapabilityDeclarations`] block below.
40    pub provides: Vec<ProviderRole>,
41    #[serde(default, skip_serializing_if = "Vec::is_empty")]
42    pub consumes: Vec<ConsumerRole>,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub bindings: Option<Bindings>,
45    /// Optional capability-grammar declarations.
46    ///
47    /// Omitting this block preserves the manifest contract used before capability
48    /// grammar was introduced. A present block is static discovery metadata that
49    /// the daemon validates before accepting a HELLO.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub capabilities: Option<CapabilityDeclarations>,
52    /// Events published as notices on `ck.{acct}.event.{module_id}.{event}.v{N}`.
53    /// The publisher serves the body; declared headers are filter hints, never authority.
54    ///
55    /// `None` means the module has not declared events. `Some(vec![])` affirmatively
56    /// declares that it publishes none. Absence remains absent on the wire.
57    ///
58    /// Declarations are validated at HELLO and retained in the daemon registry,
59    /// but no client can read them yet: `catalog.list`, `server.describe` and
60    /// `ck catalog` do not return them, because subc-control's `CatalogEntry`
61    /// has no field for them and adding one is a breaking change. A daemon built
62    /// before this field existed drops it silently. So a flow engine cannot yet
63    /// discover a module's events from the catalog, and a module must not depend
64    /// on that discovery until a daemon that serves declarations is running.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub events: Option<Vec<EventDeclaration>>,
67    /// Periodic or event-driven behavior this module performs against an external
68    /// surface, so later analysts can account for the resulting self-shaped time
69    /// series.
70    ///
71    /// Declarations describe the EFFECTIVE values in force at HELLO time. In
72    /// particular, a compile-time cadence constant belongs in
73    /// [`SignalCadence::Literal`], while a cadence resolved from configuration
74    /// belongs in [`SignalCadence::Derived`] with a pointer to that effective
75    /// source. Both provenance stories are honest; copying a stale configured
76    /// value into a literal is not.
77    ///
78    /// Ephemeral signals are out of scope for v1 because they are not durably
79    /// declarable, not because they are harmless. A v2 reader must not interpret
80    /// this field's absence in a v1 manifest as a judgement about ephemerals.
81    ///
82    /// `None` and `Some(vec![])` are deliberately distinct on the wire: an
83    /// absent block means the module has not adopted this vocabulary (readers
84    /// treat it as zero signals but must not treat it as a survey answer),
85    /// while an empty list is an affirmative declaration that the module
86    /// examined its effects and claims none are declarable. Modules that mean
87    /// "no signals" should declare the empty list; `None` is what an
88    /// un-adopted manifest looks like, not a statement.
89    ///
90    /// Convention for mutate-effect entries: where the mutation leaves a
91    /// per-observation tell on the surface itself (insula publishes the
92    /// relaxed `usedPercent` beside the raw figure, so any single reading
93    /// self-reports whether it was touched), name that tell in the
94    /// declaration's note. A standing registry row says the module sometimes
95    /// mutates; the tell says whether THIS observation was mutated — a
96    /// consumer holding one sample can act on the second, not the first.
97    /// A named tell must exist on the wire independently of this registry:
98    /// the declaration points at evidence, it is never the evidence. A tell
99    /// that exists only because the manifest describes it is a claim
100    /// vouching for itself.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub self_signals: Option<Vec<SelfSignalDeclaration>>,
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub provenance: Option<ManifestProvenance>,
105}
106
107/// Incrementally constructs a [`ModuleManifest`] without fabricating absent facts.
108#[derive(Debug, Clone)]
109pub struct ModuleManifestBuilder {
110    module_id: String,
111    module_version: String,
112    protocol_ver: u8,
113    trust_tier: Option<TrustTier>,
114    ready: Option<bool>,
115    provides: Vec<ProviderRole>,
116    consumes: Vec<ConsumerRole>,
117    bindings: Option<Bindings>,
118    capabilities: Option<CapabilityDeclarations>,
119    events: Option<Vec<EventDeclaration>>,
120    self_signals: Option<Vec<SelfSignalDeclaration>>,
121    provenance: Option<ManifestProvenance>,
122}
123
124impl ModuleManifest {
125    /// Starts a manifest builder with the minimal identification fields.
126    ///
127    /// `protocol_ver` defaults to the protocol version linked into this crate;
128    /// `provides` and `consumes` default to empty declarations. `trust_tier`
129    /// and `bindings` default to `None` because the daemon reads neither on any
130    /// production path; a required unread field forces producers to invent
131    /// fabricated values.
132    pub fn builder(
133        module_id: impl Into<String>,
134        module_version: impl Into<String>,
135    ) -> ModuleManifestBuilder {
136        ModuleManifestBuilder {
137            module_id: module_id.into(),
138            module_version: module_version.into(),
139            protocol_ver: PROTOCOL_VERSION,
140            trust_tier: None,
141            ready: None,
142            provides: Vec::new(),
143            consumes: Vec::new(),
144            bindings: None,
145            capabilities: None,
146            events: None,
147            self_signals: None,
148            provenance: None,
149        }
150    }
151}
152
153impl ModuleManifestBuilder {
154    /// Overrides the linked protocol version for compatibility fixtures.
155    pub fn protocol_ver(mut self, protocol_ver: u8) -> Self {
156        self.protocol_ver = protocol_ver;
157        self
158    }
159
160    /// Declares the optional trust tier of this module.
161    ///
162    /// The daemon does not evaluate this field on any production path.
163    pub fn trust_tier(mut self, trust_tier: Option<TrustTier>) -> Self {
164        self.trust_tier = trust_tier;
165        self
166    }
167
168    /// Declares whether the module is ready to receive new route binds.
169    pub fn ready(mut self, ready: bool) -> Self {
170        self.ready = Some(ready);
171        self
172    }
173
174    /// Declares the provider roles this module exposes.
175    pub fn provides(mut self, provides: Vec<ProviderRole>) -> Self {
176        self.provides = provides;
177        self
178    }
179
180    /// Declares the consumer roles this module requests.
181    pub fn consumes(mut self, consumes: Vec<ConsumerRole>) -> Self {
182        self.consumes = consumes;
183        self
184    }
185
186    /// Declares the optional resource and subsystem bindings of this module.
187    ///
188    /// The daemon does not evaluate this field on any production path.
189    pub fn bindings(mut self, bindings: Option<Bindings>) -> Self {
190        self.bindings = bindings;
191        self
192    }
193
194    /// Adds optional capability-grammar declarations.
195    pub fn capabilities(mut self, capabilities: Option<CapabilityDeclarations>) -> Self {
196        self.capabilities = capabilities;
197        self
198    }
199
200    /// Adds optional bus event declarations; an empty list declares no events.
201    pub fn events(mut self, events: Option<Vec<EventDeclaration>>) -> Self {
202        self.events = events;
203        self
204    }
205
206    /// Adds optional periodic or event-driven behavior declarations.
207    pub fn self_signals(mut self, self_signals: Option<Vec<SelfSignalDeclaration>>) -> Self {
208        self.self_signals = self_signals;
209        self
210    }
211
212    /// Adds optional build provenance declared by the module.
213    pub fn provenance(mut self, provenance: Option<ManifestProvenance>) -> Self {
214        self.provenance = provenance;
215        self
216    }
217
218    /// Finishes the manifest.
219    pub fn build(self) -> ModuleManifest {
220        ModuleManifest {
221            module_id: self.module_id,
222            module_version: self.module_version,
223            protocol_ver: self.protocol_ver,
224            trust_tier: self.trust_tier,
225            ready: self.ready,
226            provides: self.provides,
227            consumes: self.consumes,
228            bindings: self.bindings,
229            capabilities: self.capabilities,
230            events: self.events,
231            self_signals: self.self_signals,
232            provenance: self.provenance,
233        }
234    }
235}
236
237/// DELIBERATELY LENIENT: unknown top-level manifest keys are DROPPED at this
238/// parse boundary, not rejected and not retained. This is forward
239/// compatibility across version skew — a module built against a newer
240/// subc-protocol must still HELLO into an older daemon, and strictness here
241/// would turn every additive manifest field into a daemon-first flag day.
242/// The costs, so nobody re-derives them the hard way (CEREB found both):
243/// - A key you add module-side is INVISIBLE to the daemon until a typed field
244///   lands here. Producing it is honest; assuming a daemon-side reader exists
245///   is not. Say who the audience is next to any such producer.
246/// - There is deliberately NO untyped extension bag on this struct: a
247///   retained-verbatim Value map becomes an unversioned de-facto wire
248///   contract nobody authored (the drift class module-owned payload crates
249///   exist to prevent). When a daemon consumer materializes for a fact, the
250///   fact gets a typed optional field with a CONSUMER-IMPACT commit instead.
251///
252/// `CapabilityDeclarations` below is strict by contrast because claims are
253/// routed on: an unparseable claim must refuse loudly, never partially apply.
254#[derive(Deserialize)]
255struct ModuleManifestWire {
256    module_id: String,
257    module_version: String,
258    protocol_ver: u8,
259    #[serde(default)]
260    trust_tier: Option<TrustTier>,
261    #[serde(default)]
262    ready: Option<bool>,
263    provides: Vec<ProviderRole>,
264    #[serde(default)]
265    consumes: Vec<ConsumerRole>,
266    #[serde(default)]
267    bindings: Option<Bindings>,
268    #[serde(default)]
269    capabilities: Option<CapabilityDeclarations>,
270    #[serde(default)]
271    events: Option<Value>,
272    #[serde(default)]
273    self_signals: Option<Vec<SelfSignalDeclaration>>,
274    #[serde(default)]
275    provenance: Option<ManifestProvenance>,
276    // `runtime_computed` belongs to --manifest output rather than the retained
277    // manifest model. Deserialize it only long enough to enforce that capability
278    // declarations cannot be omitted as runtime-varying data.
279    #[serde(default)]
280    runtime_computed: Option<Value>,
281}
282
283impl<'de> Deserialize<'de> for ModuleManifest {
284    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
285    where
286        D: Deserializer<'de>,
287    {
288        let wire = ModuleManifestWire::deserialize(deserializer)?;
289        validate_runtime_computed(wire.runtime_computed.as_ref(), "runtime_computed")
290            .map_err(D::Error::custom)?;
291        validate_events_value(wire.events.as_ref()).map_err(D::Error::custom)?;
292        let events = wire
293            .events
294            .map(serde_json::from_value)
295            .transpose()
296            .map_err(D::Error::custom)?;
297        let mut builder = Self::builder(wire.module_id, wire.module_version)
298            .protocol_ver(wire.protocol_ver)
299            .trust_tier(wire.trust_tier);
300        if let Some(ready) = wire.ready {
301            builder = builder.ready(ready);
302        }
303        let manifest = builder
304            .provides(wire.provides)
305            .consumes(wire.consumes)
306            .bindings(wire.bindings)
307            .capabilities(wire.capabilities)
308            .events(events)
309            .self_signals(wire.self_signals)
310            .provenance(wire.provenance)
311            .build();
312        manifest
313            .validate_capability_grammar()
314            .map_err(D::Error::custom)?;
315        Ok(manifest)
316    }
317}
318
319/// A versioned bus event notice whose body is served by the publisher.
320#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
321#[non_exhaustive]
322pub struct EventDeclaration {
323    /// One NATS subject token: `[a-z0-9][a-z0-9_]{0,62}`.
324    ///
325    /// This is the event token rule in `cortexkit-bus-naming` 0.2.0,
326    /// [`token.rs`, lines 91–143](https://docs.rs/cortexkit-bus-naming/0.2.0/src/cortexkit_bus_naming/token.rs.html#91-143).
327    pub name: String,
328    /// Event schema version, at least 1.
329    pub version: u32,
330    /// Header names every notice carries, for filtering only, never authority.
331    /// Names match `[a-z][a-z0-9_]{0,31}` and are unique within the declaration.
332    pub headers: Vec<String>,
333    /// Short human line for consent cards, at most 200 characters without controls.
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub summary: Option<String>,
336}
337
338impl EventDeclaration {
339    /// Starts an event declaration with no filter headers or summary.
340    /// Validation happens when decoding a manifest or admitting its HELLO.
341    pub fn new(name: impl Into<String>, version: u32) -> Self {
342        Self {
343            name: name.into(),
344            version,
345            headers: Vec::new(),
346            summary: None,
347        }
348    }
349
350    /// Sets the header names carried by every notice of this event.
351    pub fn with_headers(mut self, headers: Vec<String>) -> Self {
352        self.headers = headers;
353        self
354    }
355
356    /// Sets or removes the short human summary.
357    pub fn with_summary(mut self, summary: Option<String>) -> Self {
358        self.summary = summary;
359        self
360    }
361}
362
363/// A safe-to-report event declaration failure, without echoing untrusted values.
364#[derive(Debug, Clone, PartialEq, Eq)]
365#[non_exhaustive]
366pub struct EventDeclarationError {
367    field: String,
368    reason: &'static str,
369}
370
371impl EventDeclarationError {
372    fn new(field: impl Into<String>, reason: &'static str) -> Self {
373        Self {
374            field: field.into(),
375            reason,
376        }
377    }
378
379    /// The precise malformed field path, relative to the manifest.
380    pub fn field(&self) -> &str {
381        &self.field
382    }
383}
384
385impl fmt::Display for EventDeclarationError {
386    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387        write!(
388            f,
389            "invalid event declaration: field {} {}",
390            self.field, self.reason
391        )
392    }
393}
394
395impl std::error::Error for EventDeclarationError {}
396
397/// Validates event declarations before HELLO decoding can lose field context.
398pub fn validate_hello_event_declarations(hello: &Value) -> Result<(), EventDeclarationError> {
399    validate_events_value(
400        hello
401            .get("manifest")
402            .and_then(|manifest| manifest.get("events")),
403    )
404}
405
406fn is_valid_event_name(name: &str) -> bool {
407    (1..=63).contains(&name.len())
408        && name.as_bytes()[0].is_ascii_alphanumeric()
409        && name
410            .bytes()
411            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
412}
413
414fn validate_events_value(value: Option<&Value>) -> Result<(), EventDeclarationError> {
415    let Some(value) = value.filter(|value| !value.is_null()) else {
416        return Ok(());
417    };
418    let entries = value
419        .as_array()
420        .ok_or_else(|| EventDeclarationError::new("events", "must be a list"))?;
421    if entries.len() > 64 {
422        return Err(EventDeclarationError::new(
423            "events",
424            "must contain at most 64 declarations",
425        ));
426    }
427    let mut pairs = HashSet::new();
428    for (index, entry) in entries.iter().enumerate() {
429        let field = format!("events[{index}]");
430        let entry = entry
431            .as_object()
432            .ok_or_else(|| EventDeclarationError::new(&field, "must be an object"))?;
433        let name_field = format!("{field}.name");
434        let name = entry
435            .get("name")
436            .and_then(Value::as_str)
437            .ok_or_else(|| EventDeclarationError::new(&name_field, "must be a string"))?;
438        if !is_valid_event_name(name) {
439            return Err(EventDeclarationError::new(
440                name_field,
441                "must match [a-z0-9][a-z0-9_]{0,62}",
442            ));
443        }
444        let version = entry
445            .get("version")
446            .and_then(Value::as_u64)
447            .filter(|version| (1..=u32::MAX as u64).contains(version))
448            .ok_or_else(|| {
449                EventDeclarationError::new(
450                    format!("{field}.version"),
451                    "must be an integer between 1 and u32::MAX",
452                )
453            })?;
454        if !pairs.insert((name, version)) {
455            return Err(EventDeclarationError::new(
456                &field,
457                "duplicates an event (name, version) pair",
458            ));
459        }
460        let headers_field = format!("{field}.headers");
461        let headers = entry
462            .get("headers")
463            .and_then(Value::as_array)
464            .ok_or_else(|| EventDeclarationError::new(&headers_field, "must be a list"))?;
465        if headers.len() > 16 {
466            return Err(EventDeclarationError::new(
467                headers_field,
468                "must contain at most 16 headers",
469            ));
470        }
471        let mut names = HashSet::new();
472        for (header_index, header) in headers.iter().enumerate() {
473            let header_field = format!("{headers_field}[{header_index}]");
474            let header = header
475                .as_str()
476                .ok_or_else(|| EventDeclarationError::new(&header_field, "must be a string"))?;
477            if !(1..=32).contains(&header.len())
478                || !header.as_bytes()[0].is_ascii_lowercase()
479                || !header
480                    .bytes()
481                    .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
482            {
483                return Err(EventDeclarationError::new(
484                    header_field,
485                    "must match [a-z][a-z0-9_]{0,31}",
486                ));
487            }
488            if !names.insert(header) {
489                return Err(EventDeclarationError::new(
490                    header_field,
491                    "duplicates a header name",
492                ));
493            }
494        }
495        if let Some(summary) = entry.get("summary").filter(|value| !value.is_null()) {
496            let summary_field = format!("{field}.summary");
497            let summary = summary
498                .as_str()
499                .ok_or_else(|| EventDeclarationError::new(&summary_field, "must be a string"))?;
500            if summary.chars().count() > 200 || summary.chars().any(char::is_control) {
501                return Err(EventDeclarationError::new(
502                    summary_field,
503                    "must contain at most 200 characters and no control characters",
504                ));
505            }
506        }
507    }
508    Ok(())
509}
510
511/// A raw HELLO declaration error that can be reported before serde drops context.
512#[derive(Debug, Clone, PartialEq, Eq)]
513pub struct SelfSignalDeclarationError {
514    module_id: String,
515    entry_index: usize,
516    field: &'static str,
517}
518
519impl fmt::Display for SelfSignalDeclarationError {
520    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
521        write!(
522            f,
523            "module_id '{}' self_signals[{}] is missing required field '{}'",
524            self.module_id.escape_debug(),
525            self.entry_index,
526            self.field
527        )
528    }
529}
530
531/// Reject raw HELLO self-signal declarations that omit `effect` or `anchored_to`.
532///
533/// Serde correctly rejects these omissions while decoding [`ModuleManifest`], but
534/// that decode does not retain the module id or list index needed for a useful
535/// daemon refusal. This preflight adds only that reporting context; it does not
536/// interpret a declaration's behavior.
537pub fn validate_hello_self_signal_declarations(
538    hello: &Value,
539) -> Result<(), SelfSignalDeclarationError> {
540    let Some(manifest) = hello.get("manifest").and_then(Value::as_object) else {
541        return Ok(());
542    };
543    let module_id = manifest
544        .get("module_id")
545        .and_then(Value::as_str)
546        .unwrap_or("<unknown>");
547    let Some(entries) = manifest.get("self_signals").and_then(Value::as_array) else {
548        return Ok(());
549    };
550
551    for (entry_index, entry) in entries.iter().enumerate() {
552        let Some(entry) = entry.as_object() else {
553            continue;
554        };
555        for field in ["effect", "anchored_to"] {
556            if !entry.contains_key(field) {
557                return Err(SelfSignalDeclarationError {
558                    module_id: module_id.to_string(),
559                    entry_index,
560                    field,
561                });
562            }
563        }
564    }
565    Ok(())
566}
567
568/// Static, versioned capabilities declared by a module.
569#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
570#[serde(deny_unknown_fields)]
571pub struct CapabilityDeclarations {
572    #[serde(default)]
573    pub provides: Vec<String>,
574    #[serde(default)]
575    pub requires: Vec<CapabilityRequirement>,
576    #[serde(default)]
577    pub must_never_reach: Vec<String>,
578}
579
580/// A declared periodic or event-driven behavior that shapes an external surface.
581#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
582pub struct SelfSignalDeclaration {
583    /// Stable identifier for this declared behavior, such as `codex_keepalive`.
584    pub name: String,
585    /// Signal classification. `Busy` participates in drain quiescence; the
586    /// remaining kinds are descriptive for operators and analysts.
587    ///
588    /// # A `Busy` GAUGE COUNTS WORK THE DRAIN CAN AFFECT, AND NOTHING ELSE
589    ///
590    /// The daemon waits for every declared `Busy` gauge to reach zero before
591    /// tearing a module down, up to the drain ceiling. That wait is only worth
592    /// anything for work whose RESULT WOULD BE DELIVERED if it finished during
593    /// the drain.
594    ///
595    /// The test: **if this request completed one millisecond before the
596    /// teardown, would anyone receive its answer?** If no, it does not belong
597    /// in a `Busy` gauge, and counting it makes the daemon wait out its whole
598    /// ceiling for something that cannot usefully complete.
599    ///
600    /// The case that names the rule (CEREB, 2026-09-19): cerebellum's
601    /// management requests can park for up to 300 s awaiting a HUMAN's consent
602    /// decision. Those were counted as in-flight, so a drain begun while a
603    /// prompt was open waited on a person until the 30 s ceiling expired --
604    /// and the pending answer dies with the process either way, so the wait
605    /// bought nothing. The fix splits the accounting: `executing` counts toward
606    /// `Busy`, `awaiting_consent` is reported as its own informational gauge.
607    ///
608    /// The same shape exists anywhere a request blocks on an external party the
609    /// restart invalidates -- a human, a peer seat, a vendor call whose reply
610    /// has nowhere to land. Those are legitimately IN FLIGHT and are not
611    /// legitimately `Busy`.
612    pub kind: SelfSignalKind,
613    /// Whether the signal only observes the surface or changes it.
614    pub effect: SelfSignalEffect,
615    /// The cadence, event, or health gauges that anchor this signal.
616    pub anchored_to: SignalAnchor,
617    /// The effective cadence in force at HELLO time.
618    ///
619    /// Use [`SignalCadence::Literal`] when a compile-time constant is the
620    /// effective value. Use [`SignalCadence::Derived`] when configuration or
621    /// another runtime input resolves the effective value, naming the source so
622    /// the declaration cannot silently drift from that resolution.
623    #[serde(default, skip_serializing_if = "Option::is_none")]
624    pub cadence: Option<SignalCadence>,
625    /// The external surface this behavior shapes, such as `provider-usage`.
626    #[serde(default, skip_serializing_if = "Option::is_none")]
627    pub domain: Option<String>,
628    #[serde(default, skip_serializing_if = "Option::is_none")]
629    pub note: Option<String>,
630}
631
632/// Class of a self-signal, tolerant of newer wire values.
633#[derive(Debug, Clone, PartialEq, Eq)]
634pub enum SelfSignalKind {
635    Keepalive,
636    /// Work that must settle before the daemon considers a module quiescent.
637    Busy,
638    Poller,
639    Cron,
640    Sweep,
641    Watchdog,
642    Heartbeat,
643    Other(String),
644}
645
646impl SelfSignalKind {
647    fn wire_name(&self) -> &str {
648        match self {
649            Self::Keepalive => "keepalive",
650            Self::Busy => "busy",
651            Self::Poller => "poller",
652            Self::Cron => "cron",
653            Self::Sweep => "sweep",
654            Self::Watchdog => "watchdog",
655            Self::Heartbeat => "heartbeat",
656            Self::Other(value) => value,
657        }
658    }
659}
660
661impl Serialize for SelfSignalKind {
662    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
663    where
664        S: serde::Serializer,
665    {
666        serializer.serialize_str(self.wire_name())
667    }
668}
669
670impl<'de> Deserialize<'de> for SelfSignalKind {
671    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
672    where
673        D: Deserializer<'de>,
674    {
675        let value = String::deserialize(deserializer)?;
676        Ok(match value.as_str() {
677            "keepalive" => Self::Keepalive,
678            "busy" => Self::Busy,
679            "poller" => Self::Poller,
680            "cron" => Self::Cron,
681            "sweep" => Self::Sweep,
682            "watchdog" => Self::Watchdog,
683            "heartbeat" => Self::Heartbeat,
684            _ => Self::Other(value),
685        })
686    }
687}
688
689/// The effect a self-signal has on the external surface it targets.
690#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
691#[serde(rename_all = "lowercase")]
692pub enum SelfSignalEffect {
693    Observe,
694    Mutate,
695}
696
697/// What anchors a self-signal to an interval, event, or health state.
698#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
699#[serde(rename_all = "snake_case")]
700pub enum SignalAnchor {
701    /// The behavior follows its own periodic signature, so analysts can find it
702    /// without an external event grid.
703    FixedInterval,
704    /// The behavior follows an external event boundary, which can make its shape
705    /// indistinguishable from the surface mechanism without this declaration.
706    Event { event: String },
707    /// Health metrics whose non-negative integer values are summed during drain.
708    HealthGauges { gauges: Vec<String> },
709}
710
711/// How a self-signal's effective cadence is declared.
712#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
713#[serde(rename_all = "snake_case")]
714pub enum SignalCadence {
715    Literal { interval_ms: u64 },
716    Derived { source: String },
717}
718
719/// Build facts a module DECLARES about its own binary at HELLO. The daemon
720/// overlays process-identity evidence it alone can attest; the two halves are
721/// served together via `supervisor.provenance` and never merged.
722///
723/// The canonical constructor form is a full 40-character lowercase hexadecimal
724/// `build_git_sha` and a full 64-character lowercase hexadecimal
725/// `build_lock_digest`; abbreviations are not conforming. The daemon's HELLO
726/// decoder intentionally remains lenient enough to relay older declarations,
727/// so this construction contract is enforced by [`build_provenance`] rather
728/// than by wire deserialization.
729///
730/// Honesty contract for constructors (ruled with the first adopters):
731/// - Every field is a VERIFIED-AT-BUILD claim. No field is required: a module
732///   may declare any subset, and omitting an inapplicable field is the honest
733///   choice rather than inventing a value to fill it. Populate `build_git_sha`
734///   only from a value injected by the build/release pipeline (`CK_BUILD_REV`
735///   via `option_env!` guarded by the packaging path, or build.rs equivalent)
736///   — never from ambient env at an arbitrary consumer compile, which mints a
737///   provenance claim from an accident of whoever ran cargo. A builder that
738///   can determine whether the tree was clean may declare the sha regardless
739///   of whether a release pipeline exists.
740/// - Dirty or unstamped builds declare `None` for the affected fields. A
741///   populated field stops the reader asking; absent-and-honest beats
742///   present-and-best-effort. Absence is reported at two levels with two
743///   distinct words: a module that declared no provenance block at all reads
744///   `unverifiable`, while an omitted field inside a declared block is
745///   dropped from the wire and reads `unavailable`. So omitting a field never
746///   costs a module its `Reported` status -- declaration is decided by
747///   whether the manifest carried a block, not by which fields it filled.
748/// - Dirty-tree stamps are not canonical `build_git_sha` values. A pipeline
749///   that emits `-dirty` must omit the affected field rather than pass that
750///   stamp to the canonical constructor. Stricter is better: cerebellum's
751///   build.rs reports the commit ONLY when the tree was clean, on the argument
752///   that dirty bytes match no commit and a precise-looking wrong answer beats
753///   absence at being believed.
754/// - Two silent-when-wrong checks for any build-rev embedder (CEREB): does
755///   the builder know whether the tree was clean, and can its no-git sentinel
756///   (source-tarball builds) escape into a field parsed as a sha? Sentinels
757///   render as absence, never as a value.
758/// - Fill fields FROM THE BUILD only: reading Cargo.lock or the wire crate
759///   version inside the manifest constructor describes the source tree
760///   sitting beside the running binary, not the binary — the exact claim
761///   this struct exists to avoid.
762/// - Declare what you KNOW, not blanket-None (WERNI): `store_schema_version`
763///   needs no pipeline — any module with a migration list can state its
764///   newest migration as fact, and a daemon comparing it against the store's
765///   actual version sees a stale-binary mismatch directly. Blanket `None`
766///   where a field is knowable wastes the field; blanket-fill where it is
767///   not mints a lie. Absence also beats sentinel values (CKCRED): omit the
768///   FIELD when BUILD_REV reads a builder sentinel ("unknown", "unavailable",
769///   "none", any casing) — publishing the sentinel string as a fact is a
770///   well-formed lie shape validation cannot catch. Field omission, not block
771///   omission, is the target shape for SDK modules: `wire_crate_version` is a
772///   compile-time constant of the linked crate, so a module using the SDK
773///   always has at least one honest fact and `build_provenance` reflects that
774///   by never returning an absent block. (Block absence remains meaningful on
775///   the wire — it reads `unverifiable`, the module made no claim — but it is
776///   the shape for non-adopters and proxied manifests, not a target for
777///   declarers; see #78.) The hazard in one sentence, for every referent and
778///   sentinel case alike: A PRESENT, WELL-FORMED FIELD STOPS THE READER
779///   ASKING — a value from the wrong domain and a sentinel from the wrong
780///   vocabulary are indistinguishable from a correct value to every check
781///   that inspects shape rather than meaning.
782/// - PROXIED MANIFESTS STAY None PERMANENTLY (CALLO): a process that
783///   forwards another machine's manifest cannot observe that build, and a
784///   forwarded provenance claim is indistinguishable on the wire from a
785///   verified one — filling it launders an unverifiable assertion. Same
786///   reasoning as pinning a re-exported module's trust_tier to Untrusted.
787///   Record that at the construction site: injection-wiring sweeps grep for
788///   `provenance:` and the obvious action at a re-export site is the wrong
789///   one.
790///
791/// `#[non_exhaustive]`: a new provenance fact must not break the modules that
792/// build this block, so construct it with [`ManifestProvenance::new`] and the
793/// `with_*` setters (or the `build_provenance*` helpers), never a struct
794/// literal.
795#[derive(Serialize, Debug, Clone, Default, PartialEq, Eq)]
796#[non_exhaustive]
797pub struct ManifestProvenance {
798    #[serde(default, skip_serializing_if = "Option::is_none")]
799    pub build_git_sha: Option<String>,
800    /// Why `build_git_sha` is unavailable. This is absent when the commit is
801    /// declared, and remains open so future causes do not make readers reject
802    /// the enclosing provenance declaration.
803    #[serde(default, skip_serializing_if = "Option::is_none")]
804    pub build_git_sha_absence_reason: Option<BuildGitShaAbsenceReason>,
805    #[serde(default, skip_serializing_if = "Option::is_none")]
806    pub build_lock_digest: Option<String>,
807    /// REFERENT: the `subc-protocol` crate version linked into this binary
808    /// (`subc_protocol::SUBC_PROTOCOL_CRATE_VERSION`) — the fleet's shared
809    /// wire vocabulary, one numbering space for every module. Never a
810    /// module's own envelope/payload crate version: that is real information
811    /// in a different numbering space, and here it scores as a confident
812    /// wrong answer at any census gate. (QTA's rule, learned live: a field
813    /// whose entire content is a referent cannot be documented by its
814    /// constraints — so the referent is stated here, where readers look.)
815    #[serde(default, skip_serializing_if = "Option::is_none")]
816    pub wire_crate_version: Option<String>,
817    #[serde(default, skip_serializing_if = "Option::is_none")]
818    pub store_schema_version: Option<String>,
819    /// Where the running module read its launch nonce from: `fd` (the pipe
820    /// the daemon hands over as descriptor 3) or `env` (the environment
821    /// variable kept while modules move to the pipe). Absent means the module
822    /// did not say. This is a fact about the running process, not the build.
823    #[serde(default, skip_serializing_if = "Option::is_none")]
824    pub launch_nonce_source: Option<LaunchNonceSource>,
825}
826
827/// Where a module read its launch nonce from, as reported in
828/// [`ManifestProvenance::launch_nonce_source`].
829///
830/// An open string enum, like [`BuildGitShaAbsenceReason`]: a value this crate
831/// does not know decodes as `ForwardCompatibleUnknown` instead of making the
832/// whole provenance block, and with it the module's HELLO, fail to decode.
833#[derive(Debug, Clone, PartialEq, Eq)]
834#[non_exhaustive]
835pub enum LaunchNonceSource {
836    /// Read from the inherited descriptor the daemon passes.
837    Fd,
838    /// Read from the `SUBC_LAUNCH_NONCE` environment variable.
839    Env,
840    ForwardCompatibleUnknown(String),
841}
842
843impl LaunchNonceSource {
844    /// The value as it appears on the wire.
845    pub fn wire_name(&self) -> &str {
846        match self {
847            Self::Fd => "fd",
848            Self::Env => "env",
849            Self::ForwardCompatibleUnknown(value) => value,
850        }
851    }
852
853    /// The source a wire value names; unknown values are kept as they are.
854    pub fn from_wire_name(value: &str) -> Self {
855        match value {
856            "fd" => Self::Fd,
857            "env" => Self::Env,
858            _ => Self::ForwardCompatibleUnknown(value.to_string()),
859        }
860    }
861}
862
863impl Serialize for LaunchNonceSource {
864    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
865    where
866        S: serde::Serializer,
867    {
868        serializer.serialize_str(self.wire_name())
869    }
870}
871
872impl<'de> Deserialize<'de> for LaunchNonceSource {
873    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
874    where
875        D: serde::Deserializer<'de>,
876    {
877        let value = String::deserialize(deserializer)?;
878        Ok(Self::from_wire_name(&value))
879    }
880}
881
882/// A build pipeline's reason for omitting `build_git_sha`.
883///
884/// This is an open string enum: consumers preserve a future reason instead of
885/// rejecting the enclosing provenance declaration.
886#[derive(Debug, Clone, PartialEq, Eq)]
887pub enum BuildGitShaAbsenceReason {
888    DeclinedDirty,
889    NeverDerived,
890    NoGitDir,
891    ForwardCompatibleUnknown(String),
892}
893
894impl BuildGitShaAbsenceReason {
895    fn wire_name(&self) -> &str {
896        match self {
897            Self::DeclinedDirty => "declined_dirty",
898            Self::NeverDerived => "never_derived",
899            Self::NoGitDir => "no_git_dir",
900            Self::ForwardCompatibleUnknown(value) => value,
901        }
902    }
903}
904
905impl Serialize for BuildGitShaAbsenceReason {
906    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
907    where
908        S: serde::Serializer,
909    {
910        serializer.serialize_str(self.wire_name())
911    }
912}
913
914impl<'de> Deserialize<'de> for BuildGitShaAbsenceReason {
915    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
916    where
917        D: serde::Deserializer<'de>,
918    {
919        let value = String::deserialize(deserializer)?;
920        Ok(match value.as_str() {
921            "declined_dirty" => Self::DeclinedDirty,
922            "never_derived" => Self::NeverDerived,
923            "no_git_dir" => Self::NoGitDir,
924            _ => Self::ForwardCompatibleUnknown(value),
925        })
926    }
927}
928
929/// The observable state of a git worktree when a build pipeline found a revision.
930#[derive(Debug, Clone, Copy, PartialEq, Eq)]
931pub enum GitTreeState {
932    Clean,
933    Dirty,
934}
935
936/// How the build pipeline obtained (or did not obtain) git revision data.
937///
938/// `NoGitDir` and `NeverDerived` carry no revision, so callers cannot attach
939/// those absence causes to an otherwise attested commit through this API.
940#[derive(Debug, Clone, Copy, PartialEq, Eq)]
941pub enum BuildGitShaSource<'a> {
942    Git {
943        revision: &'a str,
944        tree_state: GitTreeState,
945    },
946    NeverDerived,
947    NoGitDir,
948}
949
950/// Return a commit only when its source tree was clean at build time.
951///
952/// The rule is pure so stampers can exercise both branches without rebuilding.
953pub fn attestable_commit(revision: &str, tree_state: GitTreeState) -> Option<&str> {
954    match tree_state {
955        GitTreeState::Clean => Some(revision),
956        GitTreeState::Dirty => None,
957    }
958}
959
960const MAX_PROVENANCE_VALUE_BYTES: usize = 128;
961const BUILD_GIT_SHA_CANONICAL_FORM: &str = "exactly 40 lowercase hexadecimal characters";
962const BUILD_LOCK_DIGEST_CANONICAL_FORM: &str = "exactly 64 lowercase hexadecimal characters";
963
964#[derive(Deserialize)]
965struct ManifestProvenanceWire {
966    #[serde(default)]
967    build_git_sha: Option<String>,
968    #[serde(default)]
969    build_git_sha_absence_reason: Option<BuildGitShaAbsenceReason>,
970    #[serde(default)]
971    build_lock_digest: Option<String>,
972    #[serde(default)]
973    wire_crate_version: Option<String>,
974    #[serde(default)]
975    store_schema_version: Option<String>,
976    #[serde(default)]
977    launch_nonce_source: Option<LaunchNonceSource>,
978}
979
980impl<'de> Deserialize<'de> for ManifestProvenance {
981    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
982    where
983        D: Deserializer<'de>,
984    {
985        let wire = ManifestProvenanceWire::deserialize(deserializer)?;
986        let provenance = Self {
987            build_git_sha: wire.build_git_sha,
988            build_git_sha_absence_reason: wire.build_git_sha_absence_reason,
989            build_lock_digest: wire.build_lock_digest,
990            wire_crate_version: wire.wire_crate_version,
991            store_schema_version: wire.store_schema_version,
992            launch_nonce_source: wire.launch_nonce_source,
993        };
994        provenance.validate().map_err(D::Error::custom)?;
995        Ok(provenance)
996    }
997}
998
999/// A declared build fact did not use its canonical form.
1000#[derive(Debug, Clone, PartialEq, Eq)]
1001pub struct ProvenanceFormError {
1002    field: &'static str,
1003    length: usize,
1004    canonical_form: &'static str,
1005}
1006
1007impl ProvenanceFormError {
1008    fn new(field: &'static str, length: usize, canonical_form: &'static str) -> Self {
1009        Self {
1010            field,
1011            length,
1012            canonical_form,
1013        }
1014    }
1015
1016    /// The provenance field whose value was not canonical.
1017    pub fn field(&self) -> &str {
1018        self.field
1019    }
1020
1021    /// The offending value's length in bytes.
1022    pub fn length(&self) -> usize {
1023        self.length
1024    }
1025
1026    /// The canonical form required for this field.
1027    pub fn canonical_form(&self) -> &str {
1028        self.canonical_form
1029    }
1030}
1031
1032impl fmt::Display for ProvenanceFormError {
1033    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1034        write!(
1035            f,
1036            "invalid manifest provenance form: field {} has length {}; canonical form is {}",
1037            self.field, self.length, self.canonical_form
1038        )
1039    }
1040}
1041
1042impl std::error::Error for ProvenanceFormError {}
1043
1044#[derive(Debug, Clone, PartialEq, Eq)]
1045pub struct ManifestProvenanceError {
1046    field: String,
1047    value: String,
1048    reason: &'static str,
1049}
1050
1051impl ManifestProvenanceError {
1052    fn new(field: &str, value: &str, reason: &'static str) -> Self {
1053        Self {
1054            field: field.to_string(),
1055            value: safe_error_value(value),
1056            reason,
1057        }
1058    }
1059
1060    pub fn field(&self) -> &str {
1061        &self.field
1062    }
1063}
1064
1065impl fmt::Display for ManifestProvenanceError {
1066    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1067        write!(
1068            f,
1069            "invalid manifest provenance: field {} has {} (value {:?})",
1070            self.field, self.reason, self.value
1071        )
1072    }
1073}
1074
1075impl std::error::Error for ManifestProvenanceError {}
1076
1077impl ManifestProvenance {
1078    /// A block declaring nothing; add facts with the `with_*` setters.
1079    pub fn new() -> Self {
1080        Self::default()
1081    }
1082
1083    pub fn with_build_git_sha(mut self, value: Option<String>) -> Self {
1084        self.build_git_sha = value;
1085        self
1086    }
1087
1088    pub fn with_build_git_sha_absence_reason(
1089        mut self,
1090        value: Option<BuildGitShaAbsenceReason>,
1091    ) -> Self {
1092        self.build_git_sha_absence_reason = value;
1093        self
1094    }
1095
1096    pub fn with_build_lock_digest(mut self, value: Option<String>) -> Self {
1097        self.build_lock_digest = value;
1098        self
1099    }
1100
1101    pub fn with_wire_crate_version(mut self, value: Option<String>) -> Self {
1102        self.wire_crate_version = value;
1103        self
1104    }
1105
1106    pub fn with_store_schema_version(mut self, value: Option<String>) -> Self {
1107        self.store_schema_version = value;
1108        self
1109    }
1110
1111    /// Report where this process read its launch nonce from. A module takes
1112    /// it from its nonce accessor's cached source, so whoever reads the fleet's
1113    /// provenance (`ck provenance`) can tell a module reading the pipe from
1114    /// one still reading the environment variable.
1115    pub fn with_launch_nonce_source(mut self, value: Option<LaunchNonceSource>) -> Self {
1116        self.launch_nonce_source = value;
1117        self
1118    }
1119
1120    pub fn validate(&self) -> Result<(), ManifestProvenanceError> {
1121        if let (Some(_), Some(reason)) = (
1122            self.build_git_sha.as_ref(),
1123            self.build_git_sha_absence_reason.as_ref(),
1124        ) {
1125            return Err(ManifestProvenanceError::new(
1126                "build_git_sha_absence_reason",
1127                reason.wire_name(),
1128                "must be omitted when build_git_sha is present",
1129            ));
1130        }
1131        for (field, value) in [
1132            ("build_git_sha", self.build_git_sha.as_deref()),
1133            (
1134                "build_git_sha_absence_reason",
1135                self.build_git_sha_absence_reason
1136                    .as_ref()
1137                    .map(|reason| reason.wire_name()),
1138            ),
1139            ("build_lock_digest", self.build_lock_digest.as_deref()),
1140            ("wire_crate_version", self.wire_crate_version.as_deref()),
1141            ("store_schema_version", self.store_schema_version.as_deref()),
1142            (
1143                "launch_nonce_source",
1144                self.launch_nonce_source
1145                    .as_ref()
1146                    .map(|source| source.wire_name()),
1147            ),
1148        ] {
1149            let Some(value) = value else { continue };
1150            if value.is_empty() {
1151                return Err(ManifestProvenanceError::new(
1152                    field,
1153                    value,
1154                    "must not be empty",
1155                ));
1156            }
1157            // HELLO decoding checks only wire safety here. Canonical build forms
1158            // belong to build_provenance; the daemon is a non-adjudicating relayer
1159            // and must continue accepting legacy declarations such as 12-hex
1160            // module revisions rather than breaking a fleet on daemon upgrade.
1161            if value.len() > MAX_PROVENANCE_VALUE_BYTES {
1162                return Err(ManifestProvenanceError::new(
1163                    field,
1164                    value,
1165                    "exceeds the 128-byte maximum",
1166                ));
1167            }
1168            if value.bytes().any(|byte| !(0x20..=0x7e).contains(&byte)) {
1169                return Err(ManifestProvenanceError::new(
1170                    field,
1171                    value,
1172                    "contains non-printable ASCII",
1173                ));
1174            }
1175        }
1176        Ok(())
1177    }
1178}
1179
1180/// Build [`ManifestProvenance`] from legacy raw build facts.
1181///
1182/// Callers of this compatibility path did not supply the tree state that
1183/// explains an omitted SHA. It emits no absence reason not because the absence
1184/// has no cause, but because guessing one without that state would fabricate
1185/// the fact this API exists to report honestly.
1186///
1187/// ```
1188/// use subc_protocol::manifest::build_provenance;
1189///
1190/// let provenance = build_provenance(option_env!("CK_BUILD_REV"), None, None)
1191///     .expect("legacy build facts remain supported");
1192/// assert!(provenance.build_git_sha_absence_reason.is_none());
1193/// ```
1194///
1195/// Sentinel and empty values become field omission before canonical form
1196/// validation, preserving the established three-argument wire behavior.
1197pub fn build_provenance(
1198    build_git_sha: Option<&str>,
1199    build_lock_digest: Option<&str>,
1200    store_schema_version: Option<&str>,
1201) -> Result<ManifestProvenance, ProvenanceFormError> {
1202    let build_git_sha = normalize_and_validate_build_git_sha(build_git_sha)?;
1203    build_provenance_with_build_git_sha(
1204        build_git_sha,
1205        None,
1206        build_lock_digest,
1207        store_schema_version,
1208    )
1209}
1210
1211/// Build a [`ManifestProvenance`] from source-state-aware build facts.
1212///
1213/// The source state makes the SHA absence cause attestable: `Dirty` declines
1214/// the commit, while `NeverDerived` and `NoGitDir` name distinct source paths.
1215/// A `build_git_sha` must be exactly 40 lowercase hexadecimal characters and a
1216/// `build_lock_digest` exactly 64 lowercase hexadecimal characters. Abbreviations
1217/// are not conforming; a real value in the wrong form returns a
1218/// [`ProvenanceFormError`] instead of being discarded as if it were absent.
1219/// Sentinel values are filtered before form validation, so they remain honest
1220/// omission rather than becoming form errors.
1221///
1222/// OWNERSHIP RULE: a helper that constructs a wire type lives in the crate
1223/// that owns the type. This helper constructs `ManifestProvenance`, so it
1224/// lives here in subc-protocol (not in subc-client-rs) — transport-direct
1225/// modules that never link the client SDK can still build honest provenance.
1226pub fn build_provenance_from_source(
1227    build_git_sha_source: BuildGitShaSource<'_>,
1228    build_lock_digest: Option<&str>,
1229    store_schema_version: Option<&str>,
1230) -> Result<ManifestProvenance, ProvenanceFormError> {
1231    let (raw_build_git_sha, mut build_git_sha_absence_reason) = match build_git_sha_source {
1232        BuildGitShaSource::Git {
1233            revision,
1234            tree_state,
1235        } => match attestable_commit(revision, tree_state) {
1236            Some(revision) => (Some(revision), None),
1237            None => (None, Some(BuildGitShaAbsenceReason::DeclinedDirty)),
1238        },
1239        BuildGitShaSource::NeverDerived => (None, Some(BuildGitShaAbsenceReason::NeverDerived)),
1240        BuildGitShaSource::NoGitDir => (None, Some(BuildGitShaAbsenceReason::NoGitDir)),
1241    };
1242    let build_git_sha = normalize_and_validate_build_git_sha(raw_build_git_sha)?;
1243    if build_git_sha.is_none() {
1244        build_git_sha_absence_reason.get_or_insert(BuildGitShaAbsenceReason::NeverDerived);
1245    }
1246    build_provenance_with_build_git_sha(
1247        build_git_sha,
1248        build_git_sha_absence_reason,
1249        build_lock_digest,
1250        store_schema_version,
1251    )
1252}
1253
1254fn normalize_and_validate_build_git_sha(
1255    build_git_sha: Option<&str>,
1256) -> Result<Option<String>, ProvenanceFormError> {
1257    let build_git_sha = normalize_provenance_fact(build_git_sha);
1258    validate_provenance_form(
1259        "build_git_sha",
1260        build_git_sha.as_deref(),
1261        BUILD_GIT_SHA_CANONICAL_FORM,
1262        40,
1263    )?;
1264    Ok(build_git_sha)
1265}
1266
1267fn build_provenance_with_build_git_sha(
1268    build_git_sha: Option<String>,
1269    build_git_sha_absence_reason: Option<BuildGitShaAbsenceReason>,
1270    build_lock_digest: Option<&str>,
1271    store_schema_version: Option<&str>,
1272) -> Result<ManifestProvenance, ProvenanceFormError> {
1273    let build_lock_digest = normalize_provenance_fact(build_lock_digest);
1274    validate_provenance_form(
1275        "build_lock_digest",
1276        build_lock_digest.as_deref(),
1277        BUILD_LOCK_DIGEST_CANONICAL_FORM,
1278        64,
1279    )?;
1280
1281    Ok(ManifestProvenance {
1282        build_git_sha,
1283        build_git_sha_absence_reason,
1284        build_lock_digest,
1285        wire_crate_version: Some(crate::SUBC_PROTOCOL_CRATE_VERSION.to_string()),
1286        store_schema_version: normalize_provenance_fact(store_schema_version),
1287        launch_nonce_source: None,
1288    })
1289}
1290
1291fn validate_provenance_form(
1292    field: &'static str,
1293    value: Option<&str>,
1294    canonical_form: &'static str,
1295    expected_length: usize,
1296) -> Result<(), ProvenanceFormError> {
1297    let Some(value) = value else { return Ok(()) };
1298    if value.len() != expected_length
1299        || !value
1300            .bytes()
1301            .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
1302    {
1303        return Err(ProvenanceFormError::new(field, value.len(), canonical_form));
1304    }
1305    Ok(())
1306}
1307
1308/// Sentinel strings that build tooling emits where it means "no value": shell
1309/// fallbacks and Makefile defaults produce `unknown`, wire vocabulary uses
1310/// `unavailable`, and `git describe` failures surface as `none`. Publishing
1311/// any of them as a fact is the well-formed-lie shape the provenance contract
1312/// warns against — a present, well-formed field stops the reader asking — so
1313/// the helper maps them all to field omission. Matched case-insensitively
1314/// because `UNKNOWN`/`Unknown` are equally common from shell fallbacks.
1315pub const PROVENANCE_SENTINELS: [&str; 3] = ["unknown", "unavailable", "none"];
1316
1317fn normalize_provenance_fact(value: Option<&str>) -> Option<String> {
1318    let value = value?.trim();
1319    if value.is_empty() {
1320        return None;
1321    }
1322    let lowered = value.to_ascii_lowercase();
1323    if PROVENANCE_SENTINELS.contains(&lowered.as_str()) {
1324        return None;
1325    }
1326    Some(value.to_string())
1327}
1328
1329/// One capability a module consumes and whether its absence is tolerated.
1330#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1331#[serde(deny_unknown_fields)]
1332pub struct CapabilityRequirement {
1333    pub capability: String,
1334    pub need: CapabilityNeed,
1335}
1336
1337/// Closed capability requirement strength vocabulary.
1338#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1339#[serde(rename_all = "snake_case")]
1340pub enum CapabilityNeed {
1341    Required,
1342    Optional,
1343}
1344
1345/// A safe-to-report capability-schema validation failure.
1346#[derive(Debug, Clone, PartialEq, Eq)]
1347pub struct CapabilityGrammarError {
1348    field: String,
1349    value: String,
1350}
1351
1352impl CapabilityGrammarError {
1353    fn new(field: impl Into<String>, value: impl AsRef<str>) -> Self {
1354        Self {
1355            field: field.into(),
1356            value: safe_error_value(value.as_ref()),
1357        }
1358    }
1359
1360    /// The precise malformed field path.
1361    pub fn field(&self) -> &str {
1362        &self.field
1363    }
1364
1365    /// The offending value, redacted when it resembles a credential.
1366    pub fn value(&self) -> &str {
1367        &self.value
1368    }
1369}
1370
1371impl fmt::Display for CapabilityGrammarError {
1372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1373        write!(
1374            f,
1375            "invalid capability grammar: field {} has offending value {:?}",
1376            self.field, self.value
1377        )
1378    }
1379}
1380
1381impl std::error::Error for CapabilityGrammarError {}
1382
1383impl ModuleManifest {
1384    /// Validate the typed capability block after serde has decoded it.
1385    pub fn validate_capability_grammar(&self) -> Result<(), CapabilityGrammarError> {
1386        let Some(capabilities) = &self.capabilities else {
1387            return Ok(());
1388        };
1389
1390        validate_capability_list("capabilities.provides", &capabilities.provides)?;
1391        validate_requires(&capabilities.requires)?;
1392        validate_capability_list(
1393            "capabilities.must_never_reach",
1394            &capabilities.must_never_reach,
1395        )
1396    }
1397}
1398
1399/// Validate capability grammar in a standalone manifest JSON value.
1400///
1401/// The raw-value form lets HELLO distinguish schema failures from malformed JSON,
1402/// including an unknown `need` that cannot be represented by [`CapabilityNeed`].
1403pub fn validate_manifest_capability_grammar(
1404    manifest: &Value,
1405) -> Result<(), CapabilityGrammarError> {
1406    let Some(object) = manifest.as_object() else {
1407        return Ok(());
1408    };
1409
1410    validate_capabilities_value(object.get("capabilities"))?;
1411    validate_runtime_computed(object.get("runtime_computed"), "runtime_computed")
1412}
1413
1414/// Validate capability grammar in a raw HELLO body.
1415///
1416/// `runtime_computed` is a top-level sibling in --manifest output. HELLO keeps
1417/// accepting that sibling only so an attempted dynamic capability declaration is
1418/// refused explicitly instead of being silently ignored by serde.
1419pub fn validate_hello_capability_grammar(hello: &Value) -> Result<(), CapabilityGrammarError> {
1420    let Some(object) = hello.as_object() else {
1421        return Ok(());
1422    };
1423    if let Some(manifest) = object.get("manifest") {
1424        validate_manifest_capability_grammar(manifest)?;
1425    }
1426    validate_runtime_computed(object.get("runtime_computed"), "runtime_computed")
1427}
1428
1429/// Return whether `identifier` has the exact `<name>/v<N>` capability spelling.
1430pub fn is_valid_capability_identifier(identifier: &str) -> bool {
1431    if identifier.chars().any(char::is_whitespace) {
1432        return false;
1433    }
1434    let Some((name, version)) = identifier.split_once("/v") else {
1435        return false;
1436    };
1437    if name.is_empty() || name.len() > 64 || version.is_empty() {
1438        return false;
1439    }
1440
1441    let name_bytes = name.as_bytes();
1442    if !name_bytes[0].is_ascii_lowercase()
1443        || (name.len() > 1
1444            && !name_bytes[name.len() - 1].is_ascii_lowercase()
1445            && !name_bytes[name.len() - 1].is_ascii_digit())
1446        || name_bytes.windows(2).any(|pair| pair == b"--")
1447    {
1448        return false;
1449    }
1450    if !name_bytes
1451        .iter()
1452        .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
1453    {
1454        return false;
1455    }
1456
1457    if version.len() > 1 && version.starts_with('0')
1458        || !version.bytes().all(|byte| byte.is_ascii_digit())
1459    {
1460        return false;
1461    }
1462    matches!(
1463        version.parse::<u64>(),
1464        Ok(value) if (1..=u64::from(u32::MAX)).contains(&value)
1465    )
1466}
1467
1468fn validate_capabilities_value(value: Option<&Value>) -> Result<(), CapabilityGrammarError> {
1469    let Some(value) = value else {
1470        return Ok(());
1471    };
1472    let Some(object) = value.as_object() else {
1473        return Err(CapabilityGrammarError::new(
1474            "capabilities",
1475            value_description(value),
1476        ));
1477    };
1478
1479    for (key, value) in object {
1480        if !matches!(key.as_str(), "provides" | "requires" | "must_never_reach") {
1481            return Err(CapabilityGrammarError::new(
1482                field_child("capabilities", key),
1483                value_description(value),
1484            ));
1485        }
1486    }
1487
1488    validate_capability_list_value("capabilities.provides", object.get("provides"))?;
1489    validate_requires_value(object.get("requires"))?;
1490    validate_capability_list_value(
1491        "capabilities.must_never_reach",
1492        object.get("must_never_reach"),
1493    )
1494}
1495
1496fn validate_capability_list_value(
1497    field: &str,
1498    value: Option<&Value>,
1499) -> Result<(), CapabilityGrammarError> {
1500    let Some(value) = value else {
1501        return Ok(());
1502    };
1503    let Some(values) = value.as_array() else {
1504        return Err(CapabilityGrammarError::new(field, value_description(value)));
1505    };
1506
1507    let mut seen = HashSet::new();
1508    for (index, value) in values.iter().enumerate() {
1509        let field = format!("{field}[{index}]");
1510        let Some(identifier) = value.as_str() else {
1511            return Err(CapabilityGrammarError::new(field, value_description(value)));
1512        };
1513        validate_capability_identifier(&field, identifier)?;
1514        if !seen.insert(identifier) {
1515            return Err(CapabilityGrammarError::new(field, identifier));
1516        }
1517    }
1518    Ok(())
1519}
1520
1521fn validate_requires_value(value: Option<&Value>) -> Result<(), CapabilityGrammarError> {
1522    let Some(value) = value else {
1523        return Ok(());
1524    };
1525    let Some(values) = value.as_array() else {
1526        return Err(CapabilityGrammarError::new(
1527            "capabilities.requires",
1528            value_description(value),
1529        ));
1530    };
1531
1532    let mut seen = HashSet::new();
1533    for (index, value) in values.iter().enumerate() {
1534        let entry_field = format!("capabilities.requires[{index}]");
1535        let Some(object) = value.as_object() else {
1536            return Err(CapabilityGrammarError::new(
1537                entry_field,
1538                value_description(value),
1539            ));
1540        };
1541        for (key, value) in object {
1542            if !matches!(key.as_str(), "capability" | "need") {
1543                return Err(CapabilityGrammarError::new(
1544                    field_child(&entry_field, key),
1545                    value_description(value),
1546                ));
1547            }
1548        }
1549        let capability_field = format!("{entry_field}.capability");
1550        let Some(capability) = object.get("capability").and_then(Value::as_str) else {
1551            return Err(CapabilityGrammarError::new(
1552                capability_field,
1553                object
1554                    .get("capability")
1555                    .map_or("<missing>".to_string(), value_description),
1556            ));
1557        };
1558        validate_capability_identifier(&capability_field, capability)?;
1559
1560        let need_field = format!("{entry_field}.need");
1561        let Some(need) = object.get("need").and_then(Value::as_str) else {
1562            return Err(CapabilityGrammarError::new(
1563                need_field,
1564                object
1565                    .get("need")
1566                    .map_or("<missing>".to_string(), value_description),
1567            ));
1568        };
1569        if !matches!(need, "required" | "optional") {
1570            return Err(CapabilityGrammarError::new(need_field, need));
1571        }
1572        if !seen.insert(capability) {
1573            return Err(CapabilityGrammarError::new(entry_field, capability));
1574        }
1575    }
1576    Ok(())
1577}
1578
1579fn validate_capability_list(field: &str, values: &[String]) -> Result<(), CapabilityGrammarError> {
1580    let mut seen = HashSet::new();
1581    for (index, identifier) in values.iter().enumerate() {
1582        let field = format!("{field}[{index}]");
1583        validate_capability_identifier(&field, identifier)?;
1584        if !seen.insert(identifier) {
1585            return Err(CapabilityGrammarError::new(field, identifier));
1586        }
1587    }
1588    Ok(())
1589}
1590
1591fn validate_requires(values: &[CapabilityRequirement]) -> Result<(), CapabilityGrammarError> {
1592    let mut seen = HashSet::new();
1593    for (index, requirement) in values.iter().enumerate() {
1594        let field = format!("capabilities.requires[{index}].capability");
1595        validate_capability_identifier(&field, &requirement.capability)?;
1596        if !seen.insert(&requirement.capability) {
1597            return Err(CapabilityGrammarError::new(
1598                format!("capabilities.requires[{index}]"),
1599                &requirement.capability,
1600            ));
1601        }
1602    }
1603    Ok(())
1604}
1605
1606fn validate_capability_identifier(
1607    field: &str,
1608    identifier: &str,
1609) -> Result<(), CapabilityGrammarError> {
1610    if is_valid_capability_identifier(identifier) {
1611        Ok(())
1612    } else {
1613        Err(CapabilityGrammarError::new(field, identifier))
1614    }
1615}
1616
1617fn validate_runtime_computed(
1618    value: Option<&Value>,
1619    field: &str,
1620) -> Result<(), CapabilityGrammarError> {
1621    let Some(value) = value else {
1622        return Ok(());
1623    };
1624    let Some(pointers) = value.as_array() else {
1625        return Err(CapabilityGrammarError::new(field, value_description(value)));
1626    };
1627
1628    for (index, pointer) in pointers.iter().enumerate() {
1629        let field = format!("{field}[{index}]");
1630        let Some(pointer) = pointer.as_str() else {
1631            return Err(CapabilityGrammarError::new(
1632                field,
1633                value_description(pointer),
1634            ));
1635        };
1636        let Some(tokens) = parse_json_pointer(pointer) else {
1637            return Err(CapabilityGrammarError::new(field, pointer));
1638        };
1639        if tokens.first().is_some_and(|token| token == "capabilities") {
1640            return Err(CapabilityGrammarError::new(field, pointer));
1641        }
1642    }
1643    Ok(())
1644}
1645
1646fn parse_json_pointer(pointer: &str) -> Option<Vec<String>> {
1647    if pointer.is_empty() {
1648        return Some(Vec::new());
1649    }
1650    let raw_tokens = pointer.strip_prefix('/')?;
1651    raw_tokens
1652        .split('/')
1653        .map(unescape_json_pointer_token)
1654        .collect()
1655}
1656
1657fn unescape_json_pointer_token(token: &str) -> Option<String> {
1658    let mut output = String::with_capacity(token.len());
1659    let mut characters = token.chars();
1660    while let Some(character) = characters.next() {
1661        if character != '~' {
1662            output.push(character);
1663            continue;
1664        }
1665        match characters.next()? {
1666            '0' => output.push('~'),
1667            '1' => output.push('/'),
1668            _ => return None,
1669        }
1670    }
1671    Some(output)
1672}
1673
1674fn field_child(parent: &str, child: &str) -> String {
1675    let child = safe_error_value(child);
1676    format!("{parent}.{child}")
1677}
1678
1679fn value_description(value: &Value) -> String {
1680    match value {
1681        Value::String(value) => safe_error_value(value),
1682        Value::Null => "null".to_string(),
1683        Value::Bool(value) => value.to_string(),
1684        Value::Number(value) => value.to_string(),
1685        Value::Array(_) => "<array>".to_string(),
1686        Value::Object(_) => "<object>".to_string(),
1687    }
1688}
1689
1690fn safe_error_value(value: &str) -> String {
1691    let lower = value.to_ascii_lowercase();
1692    if ["secret", "password", "api_key"]
1693        .iter()
1694        .any(|marker| lower.contains(marker))
1695        || lower.starts_with("sk-")
1696        || lower.starts_with("akia")
1697        || lower.starts_with("bearer ")
1698        || lower.starts_with("token=")
1699        || lower.starts_with("credential=")
1700    {
1701        "<redacted>".to_string()
1702    } else {
1703        value.to_string()
1704    }
1705}
1706
1707/// How this module was sourced, as declared by the module itself.
1708///
1709/// Not read on any daemon routing or admission path; relayed verbatim. A
1710/// module declares it because it describes the module, not because the
1711/// daemon consumes it, and leaves it absent rather than inventing a value.
1712#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1713#[serde(rename_all = "snake_case")]
1714pub enum TrustTier {
1715    FirstParty,
1716    Reviewed,
1717    Untrusted,
1718}
1719
1720/// Provider capabilities exposed by a module.
1721///
1722/// The role set is closed for protocol v1; unknown role tags fail serde decode.
1723#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1724#[serde(tag = "role", rename_all = "snake_case")]
1725pub enum ProviderRole {
1726    ToolProvider {
1727        tools: Vec<Tool>,
1728        /// Which `BindIdentity` keys PARTITION this provider's state or
1729        /// answers: a module whose reply to a call depends on the caller's
1730        /// project declares `Project`; one that threads per session declares
1731        /// `Session`; one that answers identically to every caller declares
1732        /// `[]`. It states what the module does with the keys it is handed,
1733        /// not which keys it will accept — every bind carries all of them.
1734        /// Not read on any daemon path; relayed verbatim for consumers.
1735        identity_scope: Vec<IdentityScope>,
1736        concurrency: Concurrency,
1737        emits_push: bool,
1738        sub_supervises: bool,
1739    },
1740    PipelineStage {
1741        stage: PipelineStageKind,
1742        applies_to: PipelineAppliesTo,
1743        interface: String,
1744        declares_frozen_floor: bool,
1745        needs_signals: Vec<String>,
1746        conformance_class: String,
1747    },
1748    ManagementSurface {
1749        operations: Vec<ManagementOperation>,
1750        config_schema: Value,
1751        observability: Vec<ObservabilitySurface>,
1752        /// Same meaning as on `ToolProvider`: the keys that partition this
1753        /// surface's state or answers; `[]` for a surface that serves the
1754        /// same answer to every caller.
1755        identity_scope: Vec<IdentityScope>,
1756        #[serde(default)]
1757        concurrency: Concurrency,
1758    },
1759    InternalService {
1760        service_id: String,
1761        transport: InternalTransport,
1762        agent_facing: bool,
1763        operations: Vec<String>,
1764    },
1765}
1766
1767/// How a tool's side effects are fenced for durable at-most-once handling.
1768///
1769/// Classified on a tool's externally-observable effects, never inferred from
1770/// the module's concurrency lane:
1771/// - `Pure`: no observable side effect (reads, searches, cache warming) — safe
1772///   to re-run after an indeterminate outcome.
1773/// - `Mutating`: a fenceable external side effect such as a file write — a
1774///   re-run risks a duplicate effect, so an indeterminate outcome must not
1775///   auto-retry.
1776/// - `Unfenceable`: a side effect that cannot be fenced or safely replayed,
1777///   such as running a shell command — never auto-re-run on an indeterminate
1778///   outcome.
1779#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1780#[serde(rename_all = "snake_case")]
1781pub enum ExecutionMode {
1782    Pure,
1783    Mutating,
1784    Unfenceable,
1785}
1786
1787/// Tool-plane capability exposed by a `tool_provider`.
1788#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1789pub struct Tool {
1790    pub name: String,
1791    #[serde(default, skip_serializing_if = "Option::is_none")]
1792    pub description: Option<String>,
1793    /// How the tool's side effects are fenced for durable at-most-once handling.
1794    /// Observability + durability metadata only; subc's thin core never acts on
1795    /// this for routing, scheduling, or concurrency — the module's declared
1796    /// [`Concurrency`] contract governs delivery.
1797    pub execution_mode: ExecutionMode,
1798    pub schema: Value,
1799}
1800
1801/// How subc may deliver concurrent in-flight calls to the provider.
1802///
1803/// subc records and forwards these semantics unchanged; the dispatcher that
1804/// enforces them lives in subc-core, kept separate from this frozen manifest
1805/// contract.
1806#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1807#[serde(rename_all = "snake_case")]
1808pub enum Concurrency {
1809    /// One in-flight call at a time with strict submission and response order.
1810    Serial,
1811    /// Concurrent in-flight calls may span channels, while subc preserves FIFO
1812    /// submission within each channel; the module schedules internally.
1813    ModuleManaged,
1814    /// Fully parallel delivery with no ordering guarantee across or within
1815    /// channels.
1816    StatelessParallel,
1817}
1818
1819#[allow(clippy::derivable_impls)]
1820// The default is PINNED BY HISTORY, not chosen as the best value. Before this
1821// field existed, every ManagementSurface received ModuleManaged delivery (32
1822// concurrent credits) unconditionally, so an absent-field manifest must resolve
1823// to exactly that behavior -- any other default (including the fail-closed
1824// Serial) would convert a daemon upgrade into a silent delivery-semantics
1825// change for every deployed module. A genuinely-Serial module was ALREADY
1826// receiving concurrent delivery under pre-field daemons; the field's addition
1827// is what makes declaring Serial possible at all, so the fix for such a module
1828// is an explicit declaration, and the daemon logs defaulted registrations so
1829// the fleet's exposure is readable rather than assumed.
1830impl Default for Concurrency {
1831    fn default() -> Self {
1832        Self::ModuleManaged
1833    }
1834}
1835
1836/// A `BindIdentity` key a provider partitions its state or answers by.
1837///
1838/// Declared in a role's `identity_scope` to say which caller keys change
1839/// what the module does; the daemon hands every bind all of the keys
1840/// regardless, so an empty declaration means "answers do not depend on the
1841/// caller", never "keys are refused".
1842#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1843#[serde(rename_all = "snake_case")]
1844pub enum IdentityScope {
1845    Session,
1846    Project,
1847}
1848
1849/// Proxy-plane stage kind.
1850#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1851#[serde(rename_all = "snake_case")]
1852pub enum PipelineStageKind {
1853    Transform,
1854    Codec,
1855    Auth,
1856}
1857
1858/// Provider/model selector for a pipeline stage. `"*"` denotes wildcard.
1859#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1860pub struct PipelineAppliesTo {
1861    pub provider: String,
1862    pub model: String,
1863}
1864
1865/// Operation exposed on the management plane.
1866#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1867pub struct ManagementOperation {
1868    pub name: String,
1869    pub kind: ManagementOperationKind,
1870    #[serde(default, skip_serializing_if = "Option::is_none")]
1871    pub description: Option<String>,
1872}
1873
1874#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1875#[serde(rename_all = "snake_case")]
1876pub enum ManagementOperationKind {
1877    Query,
1878    Mutate,
1879}
1880
1881/// Observable state exposed on the management plane.
1882#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1883pub struct ObservabilitySurface {
1884    pub name: String,
1885    pub kind: ObservabilityKind,
1886}
1887
1888#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1889#[serde(rename_all = "snake_case")]
1890pub enum ObservabilityKind {
1891    Snapshot,
1892    Stream,
1893}
1894
1895#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1896#[serde(rename_all = "snake_case")]
1897pub enum InternalTransport {
1898    Bulk,
1899}
1900
1901/// Consumer capabilities requested by a module.
1902#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1903#[serde(tag = "role", rename_all = "snake_case")]
1904pub enum ConsumerRole {
1905    ToolClient { of: Vec<String> },
1906    LlmClient { via: String, auth: String },
1907    ServiceClient { of: Vec<String> },
1908}
1909
1910/// External storage, vault, and identity bindings supplied through subc.
1911#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1912pub struct Bindings {
1913    pub storage: StorageBinding,
1914    pub vault_grants: Vec<VaultGrant>,
1915    pub identity: IdentityBinding,
1916}
1917
1918/// Storage backend supplied by subc; the module owns its schema.
1919#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1920pub struct StorageBinding {
1921    pub kind: StorageKind,
1922    pub scope: StorageScope,
1923    pub owns_schema: bool,
1924}
1925
1926#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1927#[serde(rename_all = "snake_case")]
1928pub enum StorageKind {
1929    Sqlite,
1930}
1931
1932#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1933#[serde(rename_all = "snake_case")]
1934pub enum StorageScope {
1935    Project,
1936}
1937
1938#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1939pub struct VaultGrant {
1940    pub secret: String,
1941    pub reason: String,
1942}
1943
1944#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1945pub struct IdentityBinding {
1946    pub requires: Vec<IdentityScope>,
1947    pub optional: Vec<IdentityScope>,
1948}
1949
1950#[cfg(test)]
1951mod tests {
1952    use super::*;
1953    use serde_json::json;
1954
1955    fn aft_manifest_fixture() -> ModuleManifest {
1956        ModuleManifest::builder("aft", "0.39.2")
1957            .trust_tier(Some(TrustTier::FirstParty))
1958            .bindings(Some(Bindings {
1959                storage: StorageBinding {
1960                    kind: StorageKind::Sqlite,
1961                    scope: StorageScope::Project,
1962                    owns_schema: true,
1963                },
1964                vault_grants: vec![VaultGrant {
1965                    secret: "provider_api_key".to_string(),
1966                    reason: "cortexkit_native auth".to_string(),
1967                }],
1968                identity: IdentityBinding {
1969                    requires: vec![IdentityScope::Project],
1970                    optional: vec![IdentityScope::Session],
1971                },
1972            }))
1973            .protocol_ver(1)
1974            .provides(vec![ProviderRole::ToolProvider {
1975                tools: vec![
1976                    Tool {
1977                        name: "read".to_string(),
1978                        description: None,
1979                        execution_mode: ExecutionMode::Pure,
1980                        schema: json!({"type": "object"}),
1981                    },
1982                    Tool {
1983                        name: "grep".to_string(),
1984                        description: None,
1985                        execution_mode: ExecutionMode::Pure,
1986                        schema: json!({"type": "object"}),
1987                    },
1988                    Tool {
1989                        name: "outline".to_string(),
1990                        description: None,
1991                        execution_mode: ExecutionMode::Pure,
1992                        schema: json!({"type": "object"}),
1993                    },
1994                    Tool {
1995                        name: "semantic_search".to_string(),
1996                        description: None,
1997                        execution_mode: ExecutionMode::Pure,
1998                        schema: json!({"type": "object"}),
1999                    },
2000                    Tool {
2001                        name: "edit".to_string(),
2002                        description: None,
2003                        execution_mode: ExecutionMode::Mutating,
2004                        schema: json!({"type": "object"}),
2005                    },
2006                    Tool {
2007                        name: "write".to_string(),
2008                        description: None,
2009                        execution_mode: ExecutionMode::Mutating,
2010                        schema: json!({"type": "object"}),
2011                    },
2012                    Tool {
2013                        name: "bash".to_string(),
2014                        description: None,
2015                        execution_mode: ExecutionMode::Unfenceable,
2016                        schema: json!({"type": "object"}),
2017                    },
2018                ],
2019                identity_scope: vec![IdentityScope::Session, IdentityScope::Project],
2020                concurrency: Concurrency::ModuleManaged,
2021                emits_push: true,
2022                sub_supervises: true,
2023            }])
2024            .consumes(vec![ConsumerRole::ServiceClient {
2025                of: vec!["embedding.v2".to_string()],
2026            }])
2027            .build()
2028    }
2029
2030    #[test]
2031    fn serde_round_trips_representative_manifest() {
2032        let manifest = aft_manifest_fixture();
2033        let serialized = serde_json::to_string_pretty(&manifest).unwrap();
2034        let decoded: ModuleManifest = serde_json::from_str(&serialized).unwrap();
2035
2036        assert_eq!(manifest, decoded);
2037    }
2038
2039    #[test]
2040    fn builder_defaults_additions_to_honest_absence_and_round_trips() {
2041        let manifest = ModuleManifest::builder("builder-defaults", "2.0.0").build();
2042
2043        assert_eq!(manifest.module_id, "builder-defaults");
2044        assert_eq!(manifest.module_version, "2.0.0");
2045        assert_eq!(manifest.protocol_ver, PROTOCOL_VERSION);
2046        assert_eq!(manifest.trust_tier, None);
2047        assert!(manifest.provides.is_empty());
2048        assert!(manifest.consumes.is_empty());
2049        assert_eq!(manifest.bindings, None);
2050        assert_eq!(manifest.capabilities, None);
2051        assert_eq!(manifest.self_signals, None);
2052        assert_eq!(manifest.provenance, None);
2053
2054        let encoded = serde_json::to_value(&manifest).expect("builder manifest serializes");
2055        for optional in [
2056            "trust_tier",
2057            "consumes",
2058            "bindings",
2059            "capabilities",
2060            "self_signals",
2061            "provenance",
2062        ] {
2063            assert!(
2064                encoded.get(optional).is_none(),
2065                "an absent {optional} declaration must stay absent on the wire"
2066            );
2067        }
2068        let decoded: ModuleManifest =
2069            serde_json::from_value(encoded).expect("builder manifest round-trips");
2070        assert_eq!(decoded, manifest);
2071    }
2072
2073    #[test]
2074    fn fully_populated_builder_manifest_matches_the_literal_wire_golden() {
2075        let manifest = ModuleManifest::builder("full-builder", "2.0.0")
2076            .trust_tier(Some(TrustTier::Reviewed))
2077            .bindings(Some(Bindings {
2078                storage: StorageBinding {
2079                    kind: StorageKind::Sqlite,
2080                    scope: StorageScope::Project,
2081                    owns_schema: false,
2082                },
2083                vault_grants: Vec::new(),
2084                identity: IdentityBinding {
2085                    requires: vec![IdentityScope::Project],
2086                    optional: Vec::new(),
2087                },
2088            }))
2089            .provides(vec![ProviderRole::ToolProvider {
2090                tools: vec![Tool {
2091                    name: "read".to_string(),
2092                    description: None,
2093                    execution_mode: ExecutionMode::Pure,
2094                    schema: json!({"type": "object"}),
2095                }],
2096                identity_scope: vec![IdentityScope::Project],
2097                concurrency: Concurrency::Serial,
2098                emits_push: false,
2099                sub_supervises: false,
2100            }])
2101            .consumes(vec![ConsumerRole::ServiceClient {
2102                of: vec!["embedding.v2".to_string()],
2103            }])
2104            .capabilities(Some(CapabilityDeclarations {
2105                provides: vec!["embedding/v2".to_string()],
2106                requires: Vec::new(),
2107                must_never_reach: Vec::new(),
2108            }))
2109            .self_signals(Some(vec![SelfSignalDeclaration {
2110                name: "usage_poller".to_string(),
2111                kind: SelfSignalKind::Poller,
2112                effect: SelfSignalEffect::Observe,
2113                anchored_to: SignalAnchor::FixedInterval,
2114                cadence: Some(SignalCadence::Literal {
2115                    interval_ms: 60_000,
2116                }),
2117                domain: Some("provider-usage".to_string()),
2118                note: None,
2119            }]))
2120            .provenance(Some(ManifestProvenance {
2121                build_git_sha: Some("0123456789abcdef0123456789abcdef01234567".to_string()),
2122                build_git_sha_absence_reason: None,
2123                build_lock_digest: Some(
2124                    "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(),
2125                ),
2126                wire_crate_version: Some("0.16.0".to_string()),
2127                store_schema_version: Some("42".to_string()),
2128                launch_nonce_source: None,
2129            }))
2130            .build();
2131
2132        assert_eq!(
2133            serde_json::to_vec(&manifest).expect("builder manifest serializes"),
2134            include_bytes!("../tests/golden/module_manifest_builder_full.json"),
2135            "the builder must preserve the prior fully populated literal wire bytes"
2136        );
2137    }
2138
2139    #[test]
2140    fn old_manifest_with_unread_fields_decodes_and_round_trips_verbatim() {
2141        let raw = include_bytes!("../tests/golden/module_manifest_builder_full.json");
2142        let decoded: ModuleManifest =
2143            serde_json::from_slice(raw).expect("old manifest with all unread fields decodes");
2144
2145        assert_eq!(decoded.trust_tier, Some(TrustTier::Reviewed));
2146        assert!(!decoded.consumes.is_empty());
2147        assert!(decoded.bindings.is_some());
2148
2149        let reencoded = serde_json::to_vec(&decoded).expect("re-encode succeeds");
2150        assert_eq!(
2151            reencoded, raw,
2152            "old manifest relay stays byte-for-byte verbatim"
2153        );
2154    }
2155
2156    #[test]
2157    fn new_manifest_omits_unread_fields_on_wire_and_decodes_cleanly() {
2158        let raw = include_bytes!("../tests/golden/module_manifest_diet.json");
2159        let decoded: ModuleManifest =
2160            serde_json::from_slice(raw).expect("new manifest omitting unread fields decodes");
2161
2162        assert_eq!(decoded.trust_tier, None);
2163        assert!(decoded.consumes.is_empty());
2164        assert_eq!(decoded.bindings, None);
2165
2166        let pretty = format!("{}\n", serde_json::to_string_pretty(&decoded).unwrap());
2167        assert_eq!(
2168            pretty.as_bytes(),
2169            raw,
2170            "new manifest matches golden byte-for-byte without unread keys"
2171        );
2172
2173        let as_val: serde_json::Value = serde_json::to_value(&decoded).unwrap();
2174        assert!(
2175            as_val.get("trust_tier").is_none(),
2176            "no trust_tier on wire for new manifest"
2177        );
2178        assert!(
2179            as_val.get("consumes").is_none(),
2180            "no consumes on wire for empty consumes"
2181        );
2182        assert!(
2183            as_val.get("bindings").is_none(),
2184            "no bindings on wire for new manifest"
2185        );
2186    }
2187
2188    #[test]
2189    fn aft_manifest_fixture_matches_v1_contract() {
2190        let manifest = aft_manifest_fixture();
2191
2192        assert_eq!(manifest.module_id, "aft");
2193        let ProviderRole::ToolProvider {
2194            tools,
2195            identity_scope,
2196            concurrency,
2197            emits_push,
2198            sub_supervises,
2199        } = &manifest.provides[0]
2200        else {
2201            panic!("AFT fixture must expose one tool_provider role");
2202        };
2203
2204        assert_eq!(*concurrency, Concurrency::ModuleManaged);
2205        assert!(*emits_push);
2206        assert!(*sub_supervises);
2207        assert_eq!(
2208            identity_scope,
2209            &vec![IdentityScope::Session, IdentityScope::Project]
2210        );
2211        assert_eq!(
2212            tools
2213                .iter()
2214                .map(|tool| (tool.name.as_str(), tool.execution_mode))
2215                .collect::<Vec<_>>(),
2216            vec![
2217                ("read", ExecutionMode::Pure),
2218                ("grep", ExecutionMode::Pure),
2219                ("outline", ExecutionMode::Pure),
2220                ("semantic_search", ExecutionMode::Pure),
2221                ("edit", ExecutionMode::Mutating),
2222                ("write", ExecutionMode::Mutating),
2223                ("bash", ExecutionMode::Unfenceable),
2224            ]
2225        );
2226    }
2227
2228    #[test]
2229    fn tool_provider_role_tag_serializes_as_snake_case() {
2230        let manifest = aft_manifest_fixture();
2231        let value = serde_json::to_value(&manifest).unwrap();
2232
2233        assert_eq!(value["provides"][0]["role"], "tool_provider");
2234    }
2235
2236    #[test]
2237    fn manifest_without_capabilities_preserves_the_existing_wire_shape() {
2238        let manifest = aft_manifest_fixture();
2239        let encoded = serde_json::to_value(&manifest).expect("manifest serializes");
2240        assert!(encoded.get("capabilities").is_none());
2241
2242        let decoded: ModuleManifest =
2243            serde_json::from_value(encoded).expect("legacy manifest parses");
2244        assert_eq!(decoded.capabilities, None);
2245    }
2246
2247    #[test]
2248    fn capability_identifier_lexical_grammar_accepts_only_pinned_forms() {
2249        for identifier in [
2250            "a/v1",
2251            "credentials-provider/v1",
2252            "a1-b2/v4294967295",
2253            "a123456789012345678901234567890123456789012345678901234567890123/v1",
2254        ] {
2255            assert!(
2256                is_valid_capability_identifier(identifier),
2257                "identifier must be accepted: {identifier}"
2258            );
2259        }
2260
2261        for identifier in [
2262            "credentials-Provider/v1",
2263            "credentials-provider/v01",
2264            "credentials-provider-/v1",
2265            "credentials--provider/v1",
2266            "Credentials-provider/v1",
2267            "credentials-provider/1",
2268            "credentials provider/v1",
2269            "credentials-provider/v0",
2270            "credentials-provider/v4294967296",
2271            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/v1",
2272        ] {
2273            assert!(
2274                !is_valid_capability_identifier(identifier),
2275                "identifier must be rejected: {identifier}"
2276            );
2277        }
2278    }
2279
2280    #[test]
2281    fn capability_grammar_errors_redact_secret_shaped_values() {
2282        let error = validate_manifest_capability_grammar(&json!({
2283            "capabilities": { "provides": ["sk-secret-value/v0"] }
2284        }))
2285        .expect_err("secret-shaped capability identifier is malformed");
2286        assert_eq!(error.field(), "capabilities.provides[0]");
2287        assert_eq!(error.value(), "<redacted>");
2288        assert!(!error.to_string().contains("sk-secret-value"));
2289    }
2290
2291    /// Builder sentinels are the strings tooling emits where it means "no
2292    /// value" (shell fallbacks say `unknown`, not `unavailable`); publishing
2293    /// one as a build fact is the well-formed lie the provenance contract
2294    /// names. The helper must map every sentinel, any casing, to field
2295    /// omission — and must keep a canonical real value intact (the control arm,
2296    /// so the filter cannot pass by refusing everything).
2297    #[test]
2298    fn provenance_builder_sentinels_become_field_omission() {
2299        for sentinel in [
2300            "unknown",
2301            "UNKNOWN",
2302            "Unknown",
2303            "unavailable",
2304            "none",
2305            "None",
2306            "  unknown  ",
2307            "",
2308        ] {
2309            let p = build_provenance_from_source(
2310                BuildGitShaSource::Git {
2311                    revision: sentinel,
2312                    tree_state: GitTreeState::Clean,
2313                },
2314                Some(sentinel),
2315                Some(sentinel),
2316            )
2317            .expect("sentinels are omitted before form validation");
2318            assert_eq!(
2319                (
2320                    p.build_git_sha,
2321                    p.build_git_sha_absence_reason,
2322                    p.build_lock_digest,
2323                    p.store_schema_version,
2324                ),
2325                (
2326                    None,
2327                    Some(BuildGitShaAbsenceReason::NeverDerived),
2328                    None,
2329                    None,
2330                ),
2331                "sentinel {sentinel:?} must be omitted, not published"
2332            );
2333        }
2334        let real = build_provenance_from_source(
2335            BuildGitShaSource::Git {
2336                revision: "0123456789abcdef0123456789abcdef01234567",
2337                tree_state: GitTreeState::Clean,
2338            },
2339            None,
2340            Some("9"),
2341        )
2342        .expect("canonical build revision is accepted");
2343        assert_eq!(
2344            real.build_git_sha.as_deref(),
2345            Some("0123456789abcdef0123456789abcdef01234567")
2346        );
2347        assert_eq!(real.store_schema_version.as_deref(), Some("9"));
2348        // The always-knowable fact: an SDK-built block always carries a crate
2349        // version, so it is never empty; that is why the contract omits a
2350        // field when it is absent rather than publishing a sentinel.
2351        assert_eq!(
2352            real.wire_crate_version.as_deref(),
2353            Some(crate::SUBC_PROTOCOL_CRATE_VERSION)
2354        );
2355    }
2356
2357    #[test]
2358    fn build_provenance_accepts_canonical_sha_and_lock_digest() {
2359        let provenance = build_provenance_from_source(
2360            BuildGitShaSource::Git {
2361                revision: " 0123456789abcdef0123456789abcdef01234567 ",
2362                tree_state: GitTreeState::Clean,
2363            },
2364            Some(" abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 "),
2365            Some(" schema-v3 "),
2366        )
2367        .expect("canonical build facts are accepted");
2368
2369        assert_eq!(
2370            provenance,
2371            ManifestProvenance {
2372                build_git_sha: Some("0123456789abcdef0123456789abcdef01234567".to_string()),
2373                build_git_sha_absence_reason: None,
2374                build_lock_digest: Some(
2375                    "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(),
2376                ),
2377                wire_crate_version: Some(crate::SUBC_PROTOCOL_CRATE_VERSION.to_string()),
2378                store_schema_version: Some("schema-v3".to_string()),
2379                launch_nonce_source: None,
2380            }
2381        );
2382    }
2383
2384    #[test]
2385    fn build_provenance_refuses_an_abbreviated_git_sha() {
2386        let error = build_provenance_from_source(
2387            BuildGitShaSource::Git {
2388                revision: "0123456789ab",
2389                tree_state: GitTreeState::Clean,
2390            },
2391            None,
2392            None,
2393        )
2394        .expect_err("a 12-character abbreviation is not canonical");
2395
2396        assert_eq!(error.field(), "build_git_sha");
2397        assert_eq!(error.length(), 12);
2398        assert_eq!(error.canonical_form(), BUILD_GIT_SHA_CANONICAL_FORM);
2399        assert_eq!(
2400            error.to_string(),
2401            "invalid manifest provenance form: field build_git_sha has length 12; canonical form is exactly 40 lowercase hexadecimal characters"
2402        );
2403    }
2404
2405    #[test]
2406    fn build_provenance_refuses_an_abbreviated_lock_digest() {
2407        let error = build_provenance_from_source(
2408            BuildGitShaSource::NeverDerived,
2409            Some("0123456789abcdef"),
2410            None,
2411        )
2412        .expect_err("a 16-character digest is not canonical");
2413
2414        assert_eq!(error.field(), "build_lock_digest");
2415        assert_eq!(error.length(), 16);
2416        assert_eq!(error.canonical_form(), BUILD_LOCK_DIGEST_CANONICAL_FORM);
2417    }
2418
2419    #[test]
2420    fn build_provenance_refuses_uppercase_hex() {
2421        let uppercase_sha = "A".repeat(40);
2422        let error = build_provenance_from_source(
2423            BuildGitShaSource::Git {
2424                revision: &uppercase_sha,
2425                tree_state: GitTreeState::Clean,
2426            },
2427            None,
2428            None,
2429        )
2430        .expect_err("uppercase hexadecimal is not canonical");
2431
2432        assert_eq!(error.field(), "build_git_sha");
2433        assert_eq!(error.length(), 40);
2434        assert_eq!(error.canonical_form(), BUILD_GIT_SHA_CANONICAL_FORM);
2435    }
2436
2437    #[test]
2438    fn build_provenance_refuses_dirty_revision_stamp_claimed_clean() {
2439        let error = build_provenance_from_source(
2440            BuildGitShaSource::Git {
2441                revision: "0123456789abcdef0123456789abcdef01234567-dirty",
2442                tree_state: GitTreeState::Clean,
2443            },
2444            None,
2445            None,
2446        )
2447        .expect_err("a dirty stamp is not a canonical build revision");
2448
2449        assert_eq!(error.field(), "build_git_sha");
2450        assert_eq!(error.length(), 46);
2451        assert_eq!(error.canonical_form(), BUILD_GIT_SHA_CANONICAL_FORM);
2452    }
2453
2454    #[test]
2455    fn build_provenance_keeps_a_lock_digest_when_identity_is_unavailable() {
2456        let provenance = build_provenance_from_source(
2457            BuildGitShaSource::Git {
2458                revision: "unavailable",
2459                tree_state: GitTreeState::Clean,
2460            },
2461            Some("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"),
2462            None,
2463        )
2464        .expect("sentinel SHA is omitted before the valid lock digest is checked");
2465
2466        assert_eq!(provenance.build_git_sha, None);
2467        assert_eq!(
2468            provenance.build_lock_digest,
2469            Some("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string())
2470        );
2471        assert_eq!(
2472            provenance.wire_crate_version,
2473            Some(crate::SUBC_PROTOCOL_CRATE_VERSION.to_string())
2474        );
2475    }
2476
2477    #[test]
2478    fn build_provenance_omits_fully_unavailable_inputs() {
2479        let provenance = build_provenance_from_source(
2480            BuildGitShaSource::NeverDerived,
2481            Some(" unavailable "),
2482            Some("   "),
2483        )
2484        .expect("omitted and sentinel inputs are not form errors");
2485
2486        assert_eq!(provenance.build_git_sha, None);
2487        assert_eq!(provenance.build_lock_digest, None);
2488        assert_eq!(provenance.store_schema_version, None);
2489        assert_eq!(
2490            provenance.wire_crate_version,
2491            Some(crate::SUBC_PROTOCOL_CRATE_VERSION.to_string())
2492        );
2493    }
2494
2495    #[test]
2496    fn legacy_build_provenance_keeps_master_wire_bytes_without_an_absence_reason() {
2497        let revision = "0123456789abcdef0123456789abcdef01234567";
2498        for (input, expected) in [
2499            (
2500                Some(revision),
2501                format!(
2502                    r#"{{"build_git_sha":"{revision}","wire_crate_version":"{}"}}"#,
2503                    crate::SUBC_PROTOCOL_CRATE_VERSION
2504                ),
2505            ),
2506            (
2507                None,
2508                format!(
2509                    r#"{{"wire_crate_version":"{}"}}"#,
2510                    crate::SUBC_PROTOCOL_CRATE_VERSION
2511                ),
2512            ),
2513            (
2514                Some("unknown"),
2515                format!(
2516                    r#"{{"wire_crate_version":"{}"}}"#,
2517                    crate::SUBC_PROTOCOL_CRATE_VERSION
2518                ),
2519            ),
2520        ] {
2521            let provenance = build_provenance(input, None, None)
2522                .expect("the legacy build facts remain constructible");
2523            assert_eq!(provenance.build_git_sha_absence_reason, None);
2524            assert_eq!(
2525                serde_json::to_string(&provenance).expect("legacy provenance serializes"),
2526                expected
2527            );
2528        }
2529    }
2530
2531    #[test]
2532    fn build_provenance_derives_git_sha_absence_from_the_stamping_inputs() {
2533        let revision = "0123456789abcdef0123456789abcdef01234567";
2534        let cases = [
2535            (
2536                BuildGitShaSource::Git {
2537                    revision,
2538                    tree_state: GitTreeState::Clean,
2539                },
2540                Some(revision),
2541                None,
2542            ),
2543            (
2544                BuildGitShaSource::Git {
2545                    revision,
2546                    tree_state: GitTreeState::Dirty,
2547                },
2548                None,
2549                Some(BuildGitShaAbsenceReason::DeclinedDirty),
2550            ),
2551            (
2552                BuildGitShaSource::NeverDerived,
2553                None,
2554                Some(BuildGitShaAbsenceReason::NeverDerived),
2555            ),
2556            (
2557                BuildGitShaSource::NoGitDir,
2558                None,
2559                Some(BuildGitShaAbsenceReason::NoGitDir),
2560            ),
2561        ];
2562
2563        for (source, expected_sha, expected_reason) in cases {
2564            let provenance = build_provenance_from_source(source, None, None)
2565                .expect("every stamping state constructs honest provenance");
2566            assert_eq!(provenance.build_git_sha.as_deref(), expected_sha);
2567            assert_eq!(provenance.build_git_sha_absence_reason, expected_reason);
2568        }
2569    }
2570
2571    #[test]
2572    fn unknown_git_sha_absence_reason_round_trips_byte_faithfully() {
2573        let wire = format!(
2574            r#"{{"build_git_sha_absence_reason":"future_stamper_state","wire_crate_version":"{}"}}"#,
2575            crate::SUBC_PROTOCOL_CRATE_VERSION
2576        );
2577        let provenance: ManifestProvenance =
2578            serde_json::from_str(&wire).expect("future absence reasons remain readable");
2579
2580        assert_eq!(
2581            provenance.build_git_sha_absence_reason,
2582            Some(BuildGitShaAbsenceReason::ForwardCompatibleUnknown(
2583                "future_stamper_state".to_string()
2584            ))
2585        );
2586        assert_eq!(
2587            serde_json::to_string(&provenance).expect("future absence reason reserializes"),
2588            wire
2589        );
2590    }
2591
2592    #[test]
2593    fn provenance_rejects_an_absence_reason_beside_a_declared_commit() {
2594        let error = serde_json::from_value::<ManifestProvenance>(json!({
2595            "build_git_sha": "0123456789abcdef0123456789abcdef01234567",
2596            "build_git_sha_absence_reason": "declined_dirty"
2597        }))
2598        .expect_err("a declared commit cannot also claim an absence reason");
2599
2600        assert!(error.to_string().contains(
2601            "build_git_sha_absence_reason has must be omitted when build_git_sha is present"
2602        ));
2603    }
2604}