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    /// Existing role declarations; capability grammar claims deliberately live in
31    /// the separate [`CapabilityDeclarations`] block below.
32    pub provides: Vec<ProviderRole>,
33    #[serde(default, skip_serializing_if = "Vec::is_empty")]
34    pub consumes: Vec<ConsumerRole>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub bindings: Option<Bindings>,
37    /// Optional capability-grammar declarations.
38    ///
39    /// Omitting this block preserves the manifest contract used before capability
40    /// grammar was introduced. A present block is static discovery metadata that
41    /// the daemon validates before accepting a HELLO.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub capabilities: Option<CapabilityDeclarations>,
44    /// Periodic or event-driven behavior this module performs against an external
45    /// surface, so later analysts can account for the resulting self-shaped time
46    /// series.
47    ///
48    /// Declarations describe the EFFECTIVE values in force at HELLO time. In
49    /// particular, a compile-time cadence constant belongs in
50    /// [`SignalCadence::Literal`], while a cadence resolved from configuration
51    /// belongs in [`SignalCadence::Derived`] with a pointer to that effective
52    /// source. Both provenance stories are honest; copying a stale configured
53    /// value into a literal is not.
54    ///
55    /// Ephemeral signals are out of scope for v1 because they are not durably
56    /// declarable, not because they are harmless. A v2 reader must not interpret
57    /// this field's absence in a v1 manifest as a judgement about ephemerals.
58    ///
59    /// `None` and `Some(vec![])` are deliberately distinct on the wire: an
60    /// absent block means the module has not adopted this vocabulary (readers
61    /// treat it as zero signals but must not treat it as a survey answer),
62    /// while an empty list is an affirmative declaration that the module
63    /// examined its effects and claims none are declarable. Modules that mean
64    /// "no signals" should declare the empty list; `None` is what an
65    /// un-adopted manifest looks like, not a statement.
66    ///
67    /// Convention for mutate-effect entries: where the mutation leaves a
68    /// per-observation tell on the surface itself (insula publishes the
69    /// relaxed `usedPercent` beside the raw figure, so any single reading
70    /// self-reports whether it was touched), name that tell in the
71    /// declaration's note. A standing registry row says the module sometimes
72    /// mutates; the tell says whether THIS observation was mutated — a
73    /// consumer holding one sample can act on the second, not the first.
74    /// A named tell must exist on the wire independently of this registry:
75    /// the declaration points at evidence, it is never the evidence. A tell
76    /// that exists only because the manifest describes it is a claim
77    /// vouching for itself.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub self_signals: Option<Vec<SelfSignalDeclaration>>,
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub provenance: Option<ManifestProvenance>,
82}
83
84/// Incrementally constructs a [`ModuleManifest`] without fabricating absent facts.
85#[derive(Debug, Clone)]
86pub struct ModuleManifestBuilder {
87    module_id: String,
88    module_version: String,
89    protocol_ver: u8,
90    trust_tier: Option<TrustTier>,
91    provides: Vec<ProviderRole>,
92    consumes: Vec<ConsumerRole>,
93    bindings: Option<Bindings>,
94    capabilities: Option<CapabilityDeclarations>,
95    self_signals: Option<Vec<SelfSignalDeclaration>>,
96    provenance: Option<ManifestProvenance>,
97}
98
99impl ModuleManifest {
100    /// Starts a manifest builder with the minimal identification fields.
101    ///
102    /// `protocol_ver` defaults to the protocol version linked into this crate;
103    /// `provides` and `consumes` default to empty declarations. `trust_tier`
104    /// and `bindings` default to `None` because the daemon reads neither on any
105    /// production path; a required unread field forces producers to invent
106    /// fabricated values.
107    pub fn builder(
108        module_id: impl Into<String>,
109        module_version: impl Into<String>,
110    ) -> ModuleManifestBuilder {
111        ModuleManifestBuilder {
112            module_id: module_id.into(),
113            module_version: module_version.into(),
114            protocol_ver: PROTOCOL_VERSION,
115            trust_tier: None,
116            provides: Vec::new(),
117            consumes: Vec::new(),
118            bindings: None,
119            capabilities: None,
120            self_signals: None,
121            provenance: None,
122        }
123    }
124}
125
126impl ModuleManifestBuilder {
127    /// Overrides the linked protocol version for compatibility fixtures.
128    pub fn protocol_ver(mut self, protocol_ver: u8) -> Self {
129        self.protocol_ver = protocol_ver;
130        self
131    }
132
133    /// Declares the optional trust tier of this module.
134    ///
135    /// The daemon does not evaluate this field on any production path.
136    pub fn trust_tier(mut self, trust_tier: Option<TrustTier>) -> Self {
137        self.trust_tier = trust_tier;
138        self
139    }
140
141    /// Declares the provider roles this module exposes.
142    pub fn provides(mut self, provides: Vec<ProviderRole>) -> Self {
143        self.provides = provides;
144        self
145    }
146
147    /// Declares the consumer roles this module requests.
148    pub fn consumes(mut self, consumes: Vec<ConsumerRole>) -> Self {
149        self.consumes = consumes;
150        self
151    }
152
153    /// Declares the optional resource and subsystem bindings of this module.
154    ///
155    /// The daemon does not evaluate this field on any production path.
156    pub fn bindings(mut self, bindings: Option<Bindings>) -> Self {
157        self.bindings = bindings;
158        self
159    }
160
161    /// Adds optional capability-grammar declarations.
162    pub fn capabilities(mut self, capabilities: Option<CapabilityDeclarations>) -> Self {
163        self.capabilities = capabilities;
164        self
165    }
166
167    /// Adds optional periodic or event-driven behavior declarations.
168    pub fn self_signals(mut self, self_signals: Option<Vec<SelfSignalDeclaration>>) -> Self {
169        self.self_signals = self_signals;
170        self
171    }
172
173    /// Adds optional build provenance declared by the module.
174    pub fn provenance(mut self, provenance: Option<ManifestProvenance>) -> Self {
175        self.provenance = provenance;
176        self
177    }
178
179    /// Finishes the manifest.
180    pub fn build(self) -> ModuleManifest {
181        ModuleManifest {
182            module_id: self.module_id,
183            module_version: self.module_version,
184            protocol_ver: self.protocol_ver,
185            trust_tier: self.trust_tier,
186            provides: self.provides,
187            consumes: self.consumes,
188            bindings: self.bindings,
189            capabilities: self.capabilities,
190            self_signals: self.self_signals,
191            provenance: self.provenance,
192        }
193    }
194}
195
196/// DELIBERATELY LENIENT: unknown top-level manifest keys are DROPPED at this
197/// parse boundary, not rejected and not retained. This is forward
198/// compatibility across version skew — a module built against a newer
199/// subc-protocol must still HELLO into an older daemon, and strictness here
200/// would turn every additive manifest field into a daemon-first flag day.
201/// The costs, so nobody re-derives them the hard way (CEREB found both):
202/// - A key you add module-side is INVISIBLE to the daemon until a typed field
203///   lands here. Producing it is honest; assuming a daemon-side reader exists
204///   is not. Say who the audience is next to any such producer.
205/// - There is deliberately NO untyped extension bag on this struct: a
206///   retained-verbatim Value map becomes an unversioned de-facto wire
207///   contract nobody authored (the drift class module-owned payload crates
208///   exist to prevent). When a daemon consumer materializes for a fact, the
209///   fact gets a typed optional field with a CONSUMER-IMPACT commit instead.
210///
211/// `CapabilityDeclarations` below is strict by contrast because claims are
212/// routed on: an unparseable claim must refuse loudly, never partially apply.
213#[derive(Deserialize)]
214struct ModuleManifestWire {
215    module_id: String,
216    module_version: String,
217    protocol_ver: u8,
218    #[serde(default)]
219    trust_tier: Option<TrustTier>,
220    provides: Vec<ProviderRole>,
221    #[serde(default)]
222    consumes: Vec<ConsumerRole>,
223    #[serde(default)]
224    bindings: Option<Bindings>,
225    #[serde(default)]
226    capabilities: Option<CapabilityDeclarations>,
227    #[serde(default)]
228    self_signals: Option<Vec<SelfSignalDeclaration>>,
229    #[serde(default)]
230    provenance: Option<ManifestProvenance>,
231    // `runtime_computed` belongs to --manifest output rather than the retained
232    // manifest model. Deserialize it only long enough to enforce that capability
233    // declarations cannot be omitted as runtime-varying data.
234    #[serde(default)]
235    runtime_computed: Option<Value>,
236}
237
238impl<'de> Deserialize<'de> for ModuleManifest {
239    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
240    where
241        D: Deserializer<'de>,
242    {
243        let wire = ModuleManifestWire::deserialize(deserializer)?;
244        validate_runtime_computed(wire.runtime_computed.as_ref(), "runtime_computed")
245            .map_err(D::Error::custom)?;
246        let manifest = Self::builder(wire.module_id, wire.module_version)
247            .protocol_ver(wire.protocol_ver)
248            .trust_tier(wire.trust_tier)
249            .provides(wire.provides)
250            .consumes(wire.consumes)
251            .bindings(wire.bindings)
252            .capabilities(wire.capabilities)
253            .self_signals(wire.self_signals)
254            .provenance(wire.provenance)
255            .build();
256        manifest
257            .validate_capability_grammar()
258            .map_err(D::Error::custom)?;
259        Ok(manifest)
260    }
261}
262
263/// A raw HELLO declaration error that can be reported before serde drops context.
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct SelfSignalDeclarationError {
266    module_id: String,
267    entry_index: usize,
268    field: &'static str,
269}
270
271impl fmt::Display for SelfSignalDeclarationError {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        write!(
274            f,
275            "module_id '{}' self_signals[{}] is missing required field '{}'",
276            self.module_id.escape_debug(),
277            self.entry_index,
278            self.field
279        )
280    }
281}
282
283/// Reject raw HELLO self-signal declarations that omit `effect` or `anchored_to`.
284///
285/// Serde correctly rejects these omissions while decoding [`ModuleManifest`], but
286/// that decode does not retain the module id or list index needed for a useful
287/// daemon refusal. This preflight adds only that reporting context; it does not
288/// interpret a declaration's behavior.
289pub fn validate_hello_self_signal_declarations(
290    hello: &Value,
291) -> Result<(), SelfSignalDeclarationError> {
292    let Some(manifest) = hello.get("manifest").and_then(Value::as_object) else {
293        return Ok(());
294    };
295    let module_id = manifest
296        .get("module_id")
297        .and_then(Value::as_str)
298        .unwrap_or("<unknown>");
299    let Some(entries) = manifest.get("self_signals").and_then(Value::as_array) else {
300        return Ok(());
301    };
302
303    for (entry_index, entry) in entries.iter().enumerate() {
304        let Some(entry) = entry.as_object() else {
305            continue;
306        };
307        for field in ["effect", "anchored_to"] {
308            if !entry.contains_key(field) {
309                return Err(SelfSignalDeclarationError {
310                    module_id: module_id.to_string(),
311                    entry_index,
312                    field,
313                });
314            }
315        }
316    }
317    Ok(())
318}
319
320/// Static, versioned capabilities declared by a module.
321#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
322#[serde(deny_unknown_fields)]
323pub struct CapabilityDeclarations {
324    #[serde(default)]
325    pub provides: Vec<String>,
326    #[serde(default)]
327    pub requires: Vec<CapabilityRequirement>,
328    #[serde(default)]
329    pub must_never_reach: Vec<String>,
330}
331
332/// A declared periodic or event-driven behavior that shapes an external surface.
333#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
334pub struct SelfSignalDeclaration {
335    /// Stable identifier for this declared behavior, such as `codex_keepalive`.
336    pub name: String,
337    /// Informative classification only; it never substitutes for `effect` or
338    /// `anchored_to` when an analyst interprets the declaration.
339    pub kind: SelfSignalKind,
340    /// Whether the signal only observes the surface or changes it.
341    pub effect: SelfSignalEffect,
342    /// Whether the behavior follows its own interval or a surface event boundary.
343    pub anchored_to: SignalAnchor,
344    /// The effective cadence in force at HELLO time.
345    ///
346    /// Use [`SignalCadence::Literal`] when a compile-time constant is the
347    /// effective value. Use [`SignalCadence::Derived`] when configuration or
348    /// another runtime input resolves the effective value, naming the source so
349    /// the declaration cannot silently drift from that resolution.
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub cadence: Option<SignalCadence>,
352    /// The external surface this behavior shapes, such as `provider-usage`.
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub domain: Option<String>,
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub note: Option<String>,
357}
358
359/// Informative class of a self-signal, tolerant of newer wire values.
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub enum SelfSignalKind {
362    Keepalive,
363    Poller,
364    Cron,
365    Sweep,
366    Watchdog,
367    Heartbeat,
368    Other(String),
369}
370
371impl SelfSignalKind {
372    fn wire_name(&self) -> &str {
373        match self {
374            Self::Keepalive => "keepalive",
375            Self::Poller => "poller",
376            Self::Cron => "cron",
377            Self::Sweep => "sweep",
378            Self::Watchdog => "watchdog",
379            Self::Heartbeat => "heartbeat",
380            Self::Other(value) => value,
381        }
382    }
383}
384
385impl Serialize for SelfSignalKind {
386    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
387    where
388        S: serde::Serializer,
389    {
390        serializer.serialize_str(self.wire_name())
391    }
392}
393
394impl<'de> Deserialize<'de> for SelfSignalKind {
395    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
396    where
397        D: Deserializer<'de>,
398    {
399        let value = String::deserialize(deserializer)?;
400        Ok(match value.as_str() {
401            "keepalive" => Self::Keepalive,
402            "poller" => Self::Poller,
403            "cron" => Self::Cron,
404            "sweep" => Self::Sweep,
405            "watchdog" => Self::Watchdog,
406            "heartbeat" => Self::Heartbeat,
407            _ => Self::Other(value),
408        })
409    }
410}
411
412/// The effect a self-signal has on the external surface it targets.
413#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
414#[serde(rename_all = "lowercase")]
415pub enum SelfSignalEffect {
416    Observe,
417    Mutate,
418}
419
420/// What establishes a self-signal's cadence relative to the external surface.
421#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
422#[serde(rename_all = "snake_case")]
423pub enum SignalAnchor {
424    /// The behavior follows its own periodic signature, so analysts can find it
425    /// without an external event grid.
426    FixedInterval,
427    /// The behavior follows an external event boundary, which can make its shape
428    /// indistinguishable from the surface mechanism without this declaration.
429    Event { event: String },
430}
431
432/// How a self-signal's effective cadence is declared.
433#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
434#[serde(rename_all = "snake_case")]
435pub enum SignalCadence {
436    Literal { interval_ms: u64 },
437    Derived { source: String },
438}
439
440/// Build facts a module DECLARES about its own binary at HELLO. The daemon
441/// overlays process-identity evidence it alone can attest; the two halves are
442/// served together via `supervisor.provenance` and never merged.
443///
444/// The canonical constructor form is a full 40-character lowercase hexadecimal
445/// `build_git_sha` and a full 64-character lowercase hexadecimal
446/// `build_lock_digest`; abbreviations are not conforming. The daemon's HELLO
447/// decoder intentionally remains lenient enough to relay older declarations,
448/// so this construction contract is enforced by [`build_provenance`] rather
449/// than by wire deserialization.
450///
451/// Honesty contract for constructors (ruled with the first adopters):
452/// - Every field is a VERIFIED-AT-BUILD claim. No field is required: a module
453///   may declare any subset, and omitting an inapplicable field is the honest
454///   choice rather than inventing a value to fill it. Populate `build_git_sha`
455///   only from a value injected by the build/release pipeline (`CK_BUILD_REV`
456///   via `option_env!` guarded by the packaging path, or build.rs equivalent)
457///   — never from ambient env at an arbitrary consumer compile, which mints a
458///   provenance claim from an accident of whoever ran cargo. A builder that
459///   can determine whether the tree was clean may declare the sha regardless
460///   of whether a release pipeline exists.
461/// - Dirty or unstamped builds declare `None` for the affected fields. A
462///   populated field stops the reader asking; absent-and-honest beats
463///   present-and-best-effort. Absence is reported at two levels with two
464///   distinct words: a module that declared no provenance block at all reads
465///   `unverifiable`, while an omitted field inside a declared block is
466///   dropped from the wire and reads `unavailable`. So omitting a field never
467///   costs a module its `Reported` status -- declaration is decided by
468///   whether the manifest carried a block, not by which fields it filled.
469/// - Dirty-tree stamps are not canonical `build_git_sha` values. A pipeline
470///   that emits `-dirty` must omit the affected field rather than pass that
471///   stamp to the canonical constructor. Stricter is better: cerebellum's
472///   build.rs reports the commit ONLY when the tree was clean, on the argument
473///   that dirty bytes match no commit and a precise-looking wrong answer beats
474///   absence at being believed.
475/// - Two silent-when-wrong checks for any build-rev embedder (CEREB): does
476///   the builder know whether the tree was clean, and can its no-git sentinel
477///   (source-tarball builds) escape into a field parsed as a sha? Sentinels
478///   render as absence, never as a value.
479/// - Fill fields FROM THE BUILD only: reading Cargo.lock or the wire crate
480///   version inside the manifest constructor describes the source tree
481///   sitting beside the running binary, not the binary — the exact claim
482///   this struct exists to avoid.
483/// - Declare what you KNOW, not blanket-None (WERNI): `store_schema_version`
484///   needs no pipeline — any module with a migration list can state its
485///   newest migration as fact, and a daemon comparing it against the store's
486///   actual version sees a stale-binary mismatch directly. Blanket `None`
487///   where a field is knowable wastes the field; blanket-fill where it is
488///   not mints a lie. Absence also beats sentinel values (CKCRED): omit the
489///   FIELD when BUILD_REV reads a builder sentinel ("unknown", "unavailable",
490///   "none", any casing) — publishing the sentinel string as a fact is a
491///   well-formed lie shape validation cannot catch. Field omission, not block
492///   omission, is the target shape for SDK modules: `wire_crate_version` is a
493///   compile-time constant of the linked crate, so a module using the SDK
494///   always has at least one honest fact and `build_provenance` reflects that
495///   by never returning an absent block. (Block absence remains meaningful on
496///   the wire — it reads `unverifiable`, the module made no claim — but it is
497///   the shape for non-adopters and proxied manifests, not a target for
498///   declarers; see #78.) The hazard in one sentence, for every referent and
499///   sentinel case alike: A PRESENT, WELL-FORMED FIELD STOPS THE READER
500///   ASKING — a value from the wrong domain and a sentinel from the wrong
501///   vocabulary are indistinguishable from a correct value to every check
502///   that inspects shape rather than meaning.
503/// - PROXIED MANIFESTS STAY None PERMANENTLY (CALLO): a process that
504///   forwards another machine's manifest cannot observe that build, and a
505///   forwarded provenance claim is indistinguishable on the wire from a
506///   verified one — filling it launders an unverifiable assertion. Same
507///   reasoning as pinning a re-exported module's trust_tier to Untrusted.
508///   Record that at the construction site: injection-wiring sweeps grep for
509///   `provenance:` and the obvious action at a re-export site is the wrong
510///   one.
511#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
512pub struct ManifestProvenance {
513    #[serde(default, skip_serializing_if = "Option::is_none")]
514    pub build_git_sha: Option<String>,
515    #[serde(default, skip_serializing_if = "Option::is_none")]
516    pub build_lock_digest: Option<String>,
517    /// REFERENT: the `subc-protocol` crate version linked into this binary
518    /// (`subc_protocol::SUBC_PROTOCOL_CRATE_VERSION`) — the fleet's shared
519    /// wire vocabulary, one numbering space for every module. Never a
520    /// module's own envelope/payload crate version: that is real information
521    /// in a different numbering space, and here it scores as a confident
522    /// wrong answer at any census gate. (QTA's rule, learned live: a field
523    /// whose entire content is a referent cannot be documented by its
524    /// constraints — so the referent is stated here, where readers look.)
525    #[serde(default, skip_serializing_if = "Option::is_none")]
526    pub wire_crate_version: Option<String>,
527    #[serde(default, skip_serializing_if = "Option::is_none")]
528    pub store_schema_version: Option<String>,
529}
530
531const MAX_PROVENANCE_VALUE_BYTES: usize = 128;
532const BUILD_GIT_SHA_CANONICAL_FORM: &str = "exactly 40 lowercase hexadecimal characters";
533const BUILD_LOCK_DIGEST_CANONICAL_FORM: &str = "exactly 64 lowercase hexadecimal characters";
534
535#[derive(Deserialize)]
536struct ManifestProvenanceWire {
537    #[serde(default)]
538    build_git_sha: Option<String>,
539    #[serde(default)]
540    build_lock_digest: Option<String>,
541    #[serde(default)]
542    wire_crate_version: Option<String>,
543    #[serde(default)]
544    store_schema_version: Option<String>,
545}
546
547impl<'de> Deserialize<'de> for ManifestProvenance {
548    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
549    where
550        D: Deserializer<'de>,
551    {
552        let wire = ManifestProvenanceWire::deserialize(deserializer)?;
553        let provenance = Self {
554            build_git_sha: wire.build_git_sha,
555            build_lock_digest: wire.build_lock_digest,
556            wire_crate_version: wire.wire_crate_version,
557            store_schema_version: wire.store_schema_version,
558        };
559        provenance.validate().map_err(D::Error::custom)?;
560        Ok(provenance)
561    }
562}
563
564/// A declared build fact did not use its canonical form.
565#[derive(Debug, Clone, PartialEq, Eq)]
566pub struct ProvenanceFormError {
567    field: &'static str,
568    length: usize,
569    canonical_form: &'static str,
570}
571
572impl ProvenanceFormError {
573    fn new(field: &'static str, length: usize, canonical_form: &'static str) -> Self {
574        Self {
575            field,
576            length,
577            canonical_form,
578        }
579    }
580
581    /// The provenance field whose value was not canonical.
582    pub fn field(&self) -> &str {
583        self.field
584    }
585
586    /// The offending value's length in bytes.
587    pub fn length(&self) -> usize {
588        self.length
589    }
590
591    /// The canonical form required for this field.
592    pub fn canonical_form(&self) -> &str {
593        self.canonical_form
594    }
595}
596
597impl fmt::Display for ProvenanceFormError {
598    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
599        write!(
600            f,
601            "invalid manifest provenance form: field {} has length {}; canonical form is {}",
602            self.field, self.length, self.canonical_form
603        )
604    }
605}
606
607impl std::error::Error for ProvenanceFormError {}
608
609#[derive(Debug, Clone, PartialEq, Eq)]
610pub struct ManifestProvenanceError {
611    field: String,
612    value: String,
613    reason: &'static str,
614}
615
616impl ManifestProvenanceError {
617    fn new(field: &str, value: &str, reason: &'static str) -> Self {
618        Self {
619            field: field.to_string(),
620            value: safe_error_value(value),
621            reason,
622        }
623    }
624
625    pub fn field(&self) -> &str {
626        &self.field
627    }
628}
629
630impl fmt::Display for ManifestProvenanceError {
631    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
632        write!(
633            f,
634            "invalid manifest provenance: field {} has {} (value {:?})",
635            self.field, self.reason, self.value
636        )
637    }
638}
639
640impl std::error::Error for ManifestProvenanceError {}
641
642impl ManifestProvenance {
643    pub fn validate(&self) -> Result<(), ManifestProvenanceError> {
644        for (field, value) in [
645            ("build_git_sha", self.build_git_sha.as_deref()),
646            ("build_lock_digest", self.build_lock_digest.as_deref()),
647            ("wire_crate_version", self.wire_crate_version.as_deref()),
648            ("store_schema_version", self.store_schema_version.as_deref()),
649        ] {
650            let Some(value) = value else { continue };
651            if value.is_empty() {
652                return Err(ManifestProvenanceError::new(
653                    field,
654                    value,
655                    "must not be empty",
656                ));
657            }
658            // HELLO decoding checks only wire safety here. Canonical build forms
659            // belong to build_provenance; the daemon is a non-adjudicating relayer
660            // and must continue accepting legacy declarations such as 12-hex
661            // module revisions rather than breaking a fleet on daemon upgrade.
662            if value.len() > MAX_PROVENANCE_VALUE_BYTES {
663                return Err(ManifestProvenanceError::new(
664                    field,
665                    value,
666                    "exceeds the 128-byte maximum",
667                ));
668            }
669            if value.bytes().any(|byte| !(0x20..=0x7e).contains(&byte)) {
670                return Err(ManifestProvenanceError::new(
671                    field,
672                    value,
673                    "contains non-printable ASCII",
674                ));
675            }
676        }
677        Ok(())
678    }
679}
680
681/// Build a [`ManifestProvenance`] from raw build facts, normalizing sentinel
682/// and empty values to field omission, then validating canonical declared forms.
683/// A `build_git_sha` must be exactly 40 lowercase hexadecimal characters and a
684/// `build_lock_digest` exactly 64 lowercase hexadecimal characters. Abbreviations
685/// are not conforming; a real value in the wrong form returns a
686/// [`ProvenanceFormError`] instead of being discarded as if it were absent.
687/// Sentinel values are filtered before form validation, so they remain honest
688/// omission rather than becoming form errors.
689///
690/// OWNERSHIP RULE: a helper that constructs a wire type lives in the crate
691/// that owns the type. This helper constructs `ManifestProvenance`, so it
692/// lives here in subc-protocol (not in subc-client-rs) — transport-direct
693/// modules that never link the client SDK can still build honest provenance.
694pub fn build_provenance(
695    build_git_sha: Option<&str>,
696    build_lock_digest: Option<&str>,
697    store_schema_version: Option<&str>,
698) -> Result<ManifestProvenance, ProvenanceFormError> {
699    let build_git_sha = normalize_provenance_fact(build_git_sha);
700    validate_provenance_form(
701        "build_git_sha",
702        build_git_sha.as_deref(),
703        BUILD_GIT_SHA_CANONICAL_FORM,
704        40,
705    )?;
706
707    let build_lock_digest = normalize_provenance_fact(build_lock_digest);
708    validate_provenance_form(
709        "build_lock_digest",
710        build_lock_digest.as_deref(),
711        BUILD_LOCK_DIGEST_CANONICAL_FORM,
712        64,
713    )?;
714
715    Ok(ManifestProvenance {
716        build_git_sha,
717        build_lock_digest,
718        wire_crate_version: Some(crate::SUBC_PROTOCOL_CRATE_VERSION.to_string()),
719        store_schema_version: normalize_provenance_fact(store_schema_version),
720    })
721}
722
723fn validate_provenance_form(
724    field: &'static str,
725    value: Option<&str>,
726    canonical_form: &'static str,
727    expected_length: usize,
728) -> Result<(), ProvenanceFormError> {
729    let Some(value) = value else { return Ok(()) };
730    if value.len() != expected_length
731        || !value
732            .bytes()
733            .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
734    {
735        return Err(ProvenanceFormError::new(field, value.len(), canonical_form));
736    }
737    Ok(())
738}
739
740/// Sentinel strings that build tooling emits where it means "no value": shell
741/// fallbacks and Makefile defaults produce `unknown`, wire vocabulary uses
742/// `unavailable`, and `git describe` failures surface as `none`. Publishing
743/// any of them as a fact is the well-formed-lie shape the provenance contract
744/// warns against — a present, well-formed field stops the reader asking — so
745/// the helper maps them all to field omission. Matched case-insensitively
746/// because `UNKNOWN`/`Unknown` are equally common from shell fallbacks.
747pub const PROVENANCE_SENTINELS: [&str; 3] = ["unknown", "unavailable", "none"];
748
749fn normalize_provenance_fact(value: Option<&str>) -> Option<String> {
750    let value = value?.trim();
751    if value.is_empty() {
752        return None;
753    }
754    let lowered = value.to_ascii_lowercase();
755    if PROVENANCE_SENTINELS.contains(&lowered.as_str()) {
756        return None;
757    }
758    Some(value.to_string())
759}
760
761/// One capability a module consumes and whether its absence is tolerated.
762#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
763#[serde(deny_unknown_fields)]
764pub struct CapabilityRequirement {
765    pub capability: String,
766    pub need: CapabilityNeed,
767}
768
769/// Closed capability requirement strength vocabulary.
770#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
771#[serde(rename_all = "snake_case")]
772pub enum CapabilityNeed {
773    Required,
774    Optional,
775}
776
777/// A safe-to-report capability-schema validation failure.
778#[derive(Debug, Clone, PartialEq, Eq)]
779pub struct CapabilityGrammarError {
780    field: String,
781    value: String,
782}
783
784impl CapabilityGrammarError {
785    fn new(field: impl Into<String>, value: impl AsRef<str>) -> Self {
786        Self {
787            field: field.into(),
788            value: safe_error_value(value.as_ref()),
789        }
790    }
791
792    /// The precise malformed field path.
793    pub fn field(&self) -> &str {
794        &self.field
795    }
796
797    /// The offending value, redacted when it resembles a credential.
798    pub fn value(&self) -> &str {
799        &self.value
800    }
801}
802
803impl fmt::Display for CapabilityGrammarError {
804    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
805        write!(
806            f,
807            "invalid capability grammar: field {} has offending value {:?}",
808            self.field, self.value
809        )
810    }
811}
812
813impl std::error::Error for CapabilityGrammarError {}
814
815impl ModuleManifest {
816    /// Validate the typed capability block after serde has decoded it.
817    pub fn validate_capability_grammar(&self) -> Result<(), CapabilityGrammarError> {
818        let Some(capabilities) = &self.capabilities else {
819            return Ok(());
820        };
821
822        validate_capability_list("capabilities.provides", &capabilities.provides)?;
823        validate_requires(&capabilities.requires)?;
824        validate_capability_list(
825            "capabilities.must_never_reach",
826            &capabilities.must_never_reach,
827        )
828    }
829}
830
831/// Validate capability grammar in a standalone manifest JSON value.
832///
833/// The raw-value form lets HELLO distinguish schema failures from malformed JSON,
834/// including an unknown `need` that cannot be represented by [`CapabilityNeed`].
835pub fn validate_manifest_capability_grammar(
836    manifest: &Value,
837) -> Result<(), CapabilityGrammarError> {
838    let Some(object) = manifest.as_object() else {
839        return Ok(());
840    };
841
842    validate_capabilities_value(object.get("capabilities"))?;
843    validate_runtime_computed(object.get("runtime_computed"), "runtime_computed")
844}
845
846/// Validate capability grammar in a raw HELLO body.
847///
848/// `runtime_computed` is a top-level sibling in --manifest output. HELLO keeps
849/// accepting that sibling only so an attempted dynamic capability declaration is
850/// refused explicitly instead of being silently ignored by serde.
851pub fn validate_hello_capability_grammar(hello: &Value) -> Result<(), CapabilityGrammarError> {
852    let Some(object) = hello.as_object() else {
853        return Ok(());
854    };
855    if let Some(manifest) = object.get("manifest") {
856        validate_manifest_capability_grammar(manifest)?;
857    }
858    validate_runtime_computed(object.get("runtime_computed"), "runtime_computed")
859}
860
861/// Return whether `identifier` has the exact `<name>/v<N>` capability spelling.
862pub fn is_valid_capability_identifier(identifier: &str) -> bool {
863    if identifier.chars().any(char::is_whitespace) {
864        return false;
865    }
866    let Some((name, version)) = identifier.split_once("/v") else {
867        return false;
868    };
869    if name.is_empty() || name.len() > 64 || version.is_empty() {
870        return false;
871    }
872
873    let name_bytes = name.as_bytes();
874    if !name_bytes[0].is_ascii_lowercase()
875        || (name.len() > 1
876            && !name_bytes[name.len() - 1].is_ascii_lowercase()
877            && !name_bytes[name.len() - 1].is_ascii_digit())
878        || name_bytes.windows(2).any(|pair| pair == b"--")
879    {
880        return false;
881    }
882    if !name_bytes
883        .iter()
884        .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
885    {
886        return false;
887    }
888
889    if version.len() > 1 && version.starts_with('0')
890        || !version.bytes().all(|byte| byte.is_ascii_digit())
891    {
892        return false;
893    }
894    matches!(
895        version.parse::<u64>(),
896        Ok(value) if (1..=u64::from(u32::MAX)).contains(&value)
897    )
898}
899
900fn validate_capabilities_value(value: Option<&Value>) -> Result<(), CapabilityGrammarError> {
901    let Some(value) = value else {
902        return Ok(());
903    };
904    let Some(object) = value.as_object() else {
905        return Err(CapabilityGrammarError::new(
906            "capabilities",
907            value_description(value),
908        ));
909    };
910
911    for (key, value) in object {
912        if !matches!(key.as_str(), "provides" | "requires" | "must_never_reach") {
913            return Err(CapabilityGrammarError::new(
914                field_child("capabilities", key),
915                value_description(value),
916            ));
917        }
918    }
919
920    validate_capability_list_value("capabilities.provides", object.get("provides"))?;
921    validate_requires_value(object.get("requires"))?;
922    validate_capability_list_value(
923        "capabilities.must_never_reach",
924        object.get("must_never_reach"),
925    )
926}
927
928fn validate_capability_list_value(
929    field: &str,
930    value: Option<&Value>,
931) -> Result<(), CapabilityGrammarError> {
932    let Some(value) = value else {
933        return Ok(());
934    };
935    let Some(values) = value.as_array() else {
936        return Err(CapabilityGrammarError::new(field, value_description(value)));
937    };
938
939    let mut seen = HashSet::new();
940    for (index, value) in values.iter().enumerate() {
941        let field = format!("{field}[{index}]");
942        let Some(identifier) = value.as_str() else {
943            return Err(CapabilityGrammarError::new(field, value_description(value)));
944        };
945        validate_capability_identifier(&field, identifier)?;
946        if !seen.insert(identifier) {
947            return Err(CapabilityGrammarError::new(field, identifier));
948        }
949    }
950    Ok(())
951}
952
953fn validate_requires_value(value: Option<&Value>) -> Result<(), CapabilityGrammarError> {
954    let Some(value) = value else {
955        return Ok(());
956    };
957    let Some(values) = value.as_array() else {
958        return Err(CapabilityGrammarError::new(
959            "capabilities.requires",
960            value_description(value),
961        ));
962    };
963
964    let mut seen = HashSet::new();
965    for (index, value) in values.iter().enumerate() {
966        let entry_field = format!("capabilities.requires[{index}]");
967        let Some(object) = value.as_object() else {
968            return Err(CapabilityGrammarError::new(
969                entry_field,
970                value_description(value),
971            ));
972        };
973        for (key, value) in object {
974            if !matches!(key.as_str(), "capability" | "need") {
975                return Err(CapabilityGrammarError::new(
976                    field_child(&entry_field, key),
977                    value_description(value),
978                ));
979            }
980        }
981        let capability_field = format!("{entry_field}.capability");
982        let Some(capability) = object.get("capability").and_then(Value::as_str) else {
983            return Err(CapabilityGrammarError::new(
984                capability_field,
985                object
986                    .get("capability")
987                    .map_or("<missing>".to_string(), value_description),
988            ));
989        };
990        validate_capability_identifier(&capability_field, capability)?;
991
992        let need_field = format!("{entry_field}.need");
993        let Some(need) = object.get("need").and_then(Value::as_str) else {
994            return Err(CapabilityGrammarError::new(
995                need_field,
996                object
997                    .get("need")
998                    .map_or("<missing>".to_string(), value_description),
999            ));
1000        };
1001        if !matches!(need, "required" | "optional") {
1002            return Err(CapabilityGrammarError::new(need_field, need));
1003        }
1004        if !seen.insert(capability) {
1005            return Err(CapabilityGrammarError::new(entry_field, capability));
1006        }
1007    }
1008    Ok(())
1009}
1010
1011fn validate_capability_list(field: &str, values: &[String]) -> Result<(), CapabilityGrammarError> {
1012    let mut seen = HashSet::new();
1013    for (index, identifier) in values.iter().enumerate() {
1014        let field = format!("{field}[{index}]");
1015        validate_capability_identifier(&field, identifier)?;
1016        if !seen.insert(identifier) {
1017            return Err(CapabilityGrammarError::new(field, identifier));
1018        }
1019    }
1020    Ok(())
1021}
1022
1023fn validate_requires(values: &[CapabilityRequirement]) -> Result<(), CapabilityGrammarError> {
1024    let mut seen = HashSet::new();
1025    for (index, requirement) in values.iter().enumerate() {
1026        let field = format!("capabilities.requires[{index}].capability");
1027        validate_capability_identifier(&field, &requirement.capability)?;
1028        if !seen.insert(&requirement.capability) {
1029            return Err(CapabilityGrammarError::new(
1030                format!("capabilities.requires[{index}]"),
1031                &requirement.capability,
1032            ));
1033        }
1034    }
1035    Ok(())
1036}
1037
1038fn validate_capability_identifier(
1039    field: &str,
1040    identifier: &str,
1041) -> Result<(), CapabilityGrammarError> {
1042    if is_valid_capability_identifier(identifier) {
1043        Ok(())
1044    } else {
1045        Err(CapabilityGrammarError::new(field, identifier))
1046    }
1047}
1048
1049fn validate_runtime_computed(
1050    value: Option<&Value>,
1051    field: &str,
1052) -> Result<(), CapabilityGrammarError> {
1053    let Some(value) = value else {
1054        return Ok(());
1055    };
1056    let Some(pointers) = value.as_array() else {
1057        return Err(CapabilityGrammarError::new(field, value_description(value)));
1058    };
1059
1060    for (index, pointer) in pointers.iter().enumerate() {
1061        let field = format!("{field}[{index}]");
1062        let Some(pointer) = pointer.as_str() else {
1063            return Err(CapabilityGrammarError::new(
1064                field,
1065                value_description(pointer),
1066            ));
1067        };
1068        let Some(tokens) = parse_json_pointer(pointer) else {
1069            return Err(CapabilityGrammarError::new(field, pointer));
1070        };
1071        if tokens.first().is_some_and(|token| token == "capabilities") {
1072            return Err(CapabilityGrammarError::new(field, pointer));
1073        }
1074    }
1075    Ok(())
1076}
1077
1078fn parse_json_pointer(pointer: &str) -> Option<Vec<String>> {
1079    if pointer.is_empty() {
1080        return Some(Vec::new());
1081    }
1082    let raw_tokens = pointer.strip_prefix('/')?;
1083    raw_tokens
1084        .split('/')
1085        .map(unescape_json_pointer_token)
1086        .collect()
1087}
1088
1089fn unescape_json_pointer_token(token: &str) -> Option<String> {
1090    let mut output = String::with_capacity(token.len());
1091    let mut characters = token.chars();
1092    while let Some(character) = characters.next() {
1093        if character != '~' {
1094            output.push(character);
1095            continue;
1096        }
1097        match characters.next()? {
1098            '0' => output.push('~'),
1099            '1' => output.push('/'),
1100            _ => return None,
1101        }
1102    }
1103    Some(output)
1104}
1105
1106fn field_child(parent: &str, child: &str) -> String {
1107    let child = safe_error_value(child);
1108    format!("{parent}.{child}")
1109}
1110
1111fn value_description(value: &Value) -> String {
1112    match value {
1113        Value::String(value) => safe_error_value(value),
1114        Value::Null => "null".to_string(),
1115        Value::Bool(value) => value.to_string(),
1116        Value::Number(value) => value.to_string(),
1117        Value::Array(_) => "<array>".to_string(),
1118        Value::Object(_) => "<object>".to_string(),
1119    }
1120}
1121
1122fn safe_error_value(value: &str) -> String {
1123    let lower = value.to_ascii_lowercase();
1124    if ["secret", "password", "api_key"]
1125        .iter()
1126        .any(|marker| lower.contains(marker))
1127        || lower.starts_with("sk-")
1128        || lower.starts_with("akia")
1129        || lower.starts_with("bearer ")
1130        || lower.starts_with("token=")
1131        || lower.starts_with("credential=")
1132    {
1133        "<redacted>".to_string()
1134    } else {
1135        value.to_string()
1136    }
1137}
1138
1139/// How this module was sourced, as declared by the module itself.
1140///
1141/// Not read on any daemon routing or admission path; relayed verbatim. A
1142/// module declares it because it describes the module, not because the
1143/// daemon consumes it, and leaves it absent rather than inventing a value.
1144#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1145#[serde(rename_all = "snake_case")]
1146pub enum TrustTier {
1147    FirstParty,
1148    Reviewed,
1149    Untrusted,
1150}
1151
1152/// Provider capabilities exposed by a module.
1153///
1154/// The role set is closed for protocol v1; unknown role tags fail serde decode.
1155#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1156#[serde(tag = "role", rename_all = "snake_case")]
1157pub enum ProviderRole {
1158    ToolProvider {
1159        tools: Vec<Tool>,
1160        /// Which `BindIdentity` keys PARTITION this provider's state or
1161        /// answers: a module whose reply to a call depends on the caller's
1162        /// project declares `Project`; one that threads per session declares
1163        /// `Session`; one that answers identically to every caller declares
1164        /// `[]`. It states what the module does with the keys it is handed,
1165        /// not which keys it will accept — every bind carries all of them.
1166        /// Not read on any daemon path; relayed verbatim for consumers.
1167        identity_scope: Vec<IdentityScope>,
1168        concurrency: Concurrency,
1169        emits_push: bool,
1170        sub_supervises: bool,
1171    },
1172    PipelineStage {
1173        stage: PipelineStageKind,
1174        applies_to: PipelineAppliesTo,
1175        interface: String,
1176        declares_frozen_floor: bool,
1177        needs_signals: Vec<String>,
1178        conformance_class: String,
1179    },
1180    ManagementSurface {
1181        operations: Vec<ManagementOperation>,
1182        config_schema: Value,
1183        observability: Vec<ObservabilitySurface>,
1184        /// Same meaning as on `ToolProvider`: the keys that partition this
1185        /// surface's state or answers; `[]` for a surface that serves the
1186        /// same answer to every caller.
1187        identity_scope: Vec<IdentityScope>,
1188        #[serde(default)]
1189        concurrency: Concurrency,
1190    },
1191    InternalService {
1192        service_id: String,
1193        transport: InternalTransport,
1194        agent_facing: bool,
1195        operations: Vec<String>,
1196    },
1197}
1198
1199/// How a tool's side effects are fenced for durable at-most-once handling.
1200///
1201/// Classified on a tool's externally-observable effects, never inferred from
1202/// the module's concurrency lane:
1203/// - `Pure`: no observable side effect (reads, searches, cache warming) — safe
1204///   to re-run after an indeterminate outcome.
1205/// - `Mutating`: a fenceable external side effect such as a file write — a
1206///   re-run risks a duplicate effect, so an indeterminate outcome must not
1207///   auto-retry.
1208/// - `Unfenceable`: a side effect that cannot be fenced or safely replayed,
1209///   such as running a shell command — never auto-re-run on an indeterminate
1210///   outcome.
1211#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1212#[serde(rename_all = "snake_case")]
1213pub enum ExecutionMode {
1214    Pure,
1215    Mutating,
1216    Unfenceable,
1217}
1218
1219/// Tool-plane capability exposed by a `tool_provider`.
1220#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1221pub struct Tool {
1222    pub name: String,
1223    #[serde(default, skip_serializing_if = "Option::is_none")]
1224    pub description: Option<String>,
1225    /// How the tool's side effects are fenced for durable at-most-once handling.
1226    /// Observability + durability metadata only; subc's thin core never acts on
1227    /// this for routing, scheduling, or concurrency — the module's declared
1228    /// [`Concurrency`] contract governs delivery.
1229    pub execution_mode: ExecutionMode,
1230    pub schema: Value,
1231}
1232
1233/// How subc may deliver concurrent in-flight calls to the provider.
1234///
1235/// subc records and forwards these semantics unchanged; the dispatcher that
1236/// enforces them lives in subc-core, kept separate from this frozen manifest
1237/// contract.
1238#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1239#[serde(rename_all = "snake_case")]
1240pub enum Concurrency {
1241    /// One in-flight call at a time with strict submission and response order.
1242    Serial,
1243    /// Concurrent in-flight calls may span channels, while subc preserves FIFO
1244    /// submission within each channel; the module schedules internally.
1245    ModuleManaged,
1246    /// Fully parallel delivery with no ordering guarantee across or within
1247    /// channels.
1248    StatelessParallel,
1249}
1250
1251#[allow(clippy::derivable_impls)]
1252// The default is PINNED BY HISTORY, not chosen as the best value. Before this
1253// field existed, every ManagementSurface received ModuleManaged delivery (32
1254// concurrent credits) unconditionally, so an absent-field manifest must resolve
1255// to exactly that behavior -- any other default (including the fail-closed
1256// Serial) would convert a daemon upgrade into a silent delivery-semantics
1257// change for every deployed module. A genuinely-Serial module was ALREADY
1258// receiving concurrent delivery under pre-field daemons; the field's addition
1259// is what makes declaring Serial possible at all, so the fix for such a module
1260// is an explicit declaration, and the daemon logs defaulted registrations so
1261// the fleet's exposure is readable rather than assumed.
1262impl Default for Concurrency {
1263    fn default() -> Self {
1264        Self::ModuleManaged
1265    }
1266}
1267
1268/// A `BindIdentity` key a provider partitions its state or answers by.
1269///
1270/// Declared in a role's `identity_scope` to say which caller keys change
1271/// what the module does; the daemon hands every bind all of the keys
1272/// regardless, so an empty declaration means "answers do not depend on the
1273/// caller", never "keys are refused".
1274#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1275#[serde(rename_all = "snake_case")]
1276pub enum IdentityScope {
1277    Session,
1278    Project,
1279}
1280
1281/// Proxy-plane stage kind.
1282#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1283#[serde(rename_all = "snake_case")]
1284pub enum PipelineStageKind {
1285    Transform,
1286    Codec,
1287    Auth,
1288}
1289
1290/// Provider/model selector for a pipeline stage. `"*"` denotes wildcard.
1291#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1292pub struct PipelineAppliesTo {
1293    pub provider: String,
1294    pub model: String,
1295}
1296
1297/// Operation exposed on the management plane.
1298#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1299pub struct ManagementOperation {
1300    pub name: String,
1301    pub kind: ManagementOperationKind,
1302    #[serde(default, skip_serializing_if = "Option::is_none")]
1303    pub description: Option<String>,
1304}
1305
1306#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1307#[serde(rename_all = "snake_case")]
1308pub enum ManagementOperationKind {
1309    Query,
1310    Mutate,
1311}
1312
1313/// Observable state exposed on the management plane.
1314#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1315pub struct ObservabilitySurface {
1316    pub name: String,
1317    pub kind: ObservabilityKind,
1318}
1319
1320#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1321#[serde(rename_all = "snake_case")]
1322pub enum ObservabilityKind {
1323    Snapshot,
1324    Stream,
1325}
1326
1327#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1328#[serde(rename_all = "snake_case")]
1329pub enum InternalTransport {
1330    Bulk,
1331}
1332
1333/// Consumer capabilities requested by a module.
1334#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1335#[serde(tag = "role", rename_all = "snake_case")]
1336pub enum ConsumerRole {
1337    ToolClient { of: Vec<String> },
1338    LlmClient { via: String, auth: String },
1339    ServiceClient { of: Vec<String> },
1340}
1341
1342/// External storage, vault, and identity bindings supplied through subc.
1343#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1344pub struct Bindings {
1345    pub storage: StorageBinding,
1346    pub vault_grants: Vec<VaultGrant>,
1347    pub identity: IdentityBinding,
1348}
1349
1350/// Storage backend supplied by subc; the module owns its schema.
1351#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1352pub struct StorageBinding {
1353    pub kind: StorageKind,
1354    pub scope: StorageScope,
1355    pub owns_schema: bool,
1356}
1357
1358#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1359#[serde(rename_all = "snake_case")]
1360pub enum StorageKind {
1361    Sqlite,
1362}
1363
1364#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1365#[serde(rename_all = "snake_case")]
1366pub enum StorageScope {
1367    Project,
1368}
1369
1370#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1371pub struct VaultGrant {
1372    pub secret: String,
1373    pub reason: String,
1374}
1375
1376#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1377pub struct IdentityBinding {
1378    pub requires: Vec<IdentityScope>,
1379    pub optional: Vec<IdentityScope>,
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384    use super::*;
1385    use serde_json::json;
1386
1387    fn aft_manifest_fixture() -> ModuleManifest {
1388        ModuleManifest::builder("aft", "0.39.2")
1389            .trust_tier(Some(TrustTier::FirstParty))
1390            .bindings(Some(Bindings {
1391                storage: StorageBinding {
1392                    kind: StorageKind::Sqlite,
1393                    scope: StorageScope::Project,
1394                    owns_schema: true,
1395                },
1396                vault_grants: vec![VaultGrant {
1397                    secret: "provider_api_key".to_string(),
1398                    reason: "cortexkit_native auth".to_string(),
1399                }],
1400                identity: IdentityBinding {
1401                    requires: vec![IdentityScope::Project],
1402                    optional: vec![IdentityScope::Session],
1403                },
1404            }))
1405            .protocol_ver(1)
1406            .provides(vec![ProviderRole::ToolProvider {
1407                tools: vec![
1408                    Tool {
1409                        name: "read".to_string(),
1410                        description: None,
1411                        execution_mode: ExecutionMode::Pure,
1412                        schema: json!({"type": "object"}),
1413                    },
1414                    Tool {
1415                        name: "grep".to_string(),
1416                        description: None,
1417                        execution_mode: ExecutionMode::Pure,
1418                        schema: json!({"type": "object"}),
1419                    },
1420                    Tool {
1421                        name: "outline".to_string(),
1422                        description: None,
1423                        execution_mode: ExecutionMode::Pure,
1424                        schema: json!({"type": "object"}),
1425                    },
1426                    Tool {
1427                        name: "semantic_search".to_string(),
1428                        description: None,
1429                        execution_mode: ExecutionMode::Pure,
1430                        schema: json!({"type": "object"}),
1431                    },
1432                    Tool {
1433                        name: "edit".to_string(),
1434                        description: None,
1435                        execution_mode: ExecutionMode::Mutating,
1436                        schema: json!({"type": "object"}),
1437                    },
1438                    Tool {
1439                        name: "write".to_string(),
1440                        description: None,
1441                        execution_mode: ExecutionMode::Mutating,
1442                        schema: json!({"type": "object"}),
1443                    },
1444                    Tool {
1445                        name: "bash".to_string(),
1446                        description: None,
1447                        execution_mode: ExecutionMode::Unfenceable,
1448                        schema: json!({"type": "object"}),
1449                    },
1450                ],
1451                identity_scope: vec![IdentityScope::Session, IdentityScope::Project],
1452                concurrency: Concurrency::ModuleManaged,
1453                emits_push: true,
1454                sub_supervises: true,
1455            }])
1456            .consumes(vec![ConsumerRole::ServiceClient {
1457                of: vec!["embedding.v2".to_string()],
1458            }])
1459            .build()
1460    }
1461
1462    #[test]
1463    fn serde_round_trips_representative_manifest() {
1464        let manifest = aft_manifest_fixture();
1465        let serialized = serde_json::to_string_pretty(&manifest).unwrap();
1466        let decoded: ModuleManifest = serde_json::from_str(&serialized).unwrap();
1467
1468        assert_eq!(manifest, decoded);
1469    }
1470
1471    #[test]
1472    fn builder_defaults_additions_to_honest_absence_and_round_trips() {
1473        let manifest = ModuleManifest::builder("builder-defaults", "2.0.0").build();
1474
1475        assert_eq!(manifest.module_id, "builder-defaults");
1476        assert_eq!(manifest.module_version, "2.0.0");
1477        assert_eq!(manifest.protocol_ver, PROTOCOL_VERSION);
1478        assert_eq!(manifest.trust_tier, None);
1479        assert!(manifest.provides.is_empty());
1480        assert!(manifest.consumes.is_empty());
1481        assert_eq!(manifest.bindings, None);
1482        assert_eq!(manifest.capabilities, None);
1483        assert_eq!(manifest.self_signals, None);
1484        assert_eq!(manifest.provenance, None);
1485
1486        let encoded = serde_json::to_value(&manifest).expect("builder manifest serializes");
1487        for optional in [
1488            "trust_tier",
1489            "consumes",
1490            "bindings",
1491            "capabilities",
1492            "self_signals",
1493            "provenance",
1494        ] {
1495            assert!(
1496                encoded.get(optional).is_none(),
1497                "an absent {optional} declaration must stay absent on the wire"
1498            );
1499        }
1500        let decoded: ModuleManifest =
1501            serde_json::from_value(encoded).expect("builder manifest round-trips");
1502        assert_eq!(decoded, manifest);
1503    }
1504
1505    #[test]
1506    fn fully_populated_builder_manifest_matches_the_literal_wire_golden() {
1507        let manifest = ModuleManifest::builder("full-builder", "2.0.0")
1508            .trust_tier(Some(TrustTier::Reviewed))
1509            .bindings(Some(Bindings {
1510                storage: StorageBinding {
1511                    kind: StorageKind::Sqlite,
1512                    scope: StorageScope::Project,
1513                    owns_schema: false,
1514                },
1515                vault_grants: Vec::new(),
1516                identity: IdentityBinding {
1517                    requires: vec![IdentityScope::Project],
1518                    optional: Vec::new(),
1519                },
1520            }))
1521            .provides(vec![ProviderRole::ToolProvider {
1522                tools: vec![Tool {
1523                    name: "read".to_string(),
1524                    description: None,
1525                    execution_mode: ExecutionMode::Pure,
1526                    schema: json!({"type": "object"}),
1527                }],
1528                identity_scope: vec![IdentityScope::Project],
1529                concurrency: Concurrency::Serial,
1530                emits_push: false,
1531                sub_supervises: false,
1532            }])
1533            .consumes(vec![ConsumerRole::ServiceClient {
1534                of: vec!["embedding.v2".to_string()],
1535            }])
1536            .capabilities(Some(CapabilityDeclarations {
1537                provides: vec!["embedding/v2".to_string()],
1538                requires: Vec::new(),
1539                must_never_reach: Vec::new(),
1540            }))
1541            .self_signals(Some(vec![SelfSignalDeclaration {
1542                name: "usage_poller".to_string(),
1543                kind: SelfSignalKind::Poller,
1544                effect: SelfSignalEffect::Observe,
1545                anchored_to: SignalAnchor::FixedInterval,
1546                cadence: Some(SignalCadence::Literal {
1547                    interval_ms: 60_000,
1548                }),
1549                domain: Some("provider-usage".to_string()),
1550                note: None,
1551            }]))
1552            .provenance(Some(ManifestProvenance {
1553                build_git_sha: Some("0123456789abcdef0123456789abcdef01234567".to_string()),
1554                build_lock_digest: Some(
1555                    "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(),
1556                ),
1557                wire_crate_version: Some("0.16.0".to_string()),
1558                store_schema_version: Some("42".to_string()),
1559            }))
1560            .build();
1561
1562        assert_eq!(
1563            serde_json::to_vec(&manifest).expect("builder manifest serializes"),
1564            include_bytes!("../tests/golden/module_manifest_builder_full.json"),
1565            "the builder must preserve the prior fully populated literal wire bytes"
1566        );
1567    }
1568
1569    #[test]
1570    fn old_manifest_with_unread_fields_decodes_and_round_trips_verbatim() {
1571        let raw = include_bytes!("../tests/golden/module_manifest_builder_full.json");
1572        let decoded: ModuleManifest =
1573            serde_json::from_slice(raw).expect("old manifest with all unread fields decodes");
1574
1575        assert_eq!(decoded.trust_tier, Some(TrustTier::Reviewed));
1576        assert!(!decoded.consumes.is_empty());
1577        assert!(decoded.bindings.is_some());
1578
1579        let reencoded = serde_json::to_vec(&decoded).expect("re-encode succeeds");
1580        assert_eq!(
1581            reencoded, raw,
1582            "old manifest relay stays byte-for-byte verbatim"
1583        );
1584    }
1585
1586    #[test]
1587    fn new_manifest_omits_unread_fields_on_wire_and_decodes_cleanly() {
1588        let raw = include_bytes!("../tests/golden/module_manifest_diet.json");
1589        let decoded: ModuleManifest =
1590            serde_json::from_slice(raw).expect("new manifest omitting unread fields decodes");
1591
1592        assert_eq!(decoded.trust_tier, None);
1593        assert!(decoded.consumes.is_empty());
1594        assert_eq!(decoded.bindings, None);
1595
1596        let pretty = format!("{}\n", serde_json::to_string_pretty(&decoded).unwrap());
1597        assert_eq!(
1598            pretty.as_bytes(),
1599            raw,
1600            "new manifest matches golden byte-for-byte without unread keys"
1601        );
1602
1603        let as_val: serde_json::Value = serde_json::to_value(&decoded).unwrap();
1604        assert!(
1605            as_val.get("trust_tier").is_none(),
1606            "no trust_tier on wire for new manifest"
1607        );
1608        assert!(
1609            as_val.get("consumes").is_none(),
1610            "no consumes on wire for empty consumes"
1611        );
1612        assert!(
1613            as_val.get("bindings").is_none(),
1614            "no bindings on wire for new manifest"
1615        );
1616    }
1617
1618    #[test]
1619    fn aft_manifest_fixture_matches_v1_contract() {
1620        let manifest = aft_manifest_fixture();
1621
1622        assert_eq!(manifest.module_id, "aft");
1623        let ProviderRole::ToolProvider {
1624            tools,
1625            identity_scope,
1626            concurrency,
1627            emits_push,
1628            sub_supervises,
1629        } = &manifest.provides[0]
1630        else {
1631            panic!("AFT fixture must expose one tool_provider role");
1632        };
1633
1634        assert_eq!(*concurrency, Concurrency::ModuleManaged);
1635        assert!(*emits_push);
1636        assert!(*sub_supervises);
1637        assert_eq!(
1638            identity_scope,
1639            &vec![IdentityScope::Session, IdentityScope::Project]
1640        );
1641        assert_eq!(
1642            tools
1643                .iter()
1644                .map(|tool| (tool.name.as_str(), tool.execution_mode))
1645                .collect::<Vec<_>>(),
1646            vec![
1647                ("read", ExecutionMode::Pure),
1648                ("grep", ExecutionMode::Pure),
1649                ("outline", ExecutionMode::Pure),
1650                ("semantic_search", ExecutionMode::Pure),
1651                ("edit", ExecutionMode::Mutating),
1652                ("write", ExecutionMode::Mutating),
1653                ("bash", ExecutionMode::Unfenceable),
1654            ]
1655        );
1656    }
1657
1658    #[test]
1659    fn tool_provider_role_tag_serializes_as_snake_case() {
1660        let manifest = aft_manifest_fixture();
1661        let value = serde_json::to_value(&manifest).unwrap();
1662
1663        assert_eq!(value["provides"][0]["role"], "tool_provider");
1664    }
1665
1666    #[test]
1667    fn manifest_without_capabilities_preserves_the_existing_wire_shape() {
1668        let manifest = aft_manifest_fixture();
1669        let encoded = serde_json::to_value(&manifest).expect("manifest serializes");
1670        assert!(encoded.get("capabilities").is_none());
1671
1672        let decoded: ModuleManifest =
1673            serde_json::from_value(encoded).expect("legacy manifest parses");
1674        assert_eq!(decoded.capabilities, None);
1675    }
1676
1677    #[test]
1678    fn capability_identifier_lexical_grammar_accepts_only_pinned_forms() {
1679        for identifier in [
1680            "a/v1",
1681            "credentials-provider/v1",
1682            "a1-b2/v4294967295",
1683            "a123456789012345678901234567890123456789012345678901234567890123/v1",
1684        ] {
1685            assert!(
1686                is_valid_capability_identifier(identifier),
1687                "identifier must be accepted: {identifier}"
1688            );
1689        }
1690
1691        for identifier in [
1692            "credentials-Provider/v1",
1693            "credentials-provider/v01",
1694            "credentials-provider-/v1",
1695            "credentials--provider/v1",
1696            "Credentials-provider/v1",
1697            "credentials-provider/1",
1698            "credentials provider/v1",
1699            "credentials-provider/v0",
1700            "credentials-provider/v4294967296",
1701            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/v1",
1702        ] {
1703            assert!(
1704                !is_valid_capability_identifier(identifier),
1705                "identifier must be rejected: {identifier}"
1706            );
1707        }
1708    }
1709
1710    #[test]
1711    fn capability_grammar_errors_redact_secret_shaped_values() {
1712        let error = validate_manifest_capability_grammar(&json!({
1713            "capabilities": { "provides": ["sk-secret-value/v0"] }
1714        }))
1715        .expect_err("secret-shaped capability identifier is malformed");
1716        assert_eq!(error.field(), "capabilities.provides[0]");
1717        assert_eq!(error.value(), "<redacted>");
1718        assert!(!error.to_string().contains("sk-secret-value"));
1719    }
1720
1721    /// Builder sentinels are the strings tooling emits where it means "no
1722    /// value" (shell fallbacks say `unknown`, not `unavailable`); publishing
1723    /// one as a build fact is the well-formed lie the provenance contract
1724    /// names. The helper must map every sentinel, any casing, to field
1725    /// omission — and must keep a canonical real value intact (the control arm,
1726    /// so the filter cannot pass by refusing everything).
1727    #[test]
1728    fn provenance_builder_sentinels_become_field_omission() {
1729        for sentinel in [
1730            "unknown",
1731            "UNKNOWN",
1732            "Unknown",
1733            "unavailable",
1734            "none",
1735            "None",
1736            "  unknown  ",
1737            "",
1738        ] {
1739            let p = build_provenance(Some(sentinel), Some(sentinel), Some(sentinel))
1740                .expect("sentinels are omitted before form validation");
1741            assert_eq!(
1742                (p.build_git_sha, p.build_lock_digest, p.store_schema_version),
1743                (None, None, None),
1744                "sentinel {sentinel:?} must be omitted, not published"
1745            );
1746        }
1747        let real = build_provenance(
1748            Some("0123456789abcdef0123456789abcdef01234567"),
1749            None,
1750            Some("9"),
1751        )
1752        .expect("canonical build revision is accepted");
1753        assert_eq!(
1754            real.build_git_sha.as_deref(),
1755            Some("0123456789abcdef0123456789abcdef01234567")
1756        );
1757        assert_eq!(real.store_schema_version.as_deref(), Some("9"));
1758        // The always-knowable fact: an SDK-built block always carries a crate
1759        // version, so it is never empty; that is why the contract omits a
1760        // field when it is absent rather than publishing a sentinel.
1761        assert_eq!(
1762            real.wire_crate_version.as_deref(),
1763            Some(crate::SUBC_PROTOCOL_CRATE_VERSION)
1764        );
1765    }
1766
1767    #[test]
1768    fn build_provenance_accepts_canonical_sha_and_lock_digest() {
1769        let provenance = build_provenance(
1770            Some(" 0123456789abcdef0123456789abcdef01234567 "),
1771            Some(" abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 "),
1772            Some(" schema-v3 "),
1773        )
1774        .expect("canonical build facts are accepted");
1775
1776        assert_eq!(
1777            provenance,
1778            ManifestProvenance {
1779                build_git_sha: Some("0123456789abcdef0123456789abcdef01234567".to_string()),
1780                build_lock_digest: Some(
1781                    "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(),
1782                ),
1783                wire_crate_version: Some(crate::SUBC_PROTOCOL_CRATE_VERSION.to_string()),
1784                store_schema_version: Some("schema-v3".to_string()),
1785            }
1786        );
1787    }
1788
1789    #[test]
1790    fn build_provenance_refuses_an_abbreviated_git_sha() {
1791        let error = build_provenance(Some("0123456789ab"), None, None)
1792            .expect_err("a 12-character abbreviation is not canonical");
1793
1794        assert_eq!(error.field(), "build_git_sha");
1795        assert_eq!(error.length(), 12);
1796        assert_eq!(error.canonical_form(), BUILD_GIT_SHA_CANONICAL_FORM);
1797        assert_eq!(
1798            error.to_string(),
1799            "invalid manifest provenance form: field build_git_sha has length 12; canonical form is exactly 40 lowercase hexadecimal characters"
1800        );
1801    }
1802
1803    #[test]
1804    fn build_provenance_refuses_an_abbreviated_lock_digest() {
1805        let error = build_provenance(None, Some("0123456789abcdef"), None)
1806            .expect_err("a 16-character digest is not canonical");
1807
1808        assert_eq!(error.field(), "build_lock_digest");
1809        assert_eq!(error.length(), 16);
1810        assert_eq!(error.canonical_form(), BUILD_LOCK_DIGEST_CANONICAL_FORM);
1811    }
1812
1813    #[test]
1814    fn build_provenance_refuses_uppercase_hex() {
1815        let uppercase_sha = "A".repeat(40);
1816        let error = build_provenance(Some(&uppercase_sha), None, None)
1817            .expect_err("uppercase hexadecimal is not canonical");
1818
1819        assert_eq!(error.field(), "build_git_sha");
1820        assert_eq!(error.length(), 40);
1821        assert_eq!(error.canonical_form(), BUILD_GIT_SHA_CANONICAL_FORM);
1822    }
1823
1824    #[test]
1825    fn build_provenance_refuses_dirty_revision_stamp() {
1826        let error = build_provenance(
1827            Some("0123456789abcdef0123456789abcdef01234567-dirty"),
1828            None,
1829            None,
1830        )
1831        .expect_err("a dirty stamp is not a canonical build revision");
1832
1833        assert_eq!(error.field(), "build_git_sha");
1834        assert_eq!(error.length(), 46);
1835        assert_eq!(error.canonical_form(), BUILD_GIT_SHA_CANONICAL_FORM);
1836    }
1837
1838    #[test]
1839    fn build_provenance_keeps_a_lock_digest_when_identity_is_unavailable() {
1840        let provenance = build_provenance(
1841            Some("unavailable"),
1842            Some("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"),
1843            None,
1844        )
1845        .expect("sentinel SHA is omitted before the valid lock digest is checked");
1846
1847        assert_eq!(provenance.build_git_sha, None);
1848        assert_eq!(
1849            provenance.build_lock_digest,
1850            Some("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string())
1851        );
1852        assert_eq!(
1853            provenance.wire_crate_version,
1854            Some(crate::SUBC_PROTOCOL_CRATE_VERSION.to_string())
1855        );
1856    }
1857
1858    #[test]
1859    fn build_provenance_omits_fully_unavailable_inputs() {
1860        let provenance = build_provenance(None, Some(" unavailable "), Some("   "))
1861            .expect("omitted and sentinel inputs are not form errors");
1862
1863        assert_eq!(provenance.build_git_sha, None);
1864        assert_eq!(provenance.build_lock_digest, None);
1865        assert_eq!(provenance.store_schema_version, None);
1866        assert_eq!(
1867            provenance.wire_crate_version,
1868            Some(crate::SUBC_PROTOCOL_CRATE_VERSION.to_string())
1869        );
1870    }
1871}