Skip to main content

logicaffeine_compile/spatial/lower/
rdu_sdk.rs

1//! The SDK bridge kit — every plausible RDU front door behind ONE object-safe seam, so the day the vendor SDK
2//! arrives is a fill-in-the-blanks job in this file and nowhere else.
3//!
4//! A bridge has two halves. `ingest` turns the certified neutral artifact ([`NeutralArtifact`]) into whatever
5//! the SDK takes ([`VendorConfig`]) and MUST forward the proof: the config is stamped with the certificate's
6//! digest, and an artifact whose certificate does not recheck against its own program is refused before a
7//! byte leaves. `certify_back` takes whatever the SDK hands back ([`VendorPlacement`] — a placement of the
8//! same computation) and earns it the proof-carrying certificate through `certify_external_spatial`, or
9//! `None`, fail-closed.
10//!
11//! [`MockBridge`] is the identity (the neutral JSON bytes in, the neutral JSON parsed back out) — it exists
12//! so the round trip, the refusals and the CLI dispatch are locked TONIGHT against a bridge that behaves.
13//! [`SambaFlowBridge`] refuses every call by name until the vendor API exists.
14
15use std::fmt;
16
17use super::rdu::parse_neutral;
18use super::NeutralArtifact;
19use crate::spatial::certificate::{certify_external_spatial, ChoreographyCertificate};
20use crate::spatial::ir::SpatialProgram;
21
22/// The media type the mock bridge emits: the neutral artifact, byte for byte.
23pub const NEUTRAL_FORMAT: &str = "logos.spatial.neutral+json";
24
25/// Why the SambaFlow bridge refuses today — the one string the CLI, the runbook and the lock all read.
26pub const SAMBAFLOW_REASON: &str = "needs the vendor SDK";
27
28/// Every bridge `bridge_by_name` dispatches to, in the order the CLI lists them.
29pub const BRIDGE_NAMES: &[&str] = &["mock", "sambaflow"];
30
31/// What a bridge hands the vendor: the bytes in the SDK's format, stamped with the digest of the certificate
32/// they were proven under.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct VendorConfig {
35    pub format: String,
36    pub bytes: Vec<u8>,
37    pub certificate_digest: String,
38}
39
40/// What the vendor hands back: their placement of the computation, as a neutral envelope (`schema`,
41/// `version`, `lane`, `program` — exactly what `parse_neutral` reads).
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct VendorPlacement {
44    pub program_json: String,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum BridgeRefusal {
49    /// The bridge exists as a seam but the SDK behind it does not, yet.
50    NotImplemented {
51        bridge: &'static str,
52        reason: &'static str,
53    },
54    /// The artifact is not a neutral artifact.
55    Malformed(String),
56    /// The artifact's certificate does not recheck against the program it travels with.
57    Uncertified(String),
58}
59
60impl fmt::Display for BridgeRefusal {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            BridgeRefusal::NotImplemented { bridge, reason } => {
64                write!(f, "rdu bridge: {bridge} is not implemented — {reason}")
65            }
66            BridgeRefusal::Malformed(why) => {
67                write!(f, "rdu bridge: malformed neutral artifact — {why}")
68            }
69            BridgeRefusal::Uncertified(why) => {
70                write!(f, "rdu bridge: uncertified artifact — {why}")
71            }
72        }
73    }
74}
75
76impl std::error::Error for BridgeRefusal {}
77
78/// The digest a config carries: the certificate's schedule digest as sixteen lowercase hex digits.
79pub fn certificate_digest(cert: &ChoreographyCertificate) -> String {
80    format!("{:016x}", cert.schedule_digest)
81}
82
83/// The `certificate.schedule_digest` a neutral envelope carries, in the same sixteen-hex-digit form.
84pub fn envelope_digest(json: &str) -> Result<String, BridgeRefusal> {
85    let v: serde_json::Value =
86        serde_json::from_str(json).map_err(|e| BridgeRefusal::Malformed(e.to_string()))?;
87    let digest = v
88        .get("certificate")
89        .and_then(|c| c.get("schedule_digest"))
90        .and_then(serde_json::Value::as_u64)
91        .ok_or_else(|| {
92            BridgeRefusal::Malformed("no certificate.schedule_digest in the envelope".to_string())
93        })?;
94    Ok(format!("{digest:016x}"))
95}
96
97/// One RDU front door. Object-safe: the CLI holds a `Box<dyn RdaBridge>` chosen by name.
98pub trait RdaBridge {
99    fn name(&self) -> &str;
100    /// The neutral artifact → the SDK's config bytes, or a typed refusal. A bridge never emits without
101    /// forwarding the certificate: the artifact's own proof must recheck against its own program first.
102    fn ingest(&self, neutral: &NeutralArtifact) -> Result<VendorConfig, BridgeRefusal>;
103    /// The SDK's placement of `source`'s computation → a certificate on THEIR mapping, or `None`.
104    fn certify_back(
105        &self,
106        placement: &VendorPlacement,
107        source: &SpatialProgram,
108    ) -> Option<ChoreographyCertificate>;
109}
110
111/// The identity bridge: the config IS the neutral JSON, and a placement is read back with `parse_neutral`.
112pub struct MockBridge;
113
114impl RdaBridge for MockBridge {
115    fn name(&self) -> &str {
116        "mock"
117    }
118
119    fn ingest(&self, neutral: &NeutralArtifact) -> Result<VendorConfig, BridgeRefusal> {
120        let program = parse_neutral(&neutral.json).map_err(BridgeRefusal::Malformed)?;
121        let digest = certificate_digest(&neutral.certificate);
122        // The envelope's own digest must be the certificate's: the bytes forwarded and the proof they
123        // travel with are bound by that one number.
124        let in_envelope = envelope_digest(&neutral.json)?;
125        if in_envelope != digest {
126            return Err(BridgeRefusal::Uncertified(format!("the envelope names certificate digest {in_envelope} but the certificate beside it is {digest}")));
127        }
128        if !neutral.certificate.recheck(&program) {
129            return Err(BridgeRefusal::Uncertified(format!("the certificate (digest {digest}) does not recheck against the program in the artifact")));
130        }
131        Ok(VendorConfig {
132            format: NEUTRAL_FORMAT.to_string(),
133            bytes: neutral.json.clone().into_bytes(),
134            certificate_digest: digest,
135        })
136    }
137
138    fn certify_back(
139        &self,
140        placement: &VendorPlacement,
141        source: &SpatialProgram,
142    ) -> Option<ChoreographyCertificate> {
143        let external = parse_neutral(&placement.program_json).ok()?;
144        certify_external_spatial(&external, source)
145    }
146}
147
148/// The SambaFlow front door: a seam with the vendor API behind it still to come. Every call refuses by name.
149pub struct SambaFlowBridge;
150
151impl RdaBridge for SambaFlowBridge {
152    fn name(&self) -> &str {
153        "sambaflow"
154    }
155
156    fn ingest(&self, _neutral: &NeutralArtifact) -> Result<VendorConfig, BridgeRefusal> {
157        Err(BridgeRefusal::NotImplemented {
158            bridge: "sambaflow",
159            reason: SAMBAFLOW_REASON,
160        })
161    }
162
163    fn certify_back(
164        &self,
165        _placement: &VendorPlacement,
166        _source: &SpatialProgram,
167    ) -> Option<ChoreographyCertificate> {
168        None
169    }
170}
171
172/// The bridge behind a name, or `None` for a name that is not one of [`BRIDGE_NAMES`].
173pub fn bridge_by_name(name: &str) -> Option<Box<dyn RdaBridge>> {
174    match name {
175        "mock" => Some(Box::new(MockBridge)),
176        "sambaflow" => Some(Box::new(SambaFlowBridge)),
177        _ => None,
178    }
179}