Skip to main content

active_infinity/
protobuf.rs

1//! Protobuf-wire interop projection.
2//!
3//! Active Infinity is the canonical form, and it is the form peers should
4//! exchange. The rest of the world, however, speaks Protobuf: dashboards,
5//! queues, analytics, tracing pipelines and every language with a generated
6//! client. This module renders a module into protobuf wire format so those
7//! consumers can read Active data without implementing the AINF layout, and so
8//! an Active producer can feed existing infrastructure directly.
9//!
10//! The projection is **redacted by construction**: it is built from the same
11//! [`crate::interop::Projected`] tree as the JSON projection, so secrets are
12//! replaced by [`crate::interop::SECRET_MARKER`] before a single byte is
13//! written. Blob payloads never leave the module; only their identity does.
14//!
15//! # Schema
16//!
17//! ```proto
18//! syntax = "proto3";
19//! package active.infinity.v1;
20//!
21//! message Module {
22//!   bytes content_hash = 1;   // Blake3-256 of the canonical AINF image
23//!   uint64 total_size = 2;    // canonical AINF image size in bytes
24//!   Value root = 3;           // omitted when the module has no root
25//!   repeated Binding bindings = 4;
26//!   Provenance provenance = 5; // omitted when absent
27//!   repeated Annotation annotations = 6;
28//!   repeated Extension extensions = 7;
29//! }
30//!
31//! message Binding { string name = 1; Value value = 2; }
32//!
33//! message Annotation {
34//!   uint32 subject = 1;      // node id
35//!   oneof kind {
36//!     string intent = 2;     // documentation only
37//!     Constraint constraint = 3;
38//!     bool ai_immutable = 4;
39//!   }
40//! }
41//! message Constraint { string source = 1; uint32 expr = 2; }
42//!
43//! message Provenance {
44//!   string producer = 1;
45//!   uint64 timestamp_ms = 2;
46//!   string model = 3;
47//!   string tool = 4;
48//!   bytes parent = 5;        // omitted when absent (32 bytes when present)
49//!   repeated Entry entries = 6;
50//!   message Entry {
51//!     string key = 1;
52//!     oneof value {
53//!       string text = 2;
54//!       sint64 integer = 3;
55//!       bool flag = 4;
56//!       bytes hash = 5;      // 32 bytes
57//!     }
58//!   }
59//! }
60//!
61//! message Extension { uint64 tag = 1; bytes payload = 2; }
62//!
63//! message Value {
64//!   oneof kind {
65//!     Null null_value = 1;
66//!     bool boolean = 2;
67//!     sint64 integer = 3;
68//!     double float = 4;
69//!     string text = 5;
70//!     Ms duration_ms = 6;
71//!     Quantity quantity = 7;
72//!     List list = 8;
73//!     Record record = 9;
74//!     Blob blob = 10;
75//!     Env env = 11;
76//!     Expr expr = 12;
77//!   }
78//! }
79//! message Null {}
80//! message Ms { uint64 ms = 1; }
81//! message Quantity { Value value = 1; string unit = 2; }
82//! message List { repeated Value items = 1; }
83//! message Record { repeated Field fields = 1; }
84//! message Field { string name = 1; Value value = 2; }
85//! message Blob {
86//!   bytes blake3 = 1;        // 32 bytes
87//!   uint64 size = 2;
88//!   string media_type = 3;
89//!   bool tainted = 4;
90//! }
91//! message Env { string name = 1; }
92//! message Expr { string kind = 1; }
93//! ```
94//!
95//! # Encoding rules
96//!
97//! Rewriting a module always produces identical bytes, which lets consumers
98//! hash, cache and diff the projection:
99//!
100//! - fields are emitted in ascending field-number order (`#[prost]`-generated
101//!   structs parse it, and so does every conformant reader);
102//! - scalar fields are emitted even when they equal the proto3 default, so the
103//!   encoding is injective for the projected tree;
104//! - absent optional messages (`root`, `provenance`) and empty repeated fields
105//!   are omitted;
106//! - varints are minimal, `sint64` uses zig-zag, `double` is little-endian
107//!   IEEE-754, and every length prefix is exact.
108
109use crate::encode::{encode, hash_image};
110use crate::error::Result;
111use crate::interop::{project_node_with, redacted_key, secret_table, Projected};
112use crate::ir::{AnnotationKind, InfinityModule, ProvValue};
113
114// ---------------------------------------------------------------------------
115// wire primitives
116// ---------------------------------------------------------------------------
117
118const WIRE_VARINT: u8 = 0;
119const WIRE_DOUBLE: u8 = 1;
120const WIRE_LEN: u8 = 2;
121
122fn varint(out: &mut Vec<u8>, mut value: u64) {
123    while value >= 0x80 {
124        out.push((value as u8) | 0x80);
125        value >>= 7;
126    }
127    out.push(value as u8);
128}
129
130fn tag(out: &mut Vec<u8>, field: u32, wire: u8) {
131    varint(out, (u64::from(field) << 3) | u64::from(wire));
132}
133
134fn uint(out: &mut Vec<u8>, field: u32, value: u64) {
135    tag(out, field, WIRE_VARINT);
136    varint(out, value);
137}
138
139fn sint(out: &mut Vec<u8>, field: u32, value: i64) {
140    tag(out, field, WIRE_VARINT);
141    varint(out, ((value << 1) ^ (value >> 63)) as u64);
142}
143
144fn boolean(out: &mut Vec<u8>, field: u32, value: bool) {
145    tag(out, field, WIRE_VARINT);
146    varint(out, u64::from(value));
147}
148
149fn double(out: &mut Vec<u8>, field: u32, value: f64) {
150    tag(out, field, WIRE_DOUBLE);
151    out.extend_from_slice(&value.to_bits().to_le_bytes());
152}
153
154fn bytes(out: &mut Vec<u8>, field: u32, value: &[u8]) {
155    tag(out, field, WIRE_LEN);
156    varint(out, value.len() as u64);
157    out.extend_from_slice(value);
158}
159
160fn string(out: &mut Vec<u8>, field: u32, value: &str) {
161    bytes(out, field, value.as_bytes());
162}
163
164fn message(out: &mut Vec<u8>, field: u32, body: &[u8]) {
165    bytes(out, field, body);
166}
167
168// ---------------------------------------------------------------------------
169// value tree
170// ---------------------------------------------------------------------------
171
172fn encode_value(value: &Projected, out: &mut Vec<u8>) {
173    match value {
174        Projected::Null => message(out, 1, &[]),
175        Projected::Bool(flag) => boolean(out, 2, *flag),
176        Projected::Int(number) => sint(out, 3, *number),
177        Projected::Float(number) => double(out, 4, *number),
178        Projected::Text(text) => string(out, 5, text),
179        Projected::DurationMs(ms) => {
180            let mut body = Vec::new();
181            uint(&mut body, 1, *ms);
182            message(out, 6, &body);
183        }
184        Projected::Quantity(value, unit) => {
185            let mut body = Vec::new();
186            let mut inner = Vec::new();
187            encode_value(value, &mut inner);
188            message(&mut body, 1, &inner);
189            string(&mut body, 2, unit);
190            message(out, 7, &body);
191        }
192        Projected::Array(items) => {
193            let mut body = Vec::new();
194            for item in items {
195                let mut encoded = Vec::new();
196                encode_value(item, &mut encoded);
197                message(&mut body, 1, &encoded);
198            }
199            message(out, 8, &body);
200        }
201        Projected::Record(fields) => {
202            let mut body = Vec::new();
203            for (name, value) in fields {
204                let mut field = Vec::new();
205                string(&mut field, 1, name);
206                let mut encoded = Vec::new();
207                encode_value(value, &mut encoded);
208                message(&mut field, 2, &encoded);
209                message(&mut body, 1, &field);
210            }
211            message(out, 9, &body);
212        }
213        Projected::Blob {
214            hash,
215            size,
216            media_type,
217            tainted,
218        } => {
219            let mut body = Vec::new();
220            bytes(&mut body, 1, hash);
221            uint(&mut body, 2, *size);
222            string(&mut body, 3, media_type);
223            boolean(&mut body, 4, *tainted);
224            message(out, 10, &body);
225        }
226        Projected::Env(name) => {
227            let mut body = Vec::new();
228            string(&mut body, 1, name);
229            message(out, 11, &body);
230        }
231        Projected::Expr(kind) => {
232            let mut body = Vec::new();
233            string(&mut body, 1, kind);
234            message(out, 12, &body);
235        }
236    }
237}
238
239// ---------------------------------------------------------------------------
240// module
241// ---------------------------------------------------------------------------
242
243/// Project `module` into protobuf wire format.
244///
245/// The result is deterministic and redacted. The `content_hash` field is the
246/// Blake3-256 of the canonical AINF image, so a consumer can request or verify
247/// the authoritative bytes with [`crate::validate`] and the usual content
248/// addressing.
249///
250/// A module that cannot be encoded canonically (a cycle, a dangling reference,
251/// a duplicate binding) is refused instead of being projected approximately.
252pub fn to_protobuf(module: &InfinityModule) -> Result<Vec<u8>> {
253    let image = encode(module)?;
254    let hash = hash_image(&image)?;
255    let secrets = secret_table(module);
256
257    let mut out = Vec::new();
258    bytes(&mut out, 1, &hash);
259    uint(&mut out, 2, image.len() as u64);
260
261    if let Some(root) = module.root() {
262        let value = project_node_with(module, root, &secrets)?;
263        let mut body = Vec::new();
264        encode_value(&value, &mut body);
265        message(&mut out, 3, &body);
266    }
267
268    for (name, id) in module.bindings() {
269        let value = project_node_with(module, *id, &secrets)?;
270        let mut binding = Vec::new();
271        string(&mut binding, 1, &redacted_key(name, &secrets));
272        let mut encoded = Vec::new();
273        encode_value(&value, &mut encoded);
274        message(&mut binding, 2, &encoded);
275        message(&mut out, 4, &binding);
276    }
277
278    if let Some(prov) = module.provenance() {
279        message(&mut out, 5, &encode_provenance(prov, &secrets));
280    }
281
282    for annotation in module.annotations() {
283        message(&mut out, 6, &encode_annotation(annotation, &secrets));
284    }
285
286    for (tag, payload) in module.extensions() {
287        let mut extension = Vec::new();
288        uint(&mut extension, 1, *tag);
289        bytes(&mut extension, 2, payload);
290        message(&mut out, 7, &extension);
291    }
292
293    Ok(out)
294}
295
296fn encode_annotation(annotation: &crate::ir::Annotation, secrets: &[String]) -> Vec<u8> {
297    let mut body = Vec::new();
298    uint(&mut body, 1, annotation.node.index() as u64);
299    match &annotation.kind {
300        AnnotationKind::Intent(text) => string(&mut body, 2, &redacted_key(text, secrets)),
301        AnnotationKind::Constraint { source, expr } => {
302            let mut constraint = Vec::new();
303            string(&mut constraint, 1, &redacted_key(source, secrets));
304            uint(&mut constraint, 2, expr.index() as u64);
305            message(&mut body, 3, &constraint);
306        }
307        AnnotationKind::AiImmutable(flag) => boolean(&mut body, 4, *flag),
308    }
309    body
310}
311
312fn encode_provenance(prov: &crate::ir::Provenance, secrets: &[String]) -> Vec<u8> {
313    let mut body = Vec::new();
314    string(
315        &mut body,
316        1,
317        &redacted_key(prov.producer.as_deref().unwrap_or(""), secrets),
318    );
319    uint(&mut body, 2, prov.timestamp_ms);
320    string(
321        &mut body,
322        3,
323        &redacted_key(prov.model.as_deref().unwrap_or(""), secrets),
324    );
325    string(
326        &mut body,
327        4,
328        &redacted_key(prov.tool.as_deref().unwrap_or(""), secrets),
329    );
330    if let Some(parent) = &prov.parent {
331        bytes(&mut body, 5, parent);
332    }
333    for entry in &prov.entries {
334        let mut encoded = Vec::new();
335        string(&mut encoded, 1, &redacted_key(&entry.key, secrets));
336        match &entry.value {
337            ProvValue::Text(text) => string(&mut encoded, 2, &redacted_key(text, secrets)),
338            ProvValue::Int(number) => sint(&mut encoded, 3, *number),
339            ProvValue::Flag(flag) => boolean(&mut encoded, 4, *flag),
340            ProvValue::Hash(hash) => bytes(&mut encoded, 5, hash),
341        }
342        message(&mut body, 6, &encoded);
343    }
344    body
345}