Skip to main content

harn_kernel/execution/
types.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use harn_builtin_meta::Ty;
4use serde::{Deserialize, Serialize};
5
6use super::diagnostic;
7use super::resource::{validate_data_value, validate_json_value};
8use crate::{type_contract::manifest_signature_is_portable, Diagnostic};
9
10const MAX_JSON_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
11
12#[derive(Debug, Clone, PartialEq)]
13pub enum DataValue {
14    Nil,
15    Bool(bool),
16    Int(i64),
17    Float(f64),
18    String(String),
19    Bytes(Vec<u8>),
20    List(Vec<DataValue>),
21    Record(BTreeMap<String, DataValue>),
22}
23
24// DataValue's serde representation is intentionally identical to the public
25// JSON seam. Deriving serde here would turn non-finite floats into null and
26// would create a second, incompatible tagged representation for snapshots and
27// capability requests.
28impl Serialize for DataValue {
29    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
30    where
31        S: serde::Serializer,
32    {
33        self.to_json().serialize(serializer)
34    }
35}
36
37impl<'de> Deserialize<'de> for DataValue {
38    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
39    where
40        D: serde::Deserializer<'de>,
41    {
42        let value = serde_json::Value::deserialize(deserializer)?;
43        Self::from_json(value).map_err(|diagnostic| {
44            serde::de::Error::custom(format!("{}: {}", diagnostic.code, diagnostic.message))
45        })
46    }
47}
48
49impl DataValue {
50    pub fn from_json(value: serde_json::Value) -> Result<Self, Diagnostic> {
51        validate_json_value(&value)?;
52        Self::from_json_validated(value)
53    }
54
55    fn from_json_validated(value: serde_json::Value) -> Result<Self, Diagnostic> {
56        Ok(match value {
57            serde_json::Value::Null => Self::Nil,
58            serde_json::Value::Bool(value) => Self::Bool(value),
59            serde_json::Value::Number(value) => {
60                if let Some(value) = value.as_i64() {
61                    Self::Int(value)
62                } else {
63                    Self::Float(value.as_f64().ok_or_else(|| {
64                        diagnostic(
65                            "input_number",
66                            "JSON number is outside Harn's numeric range",
67                        )
68                    })?)
69                }
70            }
71            serde_json::Value::String(value) => Self::String(value),
72            serde_json::Value::Array(values) => Self::List(
73                values
74                    .into_iter()
75                    .map(Self::from_json_validated)
76                    .collect::<Result<_, _>>()?,
77            ),
78            serde_json::Value::Object(entries) => {
79                if entries.len() == 1 {
80                    let (tag, tagged) = entries.iter().next().expect("single entry");
81                    match (tag.as_str(), tagged) {
82                        ("$int", serde_json::Value::String(value)) => {
83                            return value.parse::<i64>().map(Self::Int).map_err(|_| {
84                                diagnostic(
85                                    "input_integer",
86                                    "tagged integer is outside the i64 range",
87                                )
88                            });
89                        }
90                        ("$int", _) => {
91                            return Err(diagnostic(
92                                "input_integer",
93                                "tagged integer must contain a decimal string",
94                            ));
95                        }
96                        ("$float", serde_json::Value::String(value)) => {
97                            return match value.as_str() {
98                                "nan" => Ok(Self::Float(f64::NAN)),
99                                "infinity" => Ok(Self::Float(f64::INFINITY)),
100                                "-infinity" => Ok(Self::Float(f64::NEG_INFINITY)),
101                                _ => Err(diagnostic(
102                                    "input_float",
103                                    "tagged float must be nan, infinity, or -infinity",
104                                )),
105                            };
106                        }
107                        ("$float", _) => {
108                            return Err(diagnostic(
109                                "input_float",
110                                "tagged float must contain a string",
111                            ));
112                        }
113                        ("$bytes", serde_json::Value::Array(values)) => {
114                            let mut bytes = Vec::with_capacity(values.len());
115                            for value in values {
116                                let Some(value) =
117                                    value.as_u64().and_then(|value| u8::try_from(value).ok())
118                                else {
119                                    return Err(diagnostic(
120                                        "input_bytes",
121                                        "tagged bytes must contain integers from 0 through 255",
122                                    ));
123                                };
124                                bytes.push(value);
125                            }
126                            return Ok(Self::Bytes(bytes));
127                        }
128                        ("$bytes", _) => {
129                            return Err(diagnostic(
130                                "input_bytes",
131                                "tagged bytes must contain an integer array",
132                            ));
133                        }
134                        _ => {}
135                    }
136                }
137                Self::Record(
138                    entries
139                        .into_iter()
140                        .map(|(key, value)| Ok((key, Self::from_json_validated(value)?)))
141                        .collect::<Result<_, Diagnostic>>()?,
142                )
143            }
144        })
145    }
146
147    pub fn to_json(&self) -> serde_json::Value {
148        match self {
149            Self::Nil => serde_json::Value::Null,
150            Self::Bool(value) => (*value).into(),
151            Self::Int(value) if value.unsigned_abs() <= MAX_JSON_SAFE_INTEGER as u64 => {
152                (*value).into()
153            }
154            Self::Int(value) => serde_json::json!({"$int": value.to_string()}),
155            Self::Float(value) if value.is_nan() => serde_json::json!({"$float": "nan"}),
156            Self::Float(value) if *value == f64::INFINITY => {
157                serde_json::json!({"$float": "infinity"})
158            }
159            Self::Float(value) if *value == f64::NEG_INFINITY => {
160                serde_json::json!({"$float": "-infinity"})
161            }
162            Self::Float(value) => serde_json::Number::from_f64(*value)
163                .map(serde_json::Value::Number)
164                .expect("finite floats are representable as JSON numbers"),
165            Self::String(value) => value.clone().into(),
166            Self::Bytes(value) => serde_json::json!({"$bytes": value}),
167            Self::List(values) => {
168                serde_json::Value::Array(values.iter().map(Self::to_json).collect())
169            }
170            Self::Record(entries) => serde_json::Value::Object(
171                entries
172                    .iter()
173                    .map(|(key, value)| (key.clone(), value.to_json()))
174                    .collect(),
175            ),
176        }
177    }
178
179    pub(super) fn validate(&self) -> Result<(), Diagnostic> {
180        validate_data_value(self)
181    }
182}
183
184#[derive(Clone, Default, PartialEq, Eq)]
185pub struct GrantSet {
186    grants: BTreeSet<String>,
187    snapshot_key: Option<[u8; 32]>,
188}
189
190impl GrantSet {
191    pub fn pure() -> Self {
192        Self::default()
193    }
194
195    pub fn from_names(names: impl IntoIterator<Item = String>) -> Result<Self, Diagnostic> {
196        let grants = names.into_iter().collect::<BTreeSet<_>>();
197        for grant in &grants {
198            let Some((capability, operation)) = grant.split_once('.') else {
199                return Err(diagnostic(
200                    "invalid_capability_grant",
201                    format!("grant `{grant}` must name one exact capability operation"),
202                ));
203            };
204            let Some(contract) =
205                harn_capability_contracts::capability_method_entry(capability, operation)
206            else {
207                return Err(diagnostic(
208                    "unknown_capability_grant",
209                    format!("grant `{grant}` is not in the canonical capability registry"),
210                ));
211            };
212            if !manifest_signature_is_portable(contract.signature) {
213                return Err(diagnostic(
214                    "unsupported_portable_capability_type",
215                    format!(
216                        "grant `{grant}` uses a capability type outside the portable value contract"
217                    ),
218                ));
219            }
220        }
221        Ok(Self {
222            grants,
223            snapshot_key: None,
224        })
225    }
226
227    /// Decode the shared host grant contract used by native and Wasm adapters.
228    ///
229    /// A string list grants exact operations for a terminal execution. An
230    /// object additionally carries the host-owned snapshot authentication key
231    /// required for suspend/resume.
232    pub fn from_host_json(json: &str) -> Result<Self, Diagnostic> {
233        #[derive(Deserialize)]
234        #[serde(rename_all = "camelCase", deny_unknown_fields)]
235        struct HostGrants {
236            capabilities: Vec<String>,
237            snapshot_key: Option<Vec<u8>>,
238        }
239
240        let input: HostGrants = serde_json::from_str(json).map_err(|error| {
241            diagnostic(
242                "invalid_capability_grants",
243                format!("invalid grants JSON: {error}"),
244            )
245        })?;
246        let grants = Self::from_names(input.capabilities)?;
247        match input.snapshot_key {
248            Some(snapshot_key) => {
249                let key = decode_snapshot_key(&snapshot_key)?;
250                Ok(grants.with_snapshot_key(key))
251            }
252            None => Ok(grants),
253        }
254    }
255
256    /// Install a host-owned key that authenticates resumable snapshots.
257    ///
258    /// The key is never serialized by the kernel. A host that grants a
259    /// suspendable capability must retain the same key until execution ends.
260    pub fn with_snapshot_key(mut self, key: [u8; 32]) -> Self {
261        self.snapshot_key = Some(key);
262        self
263    }
264
265    pub(super) fn snapshot_key(&self) -> Option<&[u8; 32]> {
266        self.snapshot_key.as_ref()
267    }
268
269    pub(super) fn fingerprint(&self) -> [u8; 32] {
270        let mut hasher = blake3::Hasher::new();
271        for grant in &self.grants {
272            hasher.update(grant.as_bytes());
273            hasher.update(&[0]);
274        }
275        *hasher.finalize().as_bytes()
276    }
277
278    pub fn allows(&self, capability: &str, operation: &str) -> bool {
279        self.grants.contains(&format!("{capability}.{operation}"))
280    }
281}
282
283fn decode_snapshot_key(encoded: &[u8]) -> Result<[u8; 32], Diagnostic> {
284    if encoded.len() != 32 {
285        return Err(diagnostic(
286            "invalid_snapshot_key",
287            "snapshotKey must contain exactly 32 bytes",
288        ));
289    }
290    let mut key = [0_u8; 32];
291    key.copy_from_slice(encoded);
292    Ok(key)
293}
294
295impl std::fmt::Debug for GrantSet {
296    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        formatter
298            .debug_struct("GrantSet")
299            .field("grants", &self.grants)
300            .field(
301                "snapshot_key",
302                &self.snapshot_key.is_some().then_some("<redacted>"),
303            )
304            .finish()
305    }
306}
307
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
309pub struct CapabilityRequest {
310    pub id: String,
311    pub capability: String,
312    pub operation: String,
313    pub arguments: DataValue,
314    pub expected: ValueShape,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318#[serde(rename_all = "snake_case")]
319pub enum ValueShape {
320    Any,
321    Nil,
322    Bool,
323    Int,
324    Float,
325    String,
326    Bytes,
327    List,
328    Record,
329}
330
331impl ValueShape {
332    pub(super) fn from_type(ty: Ty) -> Self {
333        match ty {
334            Ty::Named("nil") | Ty::Never => Self::Nil,
335            Ty::Named("bool") => Self::Bool,
336            Ty::Named("int") | Ty::LitInt(_) => Self::Int,
337            Ty::Named("float") => Self::Float,
338            Ty::Named("string") | Ty::LitString(_) => Self::String,
339            Ty::Named("bytes") => Self::Bytes,
340            Ty::Named("list") | Ty::Apply("list" | "List", _) => Self::List,
341            Ty::Named("dict" | "record") | Ty::Shape(_) | Ty::OpenShape(_, _) => Self::Record,
342            Ty::Optional(_)
343            | Ty::Any
344            | Ty::Generic(_)
345            | Ty::Named(_)
346            | Ty::Apply(_, _)
347            | Ty::Union(_)
348            | Ty::Fn(_, _)
349            | Ty::SchemaOf(_) => Self::Any,
350        }
351    }
352}
353
354#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
355#[serde(tag = "status", rename_all = "snake_case")]
356pub enum CapabilityResult {
357    Ok {
358        request_id: String,
359        value: DataValue,
360    },
361    Err {
362        request_id: String,
363        code: String,
364        message: String,
365    },
366}
367
368impl CapabilityResult {
369    pub(super) fn request_id(&self) -> &str {
370        match self {
371            Self::Ok { request_id, .. } | Self::Err { request_id, .. } => request_id,
372        }
373    }
374}
375
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377#[serde(tag = "status", rename_all = "snake_case")]
378pub enum Execution {
379    Completed {
380        value: DataValue,
381    },
382    Suspended {
383        request: CapabilityRequest,
384        snapshot: Vec<u8>,
385    },
386    Failed {
387        diagnostic: Diagnostic,
388    },
389}
390
391pub(super) fn value_kind(value: &DataValue) -> &'static str {
392    match value {
393        DataValue::Nil => "nil",
394        DataValue::Bool(_) => "bool",
395        DataValue::Int(_) => "int",
396        DataValue::Float(_) => "float",
397        DataValue::String(_) => "string",
398        DataValue::Bytes(_) => "bytes",
399        DataValue::List(_) => "list",
400        DataValue::Record(_) => "record",
401    }
402}