From 3c1c6bae8c037627f8610aafa2a3a4f1c56ac567 Mon Sep 17 00:00:00 2001 From: Edward Tan Date: Sat, 26 Sep 2026 14:34:21 -0700 Subject: [PATCH 1/2] feat(l0): add bounded host state snapshots --- Cargo.lock | 1 + crates/octoscript-ui-l0/Cargo.toml | 1 + crates/octoscript-ui-l0/src/lib.rs | 70 ++++++++++++++++++- crates/octoscript-ui-l0/src/value_origin.rs | 3 +- .../octoscript-ui-l0/tests/state_snapshot.rs | 16 +++++ 5 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 crates/octoscript-ui-l0/tests/state_snapshot.rs diff --git a/Cargo.lock b/Cargo.lock index 997186a..0d91ba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -604,6 +604,7 @@ name = "octoscript-ui-l0" version = "0.1.0" dependencies = [ "blake3", + "serde", "serde_json", ] diff --git a/crates/octoscript-ui-l0/Cargo.toml b/crates/octoscript-ui-l0/Cargo.toml index 7a26550..680e078 100644 --- a/crates/octoscript-ui-l0/Cargo.toml +++ b/crates/octoscript-ui-l0/Cargo.toml @@ -8,6 +8,7 @@ license = "Apache-2.0" # No VM dependency: hosts can use the profile across Makepad lineages. [dependencies] serde_json = "1" +serde = { version = "1", features = ["derive"] } blake3 = "1.8" [dev-dependencies] diff --git a/crates/octoscript-ui-l0/src/lib.rs b/crates/octoscript-ui-l0/src/lib.rs index f00a7e8..b260c5a 100644 --- a/crates/octoscript-ui-l0/src/lib.rs +++ b/crates/octoscript-ui-l0/src/lib.rs @@ -9385,7 +9385,7 @@ pub const CARD_STATE_KEY: &str = "@card"; /// Nothing in the store is authored by the model. A card cannot name an /// instance, cannot address another instance's cell, and cannot write anything /// except through a declared transition on its own state. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] pub struct InstanceStore { /// `instance-key` → field → value. cells: BTreeMap>, @@ -9398,6 +9398,74 @@ pub struct InstanceStore { } impl InstanceStore { + /// Versioned, bounded host snapshot. The host must keep these bytes in a + /// host-owned directory: origins are trusted metadata, not app input. + pub fn snapshot_bytes(&self) -> Result, String> { + self.check_snapshot_limits()?; + let bytes = serde_json::to_vec(&serde_json::json!({"schema": 1, "state": self})) + .map_err(|e| e.to_string())?; + if bytes.len() > 1_048_576 { + return Err("Card state snapshot exceeds 1 MiB".into()); + } + Ok(bytes) + } + + /// Read only snapshots written by the host. The next realization still + /// reconciles component schema and prunes cells that are no longer live. + pub fn from_snapshot_bytes(bytes: &[u8]) -> Result { + if bytes.len() > 1_048_576 { + return Err("Card state snapshot exceeds 1 MiB".into()); + } + #[derive(serde::Deserialize)] + #[serde(deny_unknown_fields)] + struct Snapshot { + schema: u32, + state: InstanceStore, + } + let snapshot: Snapshot = serde_json::from_slice(bytes).map_err(|e| e.to_string())?; + if snapshot.schema != 1 { + return Err("unsupported Card state snapshot schema".into()); + } + snapshot.state.check_snapshot_limits()?; + Ok(snapshot.state) + } + + fn check_snapshot_limits(&self) -> Result<(), String> { + if self.cells.len() > 1024 || self.schemas.len() > 128 || self.shapes.len() > 128 { + return Err("Card state snapshot has too many instances or schemas".into()); + } + let mut fields = 0usize; + let mut value_bytes = 0usize; + for (key, values) in &self.cells { + if key.len() > 256 { + return Err("Card state instance key is too long".into()); + } + fields = fields.saturating_add(values.len()); + if fields > 8192 || values.keys().any(|field| field.len() > 256) { + return Err("Card state snapshot has too many or oversized fields".into()); + } + for (value, _) in values.values() { + value_bytes = value_bytes + .saturating_add(serde_json::to_vec(value).map_err(|e| e.to_string())?.len()); + if value_bytes > 1_048_576 { + return Err("Card state snapshot exceeds 1 MiB".into()); + } + } + } + if self.schemas.keys().any(|name| name.len() > 256) + || self.shapes.iter().any(|(name, paths)| { + name.len() > 256 + || paths.len() > 8192 + || paths + .iter() + .any(|(path, shape)| path.len() > 256 || shape.len() > 4096) + }) + { + return Err("Card state snapshot has oversized schemas".into()); + } + Ok(()) + } + pub fn get(&self, key: &str, field: &str) -> Option<&serde_json::Value> { self.cells.get(key)?.get(field).map(|(v, _)| v) } diff --git a/crates/octoscript-ui-l0/src/value_origin.rs b/crates/octoscript-ui-l0/src/value_origin.rs index 7d603e3..19cdebd 100644 --- a/crates/octoscript-ui-l0/src/value_origin.rs +++ b/crates/octoscript-ui-l0/src/value_origin.rs @@ -3,7 +3,8 @@ use crate::*; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] pub enum ValueOrigin { #[default] Authored, diff --git a/crates/octoscript-ui-l0/tests/state_snapshot.rs b/crates/octoscript-ui-l0/tests/state_snapshot.rs new file mode 100644 index 0000000..5e46334 --- /dev/null +++ b/crates/octoscript-ui-l0/tests/state_snapshot.rs @@ -0,0 +1,16 @@ +use octoscript_ui_l0::{InstanceStore, ValueOrigin}; + +#[test] +fn snapshot_roundtrip_keeps_cells_and_origins() { + let mut state = InstanceStore::default(); + state.set_cell_with_origin("@card", "note", "hello".into(), ValueOrigin::UserInput); + let bytes = state.snapshot_bytes().unwrap(); + let restored = InstanceStore::from_snapshot_bytes(&bytes).unwrap(); + assert_eq!(restored.get("@card", "note").unwrap(), "hello"); + assert_eq!( + restored.origin("@card", "note"), + Some(ValueOrigin::UserInput) + ); + assert!(InstanceStore::from_snapshot_bytes(b"{\"schema\":99}").is_err()); + assert!(InstanceStore::from_snapshot_bytes(&vec![b' '; 1_048_577]).is_err()); +} From 0fa86939f8a4cef144d253d87da601c4ce0f3b79 Mon Sep 17 00:00:00 2001 From: Edward Tan Date: Tue, 29 Sep 2026 19:11:15 -0700 Subject: [PATCH 2/2] fuzz: record serde for octoscript-ui-l0 in the lockfile --- fuzz/Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index ca99920..8b551a8 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -519,6 +519,7 @@ name = "octoscript-ui-l0" version = "0.1.0" dependencies = [ "blake3", + "serde", "serde_json", ]