//! `wave48_ui_streaming` — generative-UI ladder + streaming //! substrate composed end-to-end. //! //! Three UI outputs ship on the `text.out` channel of a duplex //! voice stream — one per rung of the ladder: //! //! 1. **Static** — `weather.card` with props. Passes the //! [`StaticUiOracle`] gate against a small registry. //! 2. **Declarative** — an `a2ui/1` descriptor with `kind: card`. //! Passes the [`DeclarativeUiOracle`]'s allow-list. //! 3. **Generative** — raw HTML asking for `clipboard.write` only; //! passes the [`GenerativeUiOracle`]'s `CapabilityPolicy`. //! 4. **Generative (denied)** — same HTML but asks for `net.fetch`. //! Fails the policy. The receipt records the denial. //! //! Each render mints a JCR-1 `ui-output/1` receipt; the receipts //! correlate to the parent stream via `stream_id` + `window_id`, //! so an auditor reading both the `stream-window/1` AND //! `ui-output/1` receipts can reconstruct the full session. //! //! Run: //! cargo run -p jouleclaw-stack --example wave48_ui_streaming use jouleclaw_jcr1::{UiOutputReceipt, UiRungWire, UiVerdictWire}; use jouleclaw_oracle::{Candidate, Oracle, Verdict}; use jouleclaw_ui_oracle::{DeclarativeUiOracle, GenerativeUiOracle, StaticUiOracle}; use jouleclaw_ui_output::{ CapabilityPolicy, ComponentRegistry, RegisteredComponent, SandboxCapability, UiOutput, }; use serde_json::json; const STREAM_ID: &str = "stream-demo"; const STEWARD: &str = "did:keri:EJ9testStewardAID000000000000000"; fn build_registry() -> ComponentRegistry { let mut r = ComponentRegistry::new(); r.register(RegisteredComponent { id: "weather.card".into(), description: "renders a weather card from props".into(), }); r.register(RegisteredComponent { id: "list.simple".into(), description: "renders a flat list of strings".into(), }); r } fn build_policy() -> CapabilityPolicy { CapabilityPolicy::allow([SandboxCapability::new("clipboard.write")]) } async fn gate_with( oracle: &dyn Oracle, ui: &UiOutput, ) -> (UiVerdictWire, Vec) { let content = serde_json::to_string(ui).expect("ui serialises"); let candidate = Candidate { id: "demo".into(), content, language: "json".into(), source_tier: "stream".into(), provenance: "wave-48-demo".into(), }; match oracle.verify(&candidate).await.expect("oracle") { Verdict::Pass { chain } => { let oracle_id = chain.first().cloned().unwrap_or_default(); ( UiVerdictWire::Pass { oracle_id: oracle_id.clone(), }, chain, ) } Verdict::Fail { reasons } => { let r = reasons.into_iter().next(); let (subkind, message) = match r { Some(jouleclaw_oracle::FailReason::Custom { subkind, message }) => { (subkind, message) } Some(other) => ("unknown".into(), format!("{other:?}")), None => ("unknown".into(), String::new()), }; (UiVerdictWire::Fail { subkind, message }, vec![]) } } } fn mint_receipt( receipt_id: &str, ui: &UiOutput, verdict: UiVerdictWire, window_id: u64, ) -> UiOutputReceipt { let rung = match ui { UiOutput::Static { .. } => UiRungWire::Static, UiOutput::Declarative { .. } => UiRungWire::Declarative, UiOutput::Generative { .. } => UiRungWire::Generative, }; let content_hash = ui.content_hash().expect("hash"); let byte_length = ui.payload_bytes().expect("bytes") as u64; let granted: Vec = match ui { UiOutput::Generative { capabilities, .. } if matches!(verdict, UiVerdictWire::Pass { .. }) => { capabilities.iter().map(|c| c.to_string()).collect() } _ => vec![], }; UiOutputReceipt::build( receipt_id, format!("2026-06-05T00:00:{:02}Z", window_id % 60), STEWARD, rung, content_hash, byte_length, granted, verdict, STREAM_ID, window_id, ) } #[tokio::main(flavor = "current_thread")] async fn main() { println!("== wave-48 UI streaming demo (ladder × streaming substrate) ==\n"); let registry = build_registry(); let policy = build_policy(); let static_oracle = StaticUiOracle::new("static-ui-gate", registry); let declarative_oracle = DeclarativeUiOracle::new( "declarative-ui-gate", "a2ui/1", vec!["card".into(), "text".into(), "list".into()], ); let generative_oracle = GenerativeUiOracle::new("generative-ui-gate", policy) .deny_substring("eval(") .deny_substring("document.write("); // The four scripted UI outputs. let outputs: Vec<(u64, &str, UiOutput, &dyn Oracle)> = vec![ ( 3, "static — registered weather card", UiOutput::Static { component_id: "weather.card".into(), props: json!({"city": "Paris", "temp_c": 18}), }, &static_oracle as &dyn Oracle, ), ( 4, "declarative — a2ui/1 card descriptor", UiOutput::Declarative { schema_id: "a2ui/1".into(), descriptor: json!({ "kind": "card", "title": "Reminder", "children": [{"kind": "text", "value": "demo at 14:00"}] }), }, &declarative_oracle as &dyn Oracle, ), ( 5, "generative — minimal HTML asking for clipboard.write", UiOutput::Generative { language: "html".into(), source: "" .into(), capabilities: vec![SandboxCapability::new("clipboard.write")], }, &generative_oracle as &dyn Oracle, ), ( 6, "generative DENIED — asks for net.fetch (not in policy)", UiOutput::Generative { language: "html".into(), source: "" .into(), capabilities: vec![SandboxCapability::new("net.fetch")], }, &generative_oracle as &dyn Oracle, ), ]; let mut total_pass = 0usize; let mut total_fail = 0usize; let mut total_bytes: u64 = 0; for (window_id, label, ui, oracle) in &outputs { let (verdict, _chain) = gate_with(*oracle, ui).await; let pass = matches!(verdict, UiVerdictWire::Pass { .. }); if pass { total_pass += 1; } else { total_fail += 1; } let receipt = mint_receipt( &format!("{}:ui:{:04}", STREAM_ID, window_id), ui, verdict.clone(), *window_id, ); let bytes = ui.payload_bytes().unwrap_or(0) as u64; total_bytes += bytes; let receipt_bytes = receipt.canonical_bytes().expect("canonical"); let receipt_hash = receipt.receipt_hash().expect("hash"); let tag = if pass { "PASS " } else { "FAIL " }; let rung_tag = ui.rung(); let detail = match &verdict { UiVerdictWire::Pass { oracle_id } => format!("oracle={oracle_id}"), UiVerdictWire::Fail { subkind, .. } => format!("subkind={subkind}"), }; println!( " w{:02} {} [{:<11}] ({:>4} B) receipt {} B hash={}", window_id, tag, rung_tag, bytes, receipt_bytes.len(), &receipt_hash[..16] ); println!(" {label}"); println!(" {detail}\n"); } println!("== summary =="); println!( " outputs: {} | pass={} fail={} ui-bytes-on-wire={} B", outputs.len(), total_pass, total_fail, total_bytes ); println!(); println!("✓ UI ladder + streaming substrate compose end-to-end:"); println!( " UiOutput (Static | Declarative | Generative) → per-rung Oracle →" ); println!(" JCR-1 ui-output/1 receipt correlated to stream + window"); }