Three places where joule-ui let a figure be read as more than it was. The `Meter` trait defaulted `provenance()` to `HwShunt`. Any meter that did not think to override it, a deterministic stub included, had its figures sealed as hardware readings with `measured_uj` set. The default is now `Estimator`: a meter opts in to `ModelBased` for a model and to `HwShunt` only for readings of a hardware counter it has seen to be live. In this workspace the only meters that are hardware readings are the two `SpanMeter` adapters (the examples' and joule-ui-serve's), and both already return their span's own grade, so nothing that reads hardware changes. The stubs in the joule-ui-interaction, joule-ui-serve and joule-ui-mcp tests and the `metered_surface` example now come out `Estimator`, as they should. The conformance pack's `StepMeter` now declares `HwShunt` explicitly. C2 and C4 were sealed with it standing in for a hardware counter, so they carry `measured_uj` and exercise that path of the signed core; the explicit override, documented as a fixture grade that attests nothing (like the pack's steward key), is what keeps them byte-identical. SPEC §5 and §8 and the conformance README say so. `Authorizer::charge(action, measured_uj: u64)` handed the authorizer a bare number, so an authorizer that keeps its own record of the debit (a JCP grant) could not tell an estimate from a reading; the `jcp_metered` example worked around it by reading the provenance back off the meter. `charge` now takes `&Energy`, the exact figure the interaction is sealed with, provenance included. Every implementor and caller in the workspace is updated, and `jcp_metered`'s authorizer maps the charged figure's own provenance onto `jcp_schema::Energy` instead of holding an `Rc` to the meter. `run_interaction` turned a missing sample into `Energy::estimator(0)`. Work that ran was sealed at 0 µJ and charged nothing, so on a host with no reading a budget never bound, which SPEC §6 says is exactly what the budget is for. Now, when either sample is `None`, the handler's own wall-clock time is charged at the meter's `estimator_milliwatts()` (default `DEFAULT_ESTIMATOR_MILLIWATTS` = 20 W, the Sandbox estimator's default), rounded up and never 0 (`estimate_uj`), graded `Estimator`. A denial, whose handler never ran, still records 0 µJ. The signed core and its profile tag are unchanged. SPEC: §5 states the default and carries the shared vendor-counter liveness MUST verbatim (HwShunt only while the counter is demonstrably live for the domain it claims; for IOReport the CPU rail advancing in >= 5 intervals, in >= 4 of every 5, over >= 300 ms while the probe burns CPU; a frozen or lumped counter never yields a hardware-graded figure; no claim of a domain the counter does not read). The "real RAPL/IOReport" wording at §5 and "the hardware Meter" at §9 now describe the reference meter as a hardware reading while a vendor counter is live and its labelled estimate otherwise. §6.2 specifies the no-reading estimate and forbids recording work that ran at 0 µJ; §6.3 becomes "charge the cost, with its provenance". Tests (joule-ui-interaction, new): `a_meter_is_an_estimator_unless_it_opts_in`, `work_that_ran_without_a_reading_is_estimated_never_zero`, `the_estimate_is_wall_time_at_the_declared_power_rounded_up`, `a_budget_binds_on_a_host_with_no_reading` (a 1 J budget stops ~20 ms interactions at 20 W within three, every debit `Estimator`), and `charge_is_handed_the_sealed_figure_and_its_provenance`. All 31 tests across joule-ui-interaction, -conformance (incl. the published-pack tests), -serve and -mcp pass; the workspace builds with all targets; the three examples run (`jcp_metered` here: five ~3 J `Estimator` interactions spend the 15 J grant and the real JCP runtime denies the sixth). `gen_vectors` regenerated into a scratch directory is byte-identical to `conformance/v1/` (all five files, MANIFEST included). Falsified, each restored and re-run green: - default back to `HwShunt` with the fixture override removed: the vectors regenerate identical (under the old default they rested on the default), and `a_meter_is_an_estimator_unless_it_opts_in`, `charge_is_handed_the_sealed_figure_and_its_provenance`, joule-ui-serve's `firing_an_action_meters_seals_and_returns_the_next_surface` and joule-ui-mcp's `an_mcp_app_action_is_metered_sealed_and_attributed` fail; - new default, override removed: `c2_interaction_is_reproduced_from_the_pack` and `c2_interaction_is_metered_and_allowed` fail, and the regenerated c2, c4 and MANIFEST differ (`"measured_uj": null, "provenance": "Estimator"`), so the override is what keeps the pack; - `charge` handed `Energy::measured(uj)` regardless of grade: `charge_is_handed_the_sealed_figure_and_its_provenance` and `a_budget_binds_on_a_host_with_no_reading` fail; - the no-reading arm back to `Energy::estimator(0)`: `work_that_ran_without_a_reading_is_estimated_never_zero` and `a_budget_binds_on_a_host_with_no_reading` fail. Clippy --no-deps on the changed crates adds nothing; the remaining warnings are in untouched code (joule-ui-core's tests, the conformance pack test). Not done here: joule-ui's energy figure has no coverage field, and both `SpanMeter` adapters grade a span whose only records are hardware GPU records (NVML without a CPU probe) `HwShunt`, which claims the whole interaction. §5's coverage clause forbids that; fixing it needs either a coverage field in the signed core (a new profile tag) or a downgrade rule, which is a decision, not a cleanup. Co-Authored-By: Claude Opus 5.5 <[email protected]>
3.3 KiB
joule-ui
A typed declarative protocol for agent-emitted user interfaces.
joule-ui is the sibling to JouleClaw for the moment when an
agent's resolution has to become something a human can see and interact
with. The agent emits a widget tree — (name, props, children),
recursively — against a widget registry the host has published. The
host renders. No React in the spec, no HTML over the wire, no agent
holding the renderer.
The shape the field has converged on for this is declarative components against a registry: agents are far more reliable emitting structured data than emitting code, and renderers are far easier to harden when they own their own widgets. joule-ui pins that shape as a protocol; the renderer is product surface, owned by the consumer.
What ships in v1
-
joule-ui-rs/crates/joule-ui-core/— the protocol types (Widget,PropValue,WidgetSchema,Registry) and the recursive validator. Pure Rust, no IO, JSON-serialisable. -
joule-ui-rs/crates/joule-ui-interaction/(v0.2) — the layer no other UI protocol carries: when a human or an agent fires anAction, the invocation is an accountable event. Every fire names the principal that made it — adid:key, the same whether a person or an agent (the protocol draws no distinction; both are traced).run_interactionauthorizes it against a capability before it runs (a denied action's handler never executes), meters it in joules, charges its cost (a reading or an estimate, and which), and anInteractionLogseals it as the next link of a hash-chained receipt. The actor is bound into the signed core, so an interaction cannot be re-attributed without breaking the chain. A joule-ui session becomes a tamper-evident ledger of who fired it, what it computed, and what it cost — verifiable by anyone holding the host's key. The metering (SandboxPowerMeter) and the capability (JCP grant) are recognized edges; the receipt, the chain, and the lifecycle are joule-ui's. SeeSPEC.md§5–§7 andexamples/metered_surface.rsfor the full loop.
The surface layer makes joule-ui a peer of the declarative-UI protocols (A2UI, MCP Apps). The interaction layer makes it the only one you can hand to an auditor: a UI whose every interaction is energy-accounted and sealed, by construction.
What is not here:
- A renderer. The renderer is owned by the host (browser app, native shell, terminal UI). joule-ui validates the surface and seals the interaction; it does not paint pixels.
- A transport. The validated spec and the interaction stream ride MCP, REST, SSE, gRPC, Web MoQ — any bytes-out protocol. Transport-agnostic.
- A "model emits React" path. The agent emits a declarative tree against the registry; arbitrary code emission is out of scope.
Why a sibling, not inside JouleClaw
The spec layer is protocol — small, stable, conformant. The renderer registry is product surface — many of them, evolving fast. Splitting them protects the tools for use line: JouleClaw stays the runtime substrate; joule-ui is the UI protocol; consumers ship their own widgets.
Licence
Specification text and protocol document: CC-BY-4.0 (see LICENSE).
Reference implementation in joule-ui-rs/: Apache-2.0.