open-standards/joule-ui/README.md
Transaction Science 7609bfeaef joule-ui: a meter is an estimator until it opts in, and a charge carries its grade
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]>
2026-09-30 20:10:14 -04:00

3.3 KiB
Raw Permalink Blame History

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 an Action, the invocation is an accountable event. Every fire names the principal that made it — a did:key, the same whether a person or an agent (the protocol draws no distinction; both are traced). run_interaction authorizes 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 an InteractionLog seals 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 (Sandbox PowerMeter) and the capability (JCP grant) are recognized edges; the receipt, the chain, and the lifecycle are joule-ui's. See SPEC.md §5–§7 and examples/metered_surface.rs for 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.