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]>
68 lines
3.3 KiB
Markdown
68 lines
3.3 KiB
Markdown
# joule-ui
|
||
|
||
A typed declarative protocol for **agent-emitted user interfaces**.
|
||
|
||
joule-ui is the sibling to [JouleClaw](../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`](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`](LICENSE)).
|
||
Reference implementation in `joule-ui-rs/`: Apache-2.0.
|