All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 1m39s
standards workspaces build + test / aac-rs (pull_request) Successful in 36s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 37s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 32s
standards workspaces build + test / map-rs (pull_request) Successful in 30s
standards workspaces build + test / mesh-rs (pull_request) Successful in 24s
standards workspaces build + test / openpay (pull_request) Successful in 1m23s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 1m27s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m48s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m20s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m12s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m9s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 2m32s
standards workspaces build + test / arl-rs (pull_request) Successful in 2m12s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 4m37s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m3s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 3m0s
standards workspaces build + test / sense-rs (pull_request) Successful in 3m11s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 4m30s
joule-ui-serve's action handlers and the joule-ui-interaction examples' handlers
are in-process work, verified rather than assumed: `Host::handle` calls the
handler inside `run_interaction`, on the worker thread that holds the host
lock, and nothing in joule-ui-serve or the examples spawns a process. So they
now meter with `PowerMeter::detect_in_process()`, and on a host whose
per-process CPU energy counter is live (both Apple Silicon hosts checked,
macOS 27.0.1, where the machine-wide CPU rail is frozen) each interaction is
sealed as a `HwShunt` reading of the energy the kernel attributes to this
process's threads over the interaction, instead of the 20 W wall-clock
estimate. Where the counter is not live the meter is `detect()`'s, as before.
The figure is this process's, not one thread's: in joule-ui-serve it includes
any worker that served a socket during the span.
joule-ui-serve: `main` serves with `served_power_meter()`, which is
`detect_in_process()`, and the end-to-end test now builds its meter from that
same function and asserts, where the counter is live, that the meter is the
process probe and that each ~24 ms served interaction is `HwShunt`.
The examples: `real_meter` and `jcp_metered` use `detect_in_process()` and
say which counter they read. With real readings an interaction of ~150 ms
costs a fraction of a joule, not the estimate's 3 J, so six fires no longer
spend `jcp_metered`'s 15 J grant; it now fires until the real JCP runtime
denies one (at most 200), printing the first five and the last three, so the
budget binds whatever the figures are. The grant itself is unchanged.
SPEC §5 names the per-process counter among the reference meter's sources;
the two Cargo comments say which meter each crate uses.
Tests: joule-ui-serve, -interaction, -conformance, -mcp, -core, -a2ui and
-render pass (72). The served interactions on this M5 Max: `Energy {
measured_uj: Some(25908), ... provenance: HwShunt }` and `Some(23193)`.
`gen_vectors` regenerated into a scratch directory is byte-identical to
`conformance/v1/` (`diff -r` clean). The examples, release build, here:
real_meter: render #1: allow · 195798 µJ [HwShunt] ... sealed chain: 3 links
verified, 584156 µJ total — hardware counter deltas (HwShunt), this
process's CPU energy over each interaction
jcp_metered: 82 interactions sealed: 81 allowed; 15001229 µJ sealed,
hardware counter deltas (HwShunt) from process-energy; 15000000 of the
15000000 µJ budget debited; 1 denied by the real JCP runtime once the budget
was spent.
(The per-interaction figure varied from 0.04 to 0.21 J: the handlers burn for
150 ms of wall time, and other builds were loading this machine.)
Falsified, restored from git and re-run green: `served_power_meter` returning
`detect()` fails `the_served_meter_never_seals_zero_or_an_estimate_as_a_reading`.
Rebased onto 7cf1f036 with the commit before it; this commit's files are
unchanged. joule-ui-interaction, -serve, -conformance, -mcp, -core, -a2ui
and -render pass, and `gen_vectors` regenerated into a scratch directory is
still byte-identical to `conformance/v1/`.
Rebased again, onto a16e36cf, with the commit before it; this commit's
files are unchanged. The seven joule-ui crates pass (72), and `gen_vectors`
regenerated into a scratch directory is still byte-identical to
`conformance/v1/`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
206 lines
11 KiB
Markdown
206 lines
11 KiB
Markdown
# joule-ui — Specification
|
||
|
||
| | |
|
||
|---|---|
|
||
| **Standard** | joule-ui |
|
||
| **Status** | Draft for Review |
|
||
| **Version** | 0.2.0 |
|
||
| **Licence** | Specification text CC-BY-4.0 ([`LICENSE-SPEC`](LICENSE-SPEC)); reference code Apache-2.0 ([`LICENSE`](LICENSE)) |
|
||
| **Reference** | [`joule-ui-rs/`](joule-ui-rs/) |
|
||
|
||
## §1 What joule-ui is
|
||
|
||
joule-ui is a typed protocol for **the moment a computation has to become
|
||
something a human sees and acts on** — and for making that moment *accountable*.
|
||
It has two layers, and the second is the one no other UI protocol carries:
|
||
|
||
1. **The surface (§2–§4).** A host publishes a **registry** of widgets it can
|
||
render; an agent or program emits a **typed widget tree** against it. The
|
||
host renders. No HTML over the wire, no code emission, no agent holding the
|
||
renderer. This is the shape the field converged on (A2UI, MCP Apps SEP-1865,
|
||
CopilotKit's declarative middle); joule-ui pins it as a protocol.
|
||
|
||
2. **The interaction (§5–§7).** When a human fires an action on that surface,
|
||
the invocation is an **accountable event**: authorized against a capability,
|
||
metered in joules, and sealed into a tamper-evident receipt. A joule-ui
|
||
session is therefore not a stream of opaque callbacks — it is a hash-chained
|
||
ledger of *what the user did, what it computed, and what it cost*, verifiable
|
||
by anyone holding the host's public key.
|
||
|
||
The surface layer makes joule-ui a peer of the declarative-UI protocols. The
|
||
interaction layer makes it the only one you can hand to an auditor.
|
||
|
||
## §2 The widget tree
|
||
|
||
A **Widget** is `(name, props, children)`, recursive. `name` selects a widget
|
||
in the host's registry; `props` is a map of typed values; `children` is an
|
||
ordered list of Widgets. The wire form is plain JSON.
|
||
|
||
A **PropValue** is a closed set: `Null`, `Bool`, `Number`, `Text`, `List`,
|
||
`Object`, and **`Action`** — an opaque action id (plus optional pure-data
|
||
payload) that the host binds to a handler at render time. There are no
|
||
closures, no expressions, and no code on the wire. An interaction (§5) is the
|
||
firing of an `Action`.
|
||
|
||
## §3 The registry
|
||
|
||
A **Registry** is the host's published catalog: for each widget name, a
|
||
**WidgetSchema** declaring its prop slots (name → type, required/optional), the
|
||
children it accepts (`None` / `Any` / a named allow-list), and whether
|
||
unknown props are permitted. The registry is the host's contract; the renderer
|
||
is the host's product surface and is out of scope for this standard.
|
||
|
||
## §4 Validation
|
||
|
||
`validate(widget, registry)` checks a tree against the registry and returns
|
||
**every** error, not just the first: unknown widget, unknown/missing/mistyped
|
||
prop, disallowed child. A conforming validator MUST be exhaustive and MUST be
|
||
pure (no IO). A surface that validates is renderable by any conforming host of
|
||
that registry; validation is the well-formedness attestation the surface layer
|
||
provides.
|
||
|
||
## §5 Energy and provenance
|
||
|
||
Every interaction carries a two-part energy figure in integer **microjoules**:
|
||
`measured_uj` (present only for a hardware reading) and `estimated_uj` (always
|
||
present), tagged with a **Provenance** — `HwShunt` (a hardware energy reading),
|
||
`ModelBased` (a calibrated apportionment), or `Estimator` (a coarse estimate).
|
||
This is the family's shared energy model; a figure is reported with the honesty
|
||
of how it was obtained, never promoted.
|
||
|
||
Measurement itself is a **recognized edge**, not part of this standard: a
|
||
`Meter` is any source of a cumulative microjoule counter, and it declares the
|
||
provenance of its readings. A meter's figures are `Estimator` unless the meter
|
||
declares otherwise: a meter opts in to `ModelBased` for a calibrated model, and
|
||
to `HwShunt` only for readings of a live hardware counter (below). A meter
|
||
MUST NOT declare `HwShunt` for figures that are not such readings — a
|
||
deterministic stub, a model, a replayed counter. (The conformance pack's
|
||
fixture meter, §8, is the one exception: it declares `HwShunt` only so the
|
||
vectors exercise the reading path of the signed core, and like the pack's
|
||
published steward key it attests nothing.) The sibling
|
||
**Sandbox** standard's `PowerMeter` is the reference meter: it reads vendor
|
||
energy counters (RAPL, NVML, Apple IOReport, and for work on its own process
|
||
the per-process CPU energy the OS attributes to it) while they are live,
|
||
apportions a reading to a process tree by CPU time (`ModelBased`), and falls
|
||
back to a wall-clock estimate, labelled `Estimator`, where no counter is
|
||
live. joule-ui defines what an interaction records; the host wires the meter
|
||
it trusts.
|
||
|
||
**Vendor energy counters.** A reading from a vendor energy counter (RAPL,
|
||
NVML, Apple IOReport, ...) MAY be graded as a hardware reading only while the
|
||
counter is demonstrably live for the domain the figure claims: the
|
||
implementation MUST have observed the counter covering that domain advance
|
||
under load (for Apple IOReport: the CPU energy rail itself advancing in at
|
||
least 5 sample intervals, in at least 4 of every 5, over at least 300 ms while
|
||
the probing thread burns CPU). A counter that is present but not live — frozen,
|
||
or republished in lumps — MUST NOT produce a hardware-graded figure; the
|
||
implementation falls back to its estimator or reports the work unmetered, and
|
||
SHOULD record why. A figure MUST NOT claim coverage of a domain its counter
|
||
does not read; where the format has a coverage field, such a figure is
|
||
`Partial`. joule-ui's energy record has no coverage field, so a figure from a
|
||
counter that does not read the domain the work ran on — a GPU counter alone,
|
||
for a handler that ran on the CPU — MUST NOT be graded `HwShunt` for the
|
||
interaction: the reference adapters seal such a span as the meter's
|
||
`Estimator` figure for its wall time.
|
||
|
||
## §6 The interaction lifecycle
|
||
|
||
`run_interaction` is the protocol in one operation, in three phases:
|
||
|
||
1. **Authorize before running.** The action is checked against a capability
|
||
*before* its handler executes. A denied action's handler **MUST NOT run**.
|
||
The capability check is a recognized edge: an **Authorizer** is any
|
||
two-phase `(authorize, charge)` gate, and the sibling **JCP** standard's
|
||
runtime — gating on a signed, joule-budgeted grant — is the reference one.
|
||
2. **Meter the handler.** The handler runs once; the meter is sampled before
|
||
and after; the difference is the interaction's energy, graded with the
|
||
meter's declared provenance (§5). When the meter gives no reading — either
|
||
sample is absent — the handler's wall-clock duration is charged at the
|
||
meter's declared estimator power (20 W unless the meter declares its own),
|
||
rounded up to whole microjoules, and the figure is `Estimator`. A handler
|
||
that ran MUST NOT be recorded at 0 µJ for want of a reading: work that ran
|
||
is not free, and a figure of zero would let a budget never bind on a host
|
||
without a counter. A denied action's handler never ran, and its record
|
||
carries 0 µJ `Estimator`.
|
||
3. **Charge the cost, with its provenance.** The interaction's figure — a
|
||
reading or an estimate — is debited from the capability's remaining budget,
|
||
and the authorizer is handed the figure's provenance with it. So the budget
|
||
binds on every host, settled after the fact against the energy the work
|
||
cost as nearly as the host can know it; and an authorizer that keeps its own
|
||
record of the debit MUST record it with that provenance, never an estimate
|
||
as a reading.
|
||
|
||
The result is an **Interaction**: the surface's content address (what the user
|
||
was looking at), the action id, the content addresses of the inputs and the
|
||
outputs (the genesis sentinel when denied), the verdict, the energy, the budget
|
||
left, and the grant id. Inputs and outputs are content-addressed, not carried,
|
||
so the receipt commits to *what was supplied and produced* without embedding it.
|
||
|
||
## §7 The sealed, hash-chained interaction log
|
||
|
||
An **InteractionLog** seals each interaction as the next link of a chain. The
|
||
**canonical core** (§7.1) is signed with the host's Ed25519 key; each link's
|
||
`prev` is the content address of the previous link's core, and `seq` increments
|
||
from zero. **Allows and denies are both chained** — a denial excised from the
|
||
record is exactly the audit a malicious host would drop, so excising,
|
||
reordering, or splicing any interaction MUST break the chain at that point.
|
||
`verify_chain` checks every signature, the sequence, and every `prev` link, and
|
||
reports the first break.
|
||
|
||
### §7.1 The canonical core
|
||
|
||
The signed bytes are a fixed-order, length-prefixed concatenation: the profile
|
||
tag `joule-ui/interaction/2`, then surface, action, **actor**, inputs, outputs,
|
||
verdict (and deny reason, if any), provenance, `estimated_uj`, `measured_uj` (if
|
||
present), `budget_remaining_uj` (if present), `grant_id` (if present), `seq`,
|
||
and `prev`. Each field is preceded by its little-endian `u64` byte length.
|
||
|
||
The **actor** is the principal that fired the interaction — the `did:key:z…` of
|
||
a human or an agent (the standard makes no distinction; both are traced the
|
||
same). It is **mandatory**: every sealed interaction names who fired it, and
|
||
that name is bound into the signed core, so an interaction cannot be attributed
|
||
to a different principal without breaking the chain. A host with no per-client
|
||
authentication attributes fires to its own session principal (by convention its
|
||
own `did:key`). The profile tag is `…/interaction/2` precisely because the actor
|
||
is now part of the preimage — a `…/1` core (no actor) and a `…/2` core are
|
||
distinct signed bytes and never collide.
|
||
Deterministic and dependency-light; the BLAKE3 of this core is the link's
|
||
content address. (A host MAY additionally express a sealed interaction as a
|
||
JCR-1 / in-toto+DSSE envelope for cross-family verification — a recognized
|
||
edge, not required.)
|
||
|
||
## §8 Conformance
|
||
|
||
- **C1 — surface validation.** Given a registry and a widget tree, reproduce
|
||
the exhaustive error list (empty for a valid tree).
|
||
- **C2 — interaction metering.** Given a surface, action, actor, inputs, a stub
|
||
meter, and a handler, reproduce the Interaction: verdict, energy, content
|
||
addresses, and the sealed actor. The stub meter declares `HwShunt` as a
|
||
fixture grade, so the recorded energy carries `measured_uj` (§5).
|
||
- **C3 — authorization.** A denied action produces a deny Interaction whose
|
||
outputs are the genesis sentinel and whose handler did not run; the actor is
|
||
still named on the denial.
|
||
- **C4 — chain integrity.** Seal a sequence; `verify_chain` accepts it and
|
||
rejects any excision, reorder, or field mutation at the first broken link.
|
||
|
||
## §9 Recognized edges
|
||
|
||
joule-ui is self-contained at its core and composes outward at named edges:
|
||
the **Sandbox** standard supplies the reference `Meter` (a hardware reading
|
||
while a vendor counter is live, its labelled estimate otherwise); **JCP**
|
||
supplies the budgeted-grant `Authorizer`; **JouleContract** / in-toto+DSSE
|
||
supply an optional cross-family receipt envelope; and any transport (MCP, REST,
|
||
SSE, gRPC, Web MoQ) carries the validated surface and the interaction stream.
|
||
None are required to use the protocol; each is the reference answer where a
|
||
host needs that edge.
|
||
|
||
## §10 Non-goals
|
||
|
||
A renderer (the host's), a transport (the consumer's), a component library
|
||
(product surface), and code emission (an agent emits a typed tree against the
|
||
registry, never a renderer). joule-ui pins the protocol and the receipt; the
|
||
fast-moving surfaces stay the consumer's.
|
||
|
||
---
|
||
*joule-ui v0.2.0 — the surface layer (v0.1) plus the metered, sealed
|
||
interaction layer that makes a UI accountable by construction.*
|