open-standards/joule-ui/SPEC.md
Transaction Science 079fabafe8 joule-ui: make the interaction actor mandatory (SPEC /2)
Every sealed interaction now names the principal that fired it. The actor
is a required field on Interaction, a required argument to run_interaction,
and is bound into the signed canonical core — so an interaction can no
longer be attributed to a different principal without breaking the chain.

Human and agent are traced identically (a did:key either way); the standard
draws no distinction. An unauthenticated host attributes fires to its own
session principal — joule-ui-serve defaults this to the host's own did:key
(derived from its signing key) and exposes with_session_actor for
deployments that authenticate the caller.

The canonical-core profile tag is bumped joule-ui/interaction/1 -> /2,
since the actor is now part of the signed preimage; the two cores are
distinct bytes and never collide. SPEC §7.1 + C2/C3 updated, conformance
v1 vectors regenerated, examples and the conformance harness pass an actor.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 22:19:18 -04:00

163 lines
8.4 KiB
Markdown
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.

# 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 from a hardware interface) and `estimated_uj`
(always present), tagged with a **Provenance** — `HwShunt` (a hardware energy
reading), `ModelBased` (a calibrated apportionment), or `Estimator` (a coarse
constant). 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 the sibling
**Sandbox** standard's `PowerMeter` (real RAPL/IOReport, and CPU-time
apportionment) is the reference one. joule-ui defines what an interaction
records; the host wires the meter it trusts.
## §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. With no hardware
meter the figure is honestly `Estimator`.
3. **Charge the measured cost.** The measured joules are debited from the
capability's remaining budget — so the budget that stops a runaway UI is
energy *actually spent*, settled after the fact, not a token estimate.
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.
- **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 hardware `Meter`; **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.*