open-standards/joulehook/SPEC.md
Transaction Science 1d000aba5a joulehook: new open standard v0.1.0-draft.1 — metered/gated/attested agent work in any host
The host-binding layer for JCP + Mesh + jcp-efficiency: carry energy-metering,
capability-gating, and attested receipts into third-party AI coding agents
(Claude Code, Cursor, Windsurf, Antigravity, Zed, JetBrains, OpenCode) through
their own extension seams, without forking them. Grounded in research: no host
exposes its agent loop to extensions, but all expose three seams — a per-action
gate (PreToolUse-hook family / ACP request_permission), a model-call meter
(base_url proxy / OTel gen_ai.*), and a receipt surface. SPEC.md normalizes those
seams + defines conformance profiles (claude-code reference, hooks-family, acp,
opencode, cline, vscode, meter-only). §7 states the honest limit: a host adapter
alone cannot meter the host agent's tokens — a complete figure needs the meter seam.

Name is provisional (steward's call). Reference impl (joulehook-rs + the
claude-code plugin) lands next.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 05:42:45 -04:00

245 lines
12 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.

# JouleHook — Metered, Gated, Attested Agent Work in Any Host
**Version:** 0.1.0-draft.1 · **Status:** Draft · **Profile:** `joulehook/1`
**Specification text:** CC-BY-4.0 · **Reference code (`joulehook-rs/`):** Apache-2.0
> *Provisional name.* The steward may rename the standard; the profile string,
> receipt kind, and seam names are the stable surface.
JouleHook is the open standard for carrying **energy-metering**, **capability-gating**,
and **attested receipts** into a third-party AI coding agent or editor — through the
host's *own* extension seams, without forking it. It does not replace the host's
agent or its model. It binds the three seams that every host already exposes — a
**per-action gate**, a **model-call meter**, and a **receipt surface** — to a
decide-outside-the-model grant check, an honest energy meter, and a sealed receipt.
One agent's work becomes a metered, gated transaction — in Claude Code, Cursor,
Windsurf, Antigravity, Zed, JetBrains, or OpenCode — from one core.
JouleHook is one of the [Transaction Science open standards](../README.md).
Transaction Science is the steward, not the proprietor: the seam interfaces and the
right to fork are public. JouleHook **builds on** three sibling standards rather than
re-inventing them: **JCP** ([`../jcp`](../jcp)) supplies the grant + decision model,
**Mesh** ([`../mesh`](../mesh)) supplies the metering front door, and
**`jcp-efficiency`** supplies the `efficiency/1` receipt.
---
## §1 Why JouleHook exists
An energy-accounted, methodology-gated coding loop is worth little if it only runs
in one editor. Developers already live in Claude Code, Cursor, Antigravity, Zed, and
OpenCode, and **no host exposes its built-in agent loop to a third-party extension**:
you cannot, from an extension, observe the host agent's token consumption or
intercept its native file/shell tools. A naïve "one native plugin per editor that
wraps the agent" is therefore impossible by construction.
But every host *does* expose the same three seams, and JouleHook is the normative
contract over them:
1. A **gate** — the host lets an external process (or an in-process hook) approve,
deny, or rewrite a tool action *before it executes*. This is the
`PreToolUse`-hook family (Claude Code, Cursor, Windsurf, Antigravity, Cline,
OpenCode) and ACP `session/request_permission` (Zed, JetBrains, Neovim).
2. A **meter** — the one place every agent's tokens are authoritative and every
request can be refused is the **model-call boundary**: an OpenAI/Anthropic-shaped
proxy addressed by a `base_url` override, or the host's own OpenTelemetry
`gen_ai.*` export.
3. A **receipt surface** — a place to carry verifiable artifacts back to the user:
MCP resources, a status line, or a panel.
JouleHook standardizes the *shape* of what rides those seams, so a single core is
written once and bound to each host by a thin adapter.
---
## §2 The three seams (the whole standard)
| Seam | Binds to (host mechanisms) | JouleHook contract | Backed by |
|---|---|---|---|
| **Gate** (§3) | PreToolUse-hook family; ACP `request_permission` | Normalized `ActionDecision` (allow / deny / ask / rewrite) + sealed decision receipt | JCP grant (`../jcp`) |
| **Meter** (§4) | `base_url` proxy; OTel `gen_ai.*` ingest | `efficiency/1` per completed unit; joule-ceiling enforced at the model boundary | Mesh (`../mesh`), `jcp-efficiency` |
| **Receipt** (§5) | MCP resources; status line; panel | Hash-chained, verifiable `efficiency/1` + decision receipts | `jcp-efficiency`, `jcp-receipt` |
A conformant deployment binds **at least the Gate seam or the Meter seam**, MUST seal
receipts (§5), and MUST state honest provenance (§7). Binding all three is the
`full` profile (§6).
---
## §3 The Gate seam (normative)
### §3.1 The normalized action event
A host presents a *pending tool action* to the gate. JouleHook normalizes the host's
native schema into a host-agnostic `ActionEvent`:
| Field | Type | Meaning |
|---|---|---|
| `session_id` | string | The host session. |
| `tool` | string | The action's tool name (host-native or `mcp__<server>__<tool>`). |
| `input` | object | The tool's arguments (e.g. `{"command":"npm test"}`, `{"path":..,"content":..}`). |
| `cwd` | string | Working directory. |
| `content_id` | string \| null | Content id of the target, when derivable (a file, a command). |
An adapter MUST map its host's hook payload onto `ActionEvent` losslessly for the
fields the host provides, and MUST NOT fabricate a `content_id` it cannot derive.
### §3.2 The decision
The gate returns an `ActionDecision`:
- `allow` — the action proceeds unchanged.
- `deny` — the action is blocked; `reason` is surfaced to the agent.
- `ask` — a human is asked to approve (bound to the host's approval UI where one
exists; otherwise degrades to `deny`).
- `rewrite` — the action proceeds with `updated_input` substituted (e.g. a redaction).
The decision MUST be reached **outside the model** — it is a deterministic function of
the `ActionEvent` and the active JCP grant, never a request to the LLM (JCP §
*decide-in-the-runtime*). An adapter that asks the host's model to self-approve is
**non-conformant**.
### §3.3 The JCP binding
The gate decision is a JCP capability + budget check:
1. Resolve the active `SignedGrant` for the session. Verify its issuer signature.
2. The action `tool`×`content_id` MUST be permitted by the grant's capability set;
an action the grant does not hold is `deny`.
3. If the action carries an estimable cost, the grant's joule budget MUST be ≥ the
estimate; over budget is `deny`. On `allow`, the spent amount is debited.
4. The decision — `allow` and `deny` alike — is sealed (§5).
### §3.4 Host bindings (informative)
| Host | Gate mechanism | `deny` signal |
|---|---|---|
| Claude Code | `PreToolUse` hook (external process, JSON on stdin) | `permissionDecision:"deny"` / exit 2 — blocks even under `--dangerously-skip-permissions` |
| Cursor | `.cursor/hooks.json` (`beforeShellExecution`, `beforeMCPExecution`, …) | `permission:"deny"` / exit 2 |
| Windsurf | Cascade hooks (`pre_run_command`, `pre_write_code`, …) | exit 2 |
| Antigravity | `hooks.json` `PreToolUse` | `allow_tool:false` + `deny_reason` |
| OpenCode | `tool.execute.before` (in-process TS) | throw to deny / mutate args to rewrite |
| Cline | SDK `beforeTool` (`fail_closed`); file `PreToolUse` | `{cancel}` |
| Zed / JetBrains / Neovim | ACP `session/request_permission` | reject |
The four external-process hooks (Claude Code, Cursor, Windsurf, Antigravity) share a
JSON-in / decision-out shape; a single gate binary MAY serve all four behind a
per-host input adapter. OpenCode/Cline are served by a thin in-process shim that calls
the same core; Zed/JetBrains by an ACP agent (§6).
---
## §4 The Meter seam (normative)
### §4.1 The model-call boundary
Authoritative token accounting and enforceable spend control exist at exactly one
place common to every host: the model call. A JouleHook meter binds there by one of
two methods:
- **Proxy** *(enforcing)* — an OpenAI/Anthropic-shaped endpoint the host is pointed at
via `base_url` / `ANTHROPIC_BASE_URL`. It reads the authoritative `usage` on every
response, converts tokens→microjoules at honest provenance, and MAY **refuse** a
request whose estimated cost exceeds the active grant's joule ceiling (a hard
reject before the request reaches the provider). This is the Mesh front door
(`../mesh`); its `usage.energy` carries the receipt.
- **Telemetry** *(observing)* — ingest of the host's native OpenTelemetry GenAI
export (`gen_ai.usage.input_tokens` / `output_tokens`), where the host emits it
(Claude Code, VS Code/Copilot, Gemini CLI, Codex). Observe-only: it cannot refuse.
### §4.2 The unit and the ceiling
The meter MUST produce an `efficiency/1` receipt per **completed, verified unit of
work** (`jcp-efficiency`), carrying microjoules per accepted token at honest
provenance and the outcome anchor. A **joule ceiling** declared on the session grant
MUST be enforced by a Proxy meter (reject over budget) and, where only a Telemetry
meter is present, MAY be enforced only *after the fact* (the run is metered, not
capped) — which the adapter MUST disclose (§7).
---
## §5 The Receipt seam (normative)
Every gate decision and every completed unit seals as a receipt:
- **Decision receipts** — each `allow`/`deny` from §3, sealed as a JCP decision
receipt (non-repudiable, hash-chained).
- **`efficiency/1` receipts** — each completed unit from §4.
Receipts MUST be verifiable by a third party holding the steward's public key, MUST
hash-chain within a session, and SHOULD be surfaced through the host's receipt
surface (MCP resources, a status-line figure, or a panel). Where the meter is bound
via the Mesh proxy and the energy figure is attested (`sandbox-tee::energy`), the
receipt MAY carry the attestation commitment (`efficiency/1.energy_attestation`) and
its honest trust level.
---
## §6 Conformance profiles
A JouleHook adapter declares one or more profiles. Each names the seams it binds:
| Profile | Host(s) | Gate | Meter | Receipt |
|---|---|---|---|---|
| `claude-code` | Claude Code | PreToolUse hook | proxy or OTel | MCP + statusline |
| `hooks-family` | Cursor, Windsurf, Antigravity | hooks.json | proxy or OTel | MCP + panel |
| `acp` | Zed, JetBrains, Neovim | `request_permission` | own the loop | `session/update` |
| `opencode` | OpenCode | `tool.execute.before` | own the loop or proxy | `tui.toast` |
| `cline` | Cline | `beforeTool` | own the loop or proxy | registered tool |
| `vscode` | VS Code family (Open-VSX) | own contributed tool only | proxy or OTel | status bar + webview |
| `meter-only` | Continue, any OTel host | — | OTel `data`/`gen_ai.*` | dashboard |
**Minimal conformance:** bind ≥1 seam and seal receipts (§5) with honest provenance
(§7). **`full` conformance:** bind all three seams for a host, with a Proxy meter
(enforceable ceiling) and hash-chained receipts.
The **reference profile is `claude-code`**: a single installable plugin bundling the
gate hook, an MCP receipt server, and a status-line joule meter — the deepest gate
available anywhere and the clearest end-to-end demonstration.
---
## §7 The honest limit (normative honesty)
A host-side adapter, alone, **cannot** meter the host agent's own tokens or its
built-in (non-adapter) tool calls — those are invisible from every host's extension
sandbox (confirmed across VS Code, Cursor, Windsurf, Antigravity, Zed, JetBrains).
Therefore:
- A complete energy figure REQUIRES the **Meter seam** (§4) bound via proxy or OTel.
An adapter that presents a whole-session energy figure **without** a bound meter is
**non-conformant** — it may only report the energy of work that passed through its
own tools, and MUST label it as such.
- An adapter whose only meter is Telemetry (§4.1) MUST disclose that its joule ceiling
is **observed, not enforced**.
Overclaiming coverage is a conformance failure, not a rounding of the truth.
---
## §8 Security and trust
JouleHook inherits JCP's stance: every action is decided **in the runtime, never in
the LLM**, because a tool description and a tool result are untrusted input. The gate
is a deterministic function; a `deny` binds regardless of the host's permission mode
where the host allows a hook to override it (e.g. Claude Code `PreToolUse` fires
before the permission check). Receipts are non-repudiable and hash-chained. The
adapter runs inside the host's trust domain and MUST NOT weaken it: it adds gates and
receipts, it does not remove the host's own approvals.
---
## §9 Relationship to the sibling standards
JouleHook is the **host-binding layer**, not a re-implementation:
- **JCP** decides and grants (capability ⊗ joule budget = one signed grant); JouleHook
binds that decision to each host's gate seam.
- **Mesh** meters and routes at the model front door; JouleHook points a host at it as
the Meter seam and enforces the ceiling.
- **`jcp-efficiency`** is the receipt; JouleHook carries it through each host's receipt
surface, optionally with an attested energy figure (`sandbox-tee::energy`).
The contribution of JouleHook is the **normative seam contract + the conformance
profiles** that make one metered, gated, attested loop portable across agents that
would otherwise each need a bespoke integration.