open-standards/jcp/SPEC.md
Transaction Science 830fc085f2
Some checks failed
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Has been cancelled
standards workspaces build + test / jouleclaw-rs (pull_request) Has been cancelled
standards workspaces build + test / aac-rs (pull_request) Has been cancelled
standards workspaces build + test / arl-rs (pull_request) Has been cancelled
standards workspaces build + test / eoc-rs (pull_request) Has been cancelled
standards workspaces build + test / jcp-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-ui-rs (pull_request) Has been cancelled
standards workspaces build + test / joulecontract-rs (pull_request) Has been cancelled
standards workspaces build + test / jouledesk-rs (pull_request) Has been cancelled
standards workspaces build + test / joulehook-rs (pull_request) Has been cancelled
standards workspaces build + test / map-rs (pull_request) Has been cancelled
standards workspaces build + test / mesh-rs (pull_request) Has been cancelled
standards workspaces build + test / openpay (pull_request) Has been cancelled
standards workspaces build + test / proof-rs (pull_request) Has been cancelled
standards workspaces build + test / sandbox-rs (pull_request) Has been cancelled
standards workspaces build + test / sense-rs (pull_request) Has been cancelled
standards workspaces build + test / smart-byte-rs (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Has been cancelled
jcp-energy: a per-process span a child process worked in is partial
The per-process rung graded every span that advanced `ri_energy_nj` as
`HwShunt`, `Coverage::Whole`. That count covers only this process's threads.
A child's energy is its own, and the kernel never adds it to the parent's
count, not even when the child is reaped. So a closure that drives a child
process was under-counted while its figure claimed the whole span. SPEC §5
forbids that: a figure must not claim coverage of a domain its counter does
not read. The previous commit named this under "Not done here" and said
grading it `Partial` would need the caller's help. It does not: the same
`rusage_info_v6` shows it, and so does the kernel's list of children.

The rule, in `process_span`: a span whose count advanced is whole only when
no child process can have worked in it unseen. That means two things. The
process's reaped children's CPU time (`ri_child_user_time` +
`ri_child_system_time`) held across the span, and at the span's end
`proc_listchildpids` lists no child, running or exited and not yet reaped.
Otherwise the span is `Energy::measured_partial`: `HwShunt`, `Partial`, the
same rise. A failed child count is `Partial` too, since nothing then shows the
figure whole. A child the process had before the span counts as well, because
the meter cannot tell whether the span's work drove it. `measure` lists the
children before it reads the count, so a child reaped between the two shows
up in one or the other. A span whose count did not advance is still the
model's whole-task figure.

What the kernel does, measured on an M5 Max, macOS 27.0.1, with the C
compiler and small C and Rust programs rather than assumed:
- `ri_child_user_time` and `ri_child_system_time` are at bytes 96 and 104
  (`offsetof` against the 27 SDK). They are in Mach absolute time units: a
  `yes` that ran ~190 ms showed 4,565,473 of its own, and reaping it raised
  the parent's by 4,565,861. `ri_energy_nj` did not move on the reap.
- Spawning a child by absolute path, or letting it run, never moved those
  times; only the reap did (24 of 24 trials, posix_spawn and fork+exec
  alike).
- `proc_listchildpids` returns a count of pids when given a buffer. It lists
  a killed child until it is reaped. Without a buffer it returns a size
  estimate (1,098 with no child), so the reader always passes one.
- A failed `posix_spawn` can move the reaped children's time too: a spawn of a
  missing path moved it in 2 of 200 attempts (90-127 units). A program
  spawned by name is looked up by trying each `PATH` directory. Here, with 12
  ahead of `/usr/bin`, `Command::new("yes")` moved the time at spawn in 7 of
  24 spawns, and `/usr/bin/yes` by absolute path in none of 12. Such a span
  is `Partial` on that alone, which errs on the side of claiming less. It is
  documented on `process_span`. A run of the new left-running test, which then
  spawned `yes` by name, caught it: "reaped children's CPU 0 -> 115 Mach
  units" with nothing reaped. The tests now spawn `/usr/bin/yes`.

The decoder reads the two child-time words next to the energy words.
`process_energy::child_count()` wraps `proc_listchildpids` with a 64-pid
buffer. Docs: the crate's ladder entry, the module, `Meter::ProcessEnergy`,
`process_span`, SPEC §5.1 item 3 and the README's jcp-energy bullet now say
that a span that reaps a child or ends with one is `Partial`.

Tests. jcp-energy has 80 lib tests (78 before) and its integration test. All
pass with and without `--features nvml`, and three runs in a row were green:
- Layout: the child-time offsets equal the C compiler's 96 and 104 and
  `offset_of!` on the transcribed struct. Decoding reads all four words and
  nothing else; on a literal buffer, 446,368 + 4,119,493 = 4,565,861.
- Pure, on every platform: on a count that rose 0.48 J, a reaped child, a
  child left at the end, both, and a failed child count are each
  `HwShunt`/`Partial` with the same 480,001 µJ. Neither is whole. A held count
  is the model's whole figure whatever the children did.
- Hardware, each in a fresh process. A span that runs `/usr/bin/yes >
  /dev/null` for 200 ms and reaps it is `Partial`. The tests' own reads show
  that the reaped time rose and no child is left, so the reap alone decided
  it. A span that leaves `yes` running is `Partial`; the child is listed and
  the reaped time held, so the left child alone decided it. A pure 200 ms
  in-process burn, with no child before or after, is whole. The old
  `a_child_process_work_is_not_attributed` is now the reap test, and keeps
  its check that the child's energy is not in the figure.
- The tests' oracles are their own: `proc_listchildpids` through their own
  declaration, counting nonzero pids in a zeroed buffer, and the child times
  through the transcribed struct. A guard kills and reaps every child, so a
  failing test leaves none running.

Falsified, each by a temporary edit restored with git checkout (git diff was
empty after each):
- Ignoring children (`whole = true`) fails the pure test and both hardware
  child tests: "a span that reaped a child claimed to cover it: Energy {
  measured_uj: Some(1368), ..., coverage: Whole }" and "a span that left a
  child running claimed to cover it: ... coverage: Whole".
- Ignoring the reaped children's time fails the pure test and the reap test
  only.
- Ignoring the children left at the end fails the pure test and the
  left-running test only.
- A `child_count` that always says 0 fails the left-running test.

jcp-intercept (12) and jcp-conformance (1 unit test and 5 pack tests) pass.
The jcp-conformance binary prints "all checks passed", C9 included, and
jcp-cli runs and exits 0. clippy --no-deps -D warnings is clean on jcp-energy
for aarch64-apple-darwin (with and without `nvml`), x86_64-apple-darwin and
x86_64-unknown-linux-gnu, and `cargo doc` with `-D warnings` is clean.

M5 Max, verbatim:
  process energy: a span running `yes > /dev/null` for 200 ms, then reaping
    it -> 409 µJ (0.000409 J), HwShunt, coverage Partial; the child's own
    count 229352810 nJ (0.229 J); reaped children's CPU 0 -> 4673857 Mach
    units; children left []
  process energy: a span leaving `yes > /dev/null` running -> 29048 µJ
    (0.029048 J), HwShunt, coverage Partial; children at its end [41504]
    (the child 41504); reaped children's CPU 0 -> 0 Mach units

M3 Ultra, verbatim. The lib test binary was built here with
`target-cpu=apple-m1`, copied to 10.10.10.2 (sha256 060761dd2b0e2efc... on
both ends), run with --nocapture, then deleted. 80 of 80 passed:
  process energy: a span running `yes > /dev/null` for 200 ms, then reaping
    it -> 1322 µJ (0.001322 J), HwShunt, coverage Partial; the child's own
    count 1245873027 nJ (1.246 J); reaped children's CPU 0 -> 5041160 Mach
    units; children left []
  process energy: a span leaving `yes > /dev/null` running -> 137541 µJ
    (0.137541 J), HwShunt, coverage Partial; children at its end [30980]
    (the child 30980); reaped children's CPU 0 -> 0 Mach units
  process energy: 200 ms of one thread's CPU -> 1366000 µJ (1.366 J),
    HwShunt, coverage Whole
  ioreport liveness: status=CpuRailFrozen oracle=CpuRailMoved(false)
    selected=false

Not done here:
- Work handed to the GPU, the Neural Engine, or another process over IPC (a
  daemon serving this one) is still not in the figure and is not detected.
  rusage has no per-process GPU energy.
- A grandchild that outlives the child that started it is reparented to
  launchd and is not listed. The reap of its parent still makes that span
  `Partial`, but a later span cannot see it.
- A long-lived child the process keeps makes every span `Partial` while it
  lives. That is safe, but coarse.

Rebased onto 25bfdcd9 (the RAPL figure sums the package zones by name),
which adds four RAPL lib tests and changes no line of this commit; the counts
above, the M3 Ultra run's included, predate it. After the rebase jcp-energy
has 84 lib tests (82 before this commit), all passing on the M5 Max with and
without `--features nvml`, as do jcp-intercept, jcp-conformance (its tests
and the binary) and jcp-cli.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 22:05:52 -04:00

40 KiB
Raw Permalink Blame History

JCP v0.3 — The Joule Context Protocol

Status: v0.3.0 draft. Licence: specification text CC-BY-4.0; reference code Apache-2.0.

A unified, energy-native, capability-secured protocol for agentic systems. JCP subsumes in one spec what today's stack splits across MCP (tool/context connection), A2A/ACP (agent-to-agent), an identity layer, an authorization layer, and — missing everywhere — an energy layer. Its thesis is one object:

A capability and an energy budget are the same thing — a signed, bounded, debited grant. A capability you cannot pay for is denied; an expenditure you are not capable of is denied. One check, made deterministically outside the model, sealed in a receipt.


1. Scope and doctrine

  1. Enforcement is outside the model. Authorization and metering are decided by the runtime, never by the LLM's instruction-following. The published inverse-scaling-law result — more capable models are more exploitable because they follow injected instructions better — makes model-side control unsound by construction.
  2. Authority is a capability, not ambient. No actor holds standing privilege; every action requires an unforgeable Grant. There are no long-lived ambient secrets in env vars.
  3. Energy is first-class. Every grant carries a joule budget that is debited per action and enforced before the side effect — not merely observed.
  4. Data is never control. Tool and agent outputs are untrusted data and can never be executed as instructions or used to exercise a capability. This closes the asymmetric-trust gap that tool poisoning exploits.
  5. Every decision is non-repudiable. Allow and deny alike seal as a signed receipt. Forging the audit means forging a signature.
  6. One substrate. The same identity, grant, frame, and receipt span the tool layer and the agent layer; there is no second protocol to secure.

2. Actors and identity (REQUIRED)

Every actor — model, agent, tool, or server — is an Ed25519 key, expressed as a W3C did:key (multibase base58btc of the 0xed01 multicodec ‖ the public key). There are no anonymous actors. An actor proves itself by signing.

2.1 Identity continuity: pre-committed rotation (OPTIONAL)

For long-lived actors, an identity is its inception DID (seq 0), forever; the key that controls it rotates through a verified event log:

KeyEvent {
  identity:  did:key            ; the seq-0 (inception) DID, stable for life
  seq:       uint               ; 0 = inception, N = the Nth rotation
  current:   did:key            ; the controlling key from this event on
  next_hash: content-id         ; blake3 of the NEXT key's 32 public bytes
  parent:    content-id?        ; the previous event's id; absent at inception
  nonce:     bytes(16)
}

Inception is signed by the identity key (current MUST equal identity). Rotation event N is signed by its new current key, which MUST hash to event N−1's next_hash — possession of the pre-committed key is the proof of control. The consequences are exactly the two that matter: a thief who steals the current key still cannot rotate the identity away (they lack the pre-committed next key), and an owner who loses the current key still can (they hold it). A verifier MUST reject sequence gaps, parent-link mismatches, hash mismatches, and bad signatures.

Rotation machinery is the long-lived-identity case. The RECOMMENDED norm is cheaper: short-lived grants re-issued on a heartbeat, with revocation (§4.3) covering the emergency. Rotation pressure scales with credential lifetime.

2.2 Threshold actors: the two-agent rule (OPTIONAL)

A threshold actor is a did:key whose private key never exists in one place: it is split k-of-n (FROST, RFC 9591, Ed25519 ciphersuite), and a signature exists only when at least k shareholders cooperate. Because the group key is an ordinary Ed25519 verifying key, nothing else in JCP changes — a threshold actor issues, holds, delegates, revokes, and signs wire requests, and no verifier can tell or needs to.

Use it where one gated model is still too few: make the subject of an irreversible capability a k-of-n team, and no single agent — however capable, however poisoned — can exercise it alone. The refusal is arithmetic, not policy.

Shares MAY be minted by a trusted dealer (one party creates a team identity and hands out shares) or by distributed key generation, in which the group's private key never exists anywhere — not even momentarily at a dealer. The group key is the same ordinary Ed25519 key either way.

3. Canonical serialization and content addressing (REQUIRED)

Content addresses and the signed receipt core use canonical det-CBOR (RFC 8949 §4.2.1): length-first then bytewise key order, shortest integer form, no floats, bools, or nulls in a signed core. A content id is blake3(canonical_cbor(value)), lowercase-hex on the wire — the same encoding the JCR-1 receipt signs over (§8), so an id and a receipt agree on bytes.

4. The Grant (REQUIRED)

The metered capability is JCP's central object:

Grant {
  issuer:     did:key            ; who granted this authority
  subject:    did:key            ; who holds it
  capability: { resource: content-id, actions: [string] }
  budget_uj:  uint               ; the joule ceiling this grant may ever spend
  budget_funds: { currency: string, minor: uint }?  ; OPTIONAL money ceiling (§4.8)
  caveats:    [caveat]            ; OPTIONAL embedded conditions (§4.9); omitted when empty
  expiry:     uint               ; unix seconds; 0 = no expiry
  parent:     content-id?        ; the grant this was attenuated from
  delegate_depth: uint?          ; further hops permitted; absent = unlimited, 0 = leaf-only (§4.4)
  nonce:      bytes(16)
}

budget_funds is absent in the baseline grant. Per §3, an absent OPTIONAL field is omitted from the canonical encoding entirely, so a grant with no money ceiling is byte-identical to one minted before §4.8 existed — its content id, its signature, and every published conformance vector are unchanged.

A grant is identified by blake3(canonical_cbor(grant)) and signed by the issuer's key (Ed25519 over the canonical bytes). The resource is addressed by content, not by URL — audience binding an attacker cannot spoof by name (a content-addressed strengthening of RFC 8707).

4.1 Attenuation (REQUIRED)

Delegation is object-capability attenuation. The holder of a grant (its subject) MAY mint a child, but the child MUST be strictly weaker:

  • child.capability ⊆ parent.capability (same resource; actions a subset),
  • child.budget_uj ≤ parent.budget_uj,
  • the child's expiry MUST NOT outlive the parent's (0 = never expires),
  • child.parent = the parent's content id (forming the chain).

A conforming implementation MUST refuse any amplification and MUST refuse delegation by anyone other than the parent's subject. A chain therefore can never convey more authority — or more energy — than its root.

4.2 Chain verification (REQUIRED for multi-hop)

When a frame is decided against a delegated grant, the enforcement point MUST be presented the full chain, root first, leaf last, and MUST verify it hop by hop: the root has no parent; every later grant's parent equals the id of the grant before it; each hop is issued by the previous grant's subject; each hop narrows per §4.1; and every signature in the chain verifies. A structurally invalid chain is denied chain-broken; an amplifying one attenuation-violation.

Energy conservation across siblings. The cost of an allowed action MUST fit the remaining budget of every grant in the chain and MUST be debited from all of them. Two children attenuated from one root therefore share the root's budget — delegation can never mint joules.

4.3 Revocation (REQUIRED)

Revocation {
  grant_id:   content-id         ; the grant being revoked
  revoked_by: did:key            ; MUST equal that grant's issuer
  nonce:      bytes(16)
}

A revocation is signed by revoked_by over its canonical bytes. An enforcement point MUST verify authority before honoring one — the ids match, the revoker is the grant's issuer, the signature verifies — and MUST then deny (revoked) any decision against the revoked grant or any chain containing it: a delegation subtree dies with any of its ancestors. Both the direct and the chained decision path MUST enforce this.

4.4 Delegation control (REQUIRED)

A grant MAY carry delegate_depth: how many further attenuation hops it permits. Absent = unlimited; 0 = leaf-only — the holder may act but never re-delegate. Like every attenuation axis, depth only narrows: a child minted from a depth-d parent carries at most d−1, and chain verification (§4.2) MUST refuse a hop under a leaf-only parent (delegation-forbidden semantics map to chain-broken on the wire) and any child whose allowance exceeds its parent's bound (an amplification). In group topologies this is the primitive that makes a worker a worker: an orchestrator delegates depth-0 grants and the team cannot deepen itself.

4.5 Sharded budgets: distributed enforcement without consensus (REQUIRED baseline)

When one grant must be enforceable at N enforcement points, the holder MUST shard it: mint N sibling children whose budgets sum to no more than the parent's, one per enforcement point. Each point then enforces only its own shard, locally — no coordination, no consensus — and the root stays conserved by mint-time arithmetic. Rebalancing is revocation + re-issuance.

A conforming sharding operation MUST refuse slices that sum past the parent. Fragmentation (an unused slice in one shard unavailable to another) is the deliberate price of coordination-freedom — the same trade cash makes.

Receipt-settled budget channels (OPTIONAL profile). Where shards are too rigid, N enforcement points MAY spend optimistically against one grant and settle by exchanging signed statements of cumulative local spend:

Statement {
  grant_id: content-id
  point:    did:key              ; the enforcement point making the claim
  spent_uj: uint                 ; cumulative µJ allowed at this point since open
  seq:      uint                 ; strictly increasing per point
}

A statement is signed by point over its canonical bytes. A receiving point MUST verify the signature, refuse stale (seq not increasing) and regressing (spent_uj decreasing) statements, and thereafter count the peer's settled spend against the shared budget. Between settlements the points can collectively overspend by at most what the unsettled window allows — a bounded, tunable exposure, and after settlement the overdraft is provable from the signed statements rather than suspected. Where even bounded overdraft is unacceptable, use shards; that is why the baseline is the baseline.

4.6 Standing grants: bounded authority for event-triggered work (OPTIONAL)

An autonomous agent often starts work on its own — on a webhook, a schedule, a ticket — with no human present at the firing instant. A long-lived credential that any trigger may spend is unbounded authority in an automation costume. A standing grant keeps the object-capability discipline instead: an ordinary grant (§4) the issuer holds, plus a signed mint policy that says how a trigger may turn it into work. The policy is bound to one grant by id, signed by that grant's issuer, and MUST NOT amplify it. It fixes three bounds at once: the event class permitted to fire it (content-addressed, so a webhook for one source cannot spend a grant meant for another), the per-firing budget (a slice no larger than the grant's budget), and the maximum number of firings.

Each firing mints one fresh child through the ordinary attenuation path (§4.1): the child is strictly weaker, its parent is the standing grant, and it chain-verifies (§4.2) like any delegation — so its receipt traces back to the standing grant. A small mint ledger (firing count and cumulative spend) carried by the enforcement point holds the rate limit and the parent's budget conservation across firings by arithmetic alone, no coordination — the same local-enforcement property as sharded budgets (§4.5). A firing is refused, and the ledger left unchanged, when the event is of the wrong class, the firing count is exhausted, or the next firing would push cumulative spend past the grant. Conformance class C20.

4.7 Checkpoints and amendments: steering a long run (OPTIONAL)

A multi-hour run is not a black box that returns only at the end. At defined boundaries the running agent commits to its state — a checkpoint — and a distinct principal may steer it before it continues. The two roles are structurally separate (the separation-of-duties rule): the checkpoint's author is the agent (the grant's subject), which commits to its own state and cannot steer itself; the amender is the grant's issuer — the steward who delegated the authority and is therefore already a different principal. Steering is an issuer power, like revocation (§4.3): an amendment is a soft revoke that re-bounds the run rather than killing it.

A checkpoint cites the active grant and a content-addressed state commitment, signed by the agent. An amendment cites a checkpoint, is signed by the grant's issuer, MUST differ from the agent, and MUST only tighten — the continuation's budget is at most the checkpoint grant's, so steering can never escalate a running agent's authority; it MAY also hand the agent a new instruction-set commitment. The run resumes under a continuation grant the issuer re-issues to the same agent with the amended budget, linked back to the steered grant. This continuation is an issuer re-binding, not a holder delegation — an auditor verifies it alongside the signed checkpoint+amendment pair, which together record who intervened, when, and how. Conformance class C21.

4.8 Dual ceilings: a money budget beside the joule budget (OPTIONAL)

A grant MAY carry a second, independent ceiling denominated in money (budget_funds), enforced alongside the joule ceiling of §4. This exists because the payments ecosystem authorizes agent spend in a currency (an AP2 mandate names dollars), while JCP meters work in joules — and the two MUST NOT be reconciled by an invented exchange rate. There is no canonical joules-per-dollar; asserting one would fabricate authority. So JCP enforces both ceilings and reconciles neither.

The two dimensions are governed by two different sources of authority: the payment mandate bounds the money, the operator bounds the energy. A conforming runtime MUST, on each priced decision:

  • deny with funds-exceeded if the action's money cost would carry the grant's money spend past budget_funds.minor;
  • deny with currency-mismatch if the cost is denominated in a currency other than budget_funds.currency — a foreign-currency cost is refused, never converted;
  • on allow, debit both the joule and the money spend; on any deny, debit neither;
  • treat an absent budget_funds as "joules alone bound this grant" — the money dimension is not present, not a zero ceiling.

Because the ceilings are independent, a decision MAY deny on one while the other still has room, and the sealed receipt (§8) records which bound. A receipt that reads "denied: joule ceiling, while the money ceiling had room" is a statement no single-dimension authorization can make — and it is the point of the design.

Attenuation (§4.1) applies to both dimensions independently. A child's money ceiling MUST NOT amplify the parent's: a child bounded in money where the parent was unbounded is permitted (it only adds a constraint), but dropping a bounded parent's ceiling, raising it, or denominating it in a different currency (which cannot be shown to narrow without a rate) MUST be refused. A plain attenuation that names no money ceiling inherits the parent's unchanged — it is never silently dropped, which would amplify. Conformance class C22.

4.9 Caveats: the open attenuation axis (OPTIONAL)

The axes of §4.1 narrow by set-subtraction — remove actions, lower a ceiling, shorten an expiry. They cannot express a condition the issuer did not anticipate. A grant MAY therefore carry caveats: conditions embedded inside the signed token that MUST all hold for a decision to be allowed.

The point is that a caveat travels with the capability. Any enforcement point holding the signed grant checks it offline — no call back to the issuer, no service-side policy to consult, no shared configuration. That is what distinguishes a caveat from a policy engine: policy is something the verifier must be told; a caveat is something the token carries.

A conforming implementation MUST support:

ActionIn([string])    ; the action must be in this set — MAY narrow below the capability
PayloadIs(content-id) ; the frame's payload must be exactly this — a ONE-SHOT binding
CallerIs(did:key)     ; the caller must be this principal
NotBefore(uint)       ; unix seconds
NotAfter(uint)        ; unix seconds — a window narrower than `expiry`
MaxCostUj(uint)       ; a ceiling on ONE invocation, distinct from the cumulative budget

Every predicate ranges only over what an enforcement point holds when it decides: the frame, the caller, the clock, and the metered cost. There is deliberately no content predicate — the runtime sees a frame's payload_hash, never its bytes — so a condition on content is expressible only as PayloadIs, binding an exact payload rather than inspecting it. An implementation MUST NOT invent a predicate that requires data the decision point does not have.

Two of these express authority the fixed axes cannot. PayloadIs makes a grant one-shot: it authorizes one specific call, so a leaked grant cannot be replayed against different arguments. MaxCostUj bounds a single invocation, where budget_uj bounds the cumulative total — so a delegate cannot spend an entire remaining budget in one call.

The metered path (REQUIRED). A caveat whose predicate ranges over the invocation's cost — MaxCostUj — cannot be evaluated in the two-phase metered flow at authorization time, because the cost is not yet known. Such a caveat MUST be enforced at the debit point instead, and an implementation MUST NOT report a grant as authorized in a way that implies a cost-dependent caveat has passed. Every cost-independent caveat MUST be enforced in both phases.

When a measured cost breaches a cost-dependent caveat at the debit point, the work has already run and the joules are real: the implementation MUST still debit them — energy that was spent is always accounted — and MUST record the breach in the verdict, so the sealed receipt is evidence of it. Debiting silently, or suppressing the debit to make the record look clean, are both non-conforming.

Attenuation (REQUIRED). Caveats only accumulate. A child MUST carry every caveat of its parent; a plain attenuation inherits them unchanged, and a caveat-adding attenuation appends. Dropping an inherited caveat is an amplification and MUST be refused.

Caveats compare by exact identity, not by an ordering over their parameters: a child retains a parent's caveat only by carrying that identical caveat. There is deliberately no notion of "a tighter MaxCostUj supersedes a looser one" — such an ordering would have to be defined per variant, and a verifier that got it wrong would silently widen authority. A delegator that wants a tighter bound adds it alongside the inherited one; since all caveats must hold, the tighter of the two governs. This is the conservative reading: it can never widen, and it may be relaxed later without breaking existing grants, whereas the reverse is not true. A decision whose grant carries an unsatisfied caveat is denied caveat-unsatisfied, sealed like any other denial.

An empty caveat set is omitted from the canonical encoding, so a grant without caveats is byte-identical to one minted before they existed — its content id and signature are unchanged. Conformance class C24.

5. Energy (REQUIRED)

Every metered cost is integer microjoules with a provenance tag: HwShunt (hardware reading; a measured figure REQUIRED), ModelBased (calibrated model; measured MAY be absent), or Estimator (coarse constant; measured MUST be absent). The runtime debits cost.uj() from a grant's remaining budget on every allow; the coupling MUST be enforced. The energy axis is what no existing agentic protocol standardizes — JCP makes it a precondition, not a report.

HwShunt covers a vendor energy counter as well as a dedicated shunt. A vendor counter's count is a real reading, not an estimate; what it lacks is an independent root — how the vendor derives it is undisclosed, and the platform's own software relays it — which is a gap in provenance, not in accuracy. A reading earns the grade only while its counter is live:

Liveness (REQUIRED). 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). Where the figure sums several counters of the domain — a RAPL package counter per package, an IOReport CPU energy rail per die — every one of them MUST be observed live: a frozen one beside live ones would be left out of a figure that claims the whole domain. 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.

The record of why is the implementation's to choose; the reference meter exposes each counter's liveness status, and which rung it selected with the status of every rung above it that it declined. Coverage is defined in §5.2.

The rule: measured when the platform exposes it, estimated when it doesn't — and the provenance always says which. There is always an estimate: an environment with no energy interface still yields a usable figure, it just carries ModelBased or Estimator provenance instead of HwShunt. A figure MUST NOT claim better provenance than how it was obtained — including when a hardware backend fails mid-reading and the figure degrades to a model.

Recommended ladder, best first:

  1. RAPL (Linux powercap) — package energy counters, wraparound-corrected → HwShunt, selected only while every package counter is live under the probe's CPU burn, by the rule above as IOReport's CPU rails are. A span over which no package counter advanced is metered by a lower rung. The package counters are the top-level zones whose name is package-<n>, every socket's, each corrected at its own max_energy_range_uj; no other zone is summed or probed. A psys zone already includes the packages, so summing it beside them counts their energy twice, and a dram zone is not CPU energy.
  2. NVML — the GPU total-energy counter → HwShunt, Partial (§5.2): it reads the GPU and not the CPU's share of the work. Selected only while the counter is live by the same rule; the probe cannot load the GPU, so for this counter live means published continuously, advancing on the device's own draw. A span over which it did not advance is metered by a lower rung.
  3. Per-process CPU energy (macOS proc_pid_rusage, ri_energy_nj) — the kernel's count of the CPU energy of the metering process's own threads → HwShunt, Whole for work that runs on those threads. Selected only while the count is live by the rule above. Its domain is that process's threads alone: not other processes, not its child processes, not the GPU, the Neural Engine or DRAM. A span over which the process reaped a child (its reaped children's CPU time moved), or at whose end it has a child, running or not yet reaped, is Partial (§5.2): the child's energy is not in the count. It ranks above the SoC-wide counter because it attributes the work's own CPU energy rather than the machine's. A span over which it did not advance is metered by a lower rung.
  4. Apple IOReport (the Energy Model group) — the SoC energy counters → HwShunt, selected only while every CPU energy rail (one per die on a multi-die part) is live by the rule above. A span over which no CPU rail advanced is metered by the next rung, never graded HwShunt. Where the CPU rail is not live but the GPU rail is, the rung is declined rather than reporting the GPU rail as a Partial reading of the work: the GPU rail is machine-wide (display compositing moves it), so booking it to a task misattributes energy the task did not spend, and the partial figure would under-debit the budget, the CPU's share going unmetered. The whole-task estimate below is the honest figure.
  5. Time × calibrated power — wall-clock duration times a host-calibrated active power → ModelBased. Always available.
  6. Constant per-operation estimate → Estimator.

5.2 Coverage (REQUIRED where a figure is partial)

Provenance says how a figure was obtained; coverage says how much of the work it accounts for: Whole (the default) or Partial. A reading can be hardware-rooted and still cover only part of the work — an accelerator counter on a host whose CPU has none — and its remainder is unmeasured, not zero. A Partial figure MUST NOT be presented as a task total.

On the wire coverage is carried only when it is Partial, and omitted when Whole, so a figure sealed before the field existed keeps its exact bytes; a verifier reads an absent field as Whole. The jcp/2 receipt carries it as energy_coverage (§8); the intercept/1 step receipt of the reference interceptor chain (jcp-intercept) carries it as coverage, beside its provenance. In intercept/1 the field, when present, has exactly one value, "Partial"; a verifier MUST refuse any other value, "Whole" included, rather than read it either way.

6. The Frame (REQUIRED)

Every message is a typed Frame — one envelope across the tool layer (MCP's job) and the agent layer (A2A's job):

Frame {
  kind:         tool-call | tool-result | agent-task | agent-reply
  trust:        control | data
  grant_id:     content-id        ; the grant this frame exercises
  action:       string
  resource:     content-id
  payload_hash: content-id
}

trust = data marks untrusted output. A conforming runtime MUST refuse to exercise any capability from a data frame (§7), regardless of its content. Tool and agent outputs MUST be tagged data.

7. The decision (REQUIRED)

Given a Frame, its SignedGrant, the caller's DID, a metered cost, and the current time, a conforming runtime decides Allow or Deny(reason) by these checks, in order, the first failure naming the reason:

  1. data-tagged frame → Deny(untrusted-control).
  2. grant revoked (§4.3) → Deny(revoked).
  3. the issuer (for a chain: the root issuer) is not a configured trust root for the frame's resource → Deny(untrusted-root). An unconfigured resource is open; once any root is configured, only listed keys anchor authority.
  4. frame's grant_id ≠ the grant's id → Deny(wrong-resource).
  5. caller ≠ grant's subject → Deny(wrong-subject).
  6. issuer signature invalid → Deny(bad-signature).
  7. expired → Deny(expired).
  8. frame's resource ≠ capability's resource → Deny(wrong-resource); action ∉ capability → Deny(action-not-granted).
  9. prior spend + cost > budget_uj → Deny(budget-exceeded).

On Allow, the runtime debits cost from the grant's remaining budget. The decision is pure and deterministic; no model participates.

For a delegated grant the decision takes the full chain (§4.2): structure and signatures first (chain-broken / attenuation-violation / bad-signature), then revocation against every link (revoked), expiry of every link, the leaf-level checks above, and budget against every link, debiting all on allow.

Trust roots. An enforcement point is configured with the root-issuer keys it honors for each resource it guards — the same local, pairwise trust shape as an authorized_keys file. did:key is self-certifying, so there is no certificate authority and no hierarchy; cross-organization federation is a directory convenience, never a trust dependency. A chain whose root issuer is not configured for the frame's resource MUST be denied.

8. The receipt (REQUIRED)

Every decision — allow and deny — seals as a JCR-1 receipt: canonical det-CBOR inside a COSE_Sign1 envelope (RFC 9052, EdDSA/Ed25519, alg = -8). The jcp/2 profile signs a flat uint/text core:

jcp_receipt        = "jcp/2"
grant_id           = hex
issuer, subject    = did:key
resource, action   = hex, string
payload_hash       = hex                   ; content id of what was acted upon
frame_kind, trust  = "tool-call"…, "control" | "data"
verdict            = "allow" | "deny"
deny_reason        = string                ; omitted on allow
cost_uj            = uint
energy_provenance  = "HwShunt" | "ModelBased" | "Estimator"
energy_coverage    = "Partial"             ; omitted when Whole (§5.2)
budget_remaining_uj, budget_uj = uint
steward            = the signer's id (the COSE kid)

payload_hash binds the receipt to what the decision was over, not merely that a decision was taken. Without it a tool-call receipt does not commit its arguments, and two decisions differing only in payload seal to identical bytes — so a payload substitution is undetectable in the audit. A conforming implementation MUST include it.

Profile versioning (REQUIRED). The predecessor profile jcp/1 omitted payload_hash. A verifier MUST read the jcp_receipt tag from the signed core and recompute the preimage under that profile, so receipts sealed as jcp/1 keep verifying; it MUST refuse a tag it does not implement rather than assume a profile. Implementations MUST NOT mint jcp/1. Published jcp/1 receipts are retained in conformance/v1/legacy_jcp1.json precisely so a verifier can prove it honours the migration.

Receipts share the family's JCR-1 envelope (canonical det-CBOR + COSE_Sign1/EdDSA) and vocabulary, so family tooling verifies signatures and decodes cores uniformly. The core field list is per-profile: sibling profiles (map/1, Sandbox, proof, jouleclaw's receipt kinds) sign their own cores, and a change to jcp/… does not alter theirs. A deny receipt is as load-bearing as an allow receipt — the record of a refused attack is evidence.

8.1 The receipt stream is the write-ahead log (REQUIRED)

A receipt carries everything enforcement needs to remember: the grant, the verdict, the debit. A receipt sealed for a multi-hop chain MUST additionally record chain_ids — every grant id the decision debited, root first — so the stream is self-contained. An enforcement point that crashes MUST be able to rebuild its budget state by replaying its own verified receipt stream: allow-receipts re-apply their debits to every grant they name, ancestors included, with no external index. Durability and audit are the same artifact; there is no second database to drift from the evidence.

When a team works under sub-grants of one root, aggregation of the verified receipt stream keyed by (grant_id, subject) yields per-worker attribution — actions allowed, actions refused, joules spent — computed from evidence rather than self-reporting. An aggregator MUST count (not silently skip) receipts that fail verification: a ledger that drops entries is not a ledger.

9. Conformance

A conforming implementation passes, with real checks:

  • C1 — signed grants + audience binding: a grant verifies only for its issuer's key and authorizes only its exact content-addressed resource.
  • C2 — attenuation monotonicity: a child grant can only narrow; any amplification or non-holder delegation is refused.
  • C3 — budget enforcement: spend beyond the ceiling is denied deterministically and the denied call is never debited.
  • C4 — data/control separation: a data frame can never exercise a capability.
  • C5 — the receipt: every decision seals to JCR-1 and verifies; a wrong steward fails.
  • C6 — revocation: only the issuer revokes; a revoked ancestor kills the whole delegation subtree; a forged revocation is never admitted.
  • C7 — chain conservation: a multi-hop chain verifies hop by hop; a spliced chain is refused; sibling delegations share their root's budget.
  • C8 — wire binding: a request round-trips the jcp/1 framing canonically and an enforcement endpoint decides + seals over a live socket.
  • C9 — energy honesty: the metering ladder always yields a figure and its provenance is internally consistent.
  • C10 — delegation control: depth narrows per hop; a leaf-only grant cannot be delegated, at mint or in chain verification.
  • C11 — sharded budgets: shard slices that sum past the parent are refused at mint.
  • C12 — rotation: an un-pre-committed key cannot rotate an identity, the pre-committed key can, and the identity DID is stable across rotations.
  • C13 — caller-signed wire + threshold: an impersonated caller is denied bad-signature; one share of a 2-of-3 actor cannot sign; two can, and the endpoint treats the aggregate as any caller signature.
  • C14 — replay + group accounting: a fresh runtime restores spend from the verified receipt stream; per-worker attribution matches the original ledger; a tampered receipt contributes nothing.
  • C15 — trust roots: an unconfigured resource is open; once configured, a validly-signed grant from an unlisted root is denied untrusted-root on both decision paths.
  • C16 — chained replay: a chained receipt names every debited link; a fresh runtime replays leaf AND ancestor debits exactly and keeps conserving.
  • C17 — budget channels: optimistic spends settle by signed statement; forged, stale, and regressing statements are refused; post-settlement the merged ledger binds and any overdraft is provable.
  • C18 — DKG: a dealer-free k-of-n actor signs a standard Ed25519 signature; one share cannot.
  • C-neg: expired, forged, and wrong-caller frames are denied with the precise reason; canonical CBOR rejects floats/bools.

Published vectors live in conformance/v1/ (cargo run -p jcp-conformance --bin gen-vectors).

10. What JCP closes (mapping to the documented failures)

Documented failure JCP mechanism
Prompts-as-security; inverse scaling law §1.1 enforcement outside the model
Tool poisoning / asymmetric trust §6 data≠control
Ambient authority, static env secrets §4 unforgeable grants
Confused deputy, token passthrough §4 content-addressed audience binding
Delegated authority §4.1–§4.2 attenuation chains, verified hop by hop
A leaked grant lives forever §4.3 issuer revocation, subtree-transitive
Runaway loops, no energy accounting §5 enforced joule budgets, conserved across delegation
Mutable / optional audit §8 signed JCR-1 receipts
Fragmentation across protocols §6 one frame for tools and agents

11. Wire binding: jcp/1 over a byte stream (OPTIONAL)

JCP is transport-agnostic; this section defines the reference binding for any ordered byte stream (TCP, Unix socket, pipe, TLS session).

Framing. Each message is a 4-byte big-endian length followed by the message's canonical det-CBOR bytes (§3). A receiver MUST reject a message longer than its declared cap and MUST reject a non-canonical body — a message has exactly one wire form, so content addressing stays injective across the wire.

Messages. One request/response pair per action:

WireRequest  { frame, chain: [signed-grant…], caller: did:key, cost: energy,
               now: uint, signature: bytes(64) }
WireResponse { decision, receipt: bytes }

chain is the full delegation chain, root first (a single un-delegated grant is a one-link chain). now makes decision time an explicit input — a decision is replayable, not ambient. The response carries the deterministic decision and the sealed JCR-1 receipt, deny and allow alike.

Caller authentication. signature is Ed25519 by caller over the canonical bytes of every other field. The endpoint MUST verify it before deciding; a request that fails is denied bad-signature and sealed like any other decision — the refused impersonation is evidence too. A threshold actor (§2.2) signs the same field with a k-of-n aggregate, indistinguishable on the wire.

The wire never carries a secret: grants are signed public objects, and a captured request replays into the same deterministic decision and the same budget accounting it produced the first time. The endpoint behind the enforcement point is invoked only on Allow.

12. Non-goals

JCP does not define a model or a tool registry. It does not detect misbehavior with probabilistic "trust scores" — bounded capability and enforced budget make the dangerous action unreachable rather than observed. Behavioral scoring MAY be layered above as a secondary signal; it is never the gate.

  • C20 — standing grants: an event-triggered firing mints a strictly-weaker child that traces to the standing grant; the wrong event class, an exhausted firing count, and a firing past the cumulative budget are each refused without advancing the ledger; a policy that would amplify its grant is refused at author time.
  • C21 — checkpoints + amendments: an agent checkpoints its own state; only the grant's issuer — a principal distinct from the agent — may amend, only to tighten; an agent steering itself, a non-issuer amending, a widening amendment, and an amendment bound to a different checkpoint are each refused.
  • C23 — receipt binds its payload + profile migration: a jcp/2 receipt commits the payload_hash it decided over; substituting only the payload (same grant, action, and cost) fails verification — the forgery a payload-blind receipt could not detect; and receipts published under jcp/1 still verify, because the verifier recomputes the preimage under the profile the receipt names rather than assuming one.
  • C24 — caveats: a caveat embedded in the grant is enforced offline by the decision point; PayloadIs makes a grant one-shot (a replay with different arguments is denied caveat-unsatisfied); MaxCostUj bounds a single invocation independently of the cumulative budget; a caveat may narrow below the capability; attenuation inherits every parent caveat and refuses to drop one; and a grant with no caveats encodes byte-identically to one minted before they existed.
  • C22 — dual ceilings: a money ceiling beside the joule ceiling is enforced independently — the first to bind denies (funds-exceeded / budget-exceeded), a foreign-currency cost is refused currency-mismatch rather than converted, an allow debits both dimensions and a deny debits neither, and an absent money ceiling encodes byte-identically to a joules-only grant (so its content id and signature are unchanged). Attenuation narrows money independently: dropping, raising, or re-denominating a bounded parent's ceiling is refused; a plain attenuation inherits it.