Falsified, then fixed. With two holds outstanding on one grant, resolving the first released the second's reservation as well — measured at two 300-minor holds where resolving one left **zero** reserved instead of 300. The next call was then admitted against money and joules another call was still holding: exactly the bypass holds exist to close, reintroduced at the resolution rather than the admission. The cause is that nothing named the hold. §8.1 already said a settle "names a hold", and the receipt does name one by its envelope's content id — but the runtime's resolution took only the grant, so "resolve the grant's hold" was the only thing it could mean, and it removed the grant's whole entry. The joule path had this from the start; the money dimension inherited it last commit. Holds are now tracked individually and resolved by a `HoldId` the hold returns. Two ids exist on purpose and cannot be one: the handle is needed to *decide* a resolution, and the receipt's content id does not exist until the hold has been sealed — after the decision that placed it. Handles are opaque, local to the enforcement point, and monotonic, so a stale one resolves nothing. `hold`, `hold_priced` and `hold_chained` now return a `HoldHandle` carrying the id and what it reserved in both dimensions; `settle`, `settle_priced`, `settle_chained`, `cancel` and `cancel_chained` take the id. A chained hold is placed across every link under one handle, so one resolution clears it from all of them and no link is left reserved. Also corrected: `hold_chained` sized its bound from the debited spend, so two holds on one chain could each size against the same room — the same defect `hold_bound` had, in the chained path. `X402Profile` gains `ChargeHandle`, which is what resolves the one-charge-at-a-time limit it had: several `upto` charges can be reserved at once, each settled on its own terms. Whether a second is admitted is the grant's business — a per-call joule ceiling leaves room, a grant without one reserves its whole remainder and the next is refused. SPEC §8.1 states the naming rule and why, C30 covers it, and the conformance checker runs it: two concurrent holds, resolve one, the other's reservation stands in both dimensions, then each debits exactly what it settled. Co-Authored-By: Claude Opus 5 <[email protected]>
1954 lines
123 KiB
Markdown
1954 lines
123 KiB
Markdown
# 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](#4-the-grant-required).
|
||
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, carries every caveat of the grant before it (§4.9) and no
|
||
wider money ceiling (§4.8); and every signature in the chain verifies. A
|
||
structurally invalid chain is denied `chain-broken`; an amplifying one
|
||
`attenuation-violation`. The rules `attenuate` holds when it mints are held
|
||
again here, because a chain arrives signed by whoever minted it.
|
||
|
||
A delegated grant (one with a `parent`) MUST NOT be decided on its own. Alone,
|
||
nothing shows that it narrows what its issuer held, so its budget, caveats and
|
||
expiry would be taken at their word, as a root's are. A single-grant decision
|
||
over a delegated grant is denied `chain-broken`.
|
||
|
||
**Caveats bind on every link.** A chained decision MUST evaluate the caveats of
|
||
every grant in the chain, exactly as a single-grant decision evaluates its own
|
||
(§4.9); a chain is never a way past a condition its root imposed. In the
|
||
two-phase metered path a chain is authorized without debit, cost-dependent
|
||
caveats waiting, and then charged with the measured cost, which is debited from
|
||
every link. A priced chained decision applies both ceilings of §4.8 to **every**
|
||
link: the joule ceilings first, then the money ceiling of each link that has
|
||
one. An allow debits both dimensions from every link, so siblings share their
|
||
parent's money as they share its joules; a deny debits neither.
|
||
|
||
**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.
|
||
|
||
**Propagation.** The rule above binds an enforcement point *once it holds the
|
||
revocation*, and says nothing about how it comes to hold one. §4.3 requires an enforcement point to deny a revoked grant **once it holds the
|
||
revocation**, and says nothing about how it comes to hold one. With a budget
|
||
sharded across N enforcement points (§4.5), a revocation that has reached three
|
||
of five leaves the grant live at two, and — the part that matters for an audit —
|
||
an allow issued by a point that *had not yet heard* is indistinguishable, after
|
||
the fact, from an allow by a point that heard and ignored it. The first is a
|
||
propagation delay. The second is a violation of this specification.
|
||
|
||
An implementation that supports revocation **MUST** define how revocations reach
|
||
its enforcement points, and **SHOULD** let each point state how current its
|
||
revocation knowledge is, as a marker monotonic in whatever its revocation feed
|
||
counts — a sequence number, a log index, a timestamp. JCP neither defines nor
|
||
interprets the marker; it requires only that a larger value mean "has seen at
|
||
least as much".
|
||
|
||
Given a revocation issued at marker `M`, an allow from a point at
|
||
`known_through < M` is explicable and one at `known_through >= M` is not. Where a
|
||
point states no marker, an auditor **MUST** treat its allows as *unverifiable*
|
||
for revocation rather than as compliant — "did not say" is not "was current".
|
||
|
||
### 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 debited at this point since open; never a hold
|
||
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
|
||
exposure. The settlement cadence tunes its settled part, what each point has
|
||
debited since its last statement; work still in flight under a hold at a point
|
||
(below) is bounded by what that point's own decision left, and no cadence
|
||
shortens it. 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.
|
||
|
||
**Holds at a channel's points.** A point that spends optimistically through the
|
||
two-phase path seals a hold before each call's work (§8.1), and counts its own
|
||
unsettled holds against what it decides from, beside its own spend and its peers'
|
||
stated spend, as §8.1 requires of every enforcement point. Its peers cannot count
|
||
them, because a statement does not carry them. A hold debits nothing until it
|
||
settles, so a statement of what a point has spent leaves it out; nor can `spent_uj`
|
||
carry one faithfully, since a hold settles for no more than it holds, and often for
|
||
less, while `spent_uj` may never decrease, so a point that stated a hold in it would
|
||
go on stating the difference as spent after the hold settled. The statements
|
||
settle spend between points and nothing in flight at any of them: a call
|
||
admitted at one point and still running is invisible to the others until it settles
|
||
and a later statement includes its debit. That work is part of the exposure between
|
||
settlements, beside what each point has spent since its last statement. This is a
|
||
limit of the profile as it stands. Shards have no such window: a point's holds count
|
||
against its own shard, which no other point spends.
|
||
|
||
### 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.
|
||
|
||
The ledger is a figure the enforcement point holds, not the record of the
|
||
firings, and a ledger reading fewer firings than happened looks exactly like a
|
||
standing grant that fired less often. Each firing is therefore a decision sealed
|
||
as a receipt stating `minted_uj` (§8.1). Where the enforcement point can restart
|
||
(§4.11), it **MUST** record a firing's receipt durably before the child leaves
|
||
it, and on restart **MUST** rebuild each standing grant's ledger from its receipt
|
||
stream before firing that grant again. A ledger read from anywhere else, such as
|
||
a stored copy, **MUST NOT** lower the rebuilt one: the higher firing count and
|
||
the higher spend win. A refused firing mints nothing and is not recorded as a
|
||
firing. 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. It MAY also state what the run has **spent** so far: a
|
||
cumulative microjoule figure with the provenance grade of §5. An amendment
|
||
cites a checkpoint, is signed by the grant's issuer, MUST differ from the
|
||
agent, and MUST only **tighten**; it MAY also hand the agent a new
|
||
instruction-set commitment.
|
||
|
||
What "tighten" is measured against is normative, and it is the **remainder**,
|
||
not the issued budget:
|
||
|
||
- Where the checkpoint states its spend, an amendment MUST NOT set a budget
|
||
above `grant.budget_uj − spent.uj`, saturating at zero. Both the amending
|
||
side and the verifying side MUST enforce this: an enforcement point handed a
|
||
signed amendment never calls the amending routine, so a check that lives only
|
||
there is not a check.
|
||
- Where the checkpoint states no spend, the ceiling is the issued
|
||
`grant.budget_uj`. This is the weaker rule and is retained so that a
|
||
checkpoint written before the field existed remains valid; implementations
|
||
MUST NOT read an absent spend as zero spend, and SHOULD report the remainder
|
||
as unknown rather than as the full budget.
|
||
- A declared spend is cumulative, so across one run it MUST NOT decrease, and a
|
||
checkpoint that once stated a spend MUST NOT later omit it. Otherwise an agent
|
||
recovers its full issued ceiling by going quiet.
|
||
|
||
The figure is the agent's own signed claim, not an independent measurement; its
|
||
`provenance` grade travels with it so the amender can see which kind of claim it
|
||
is, and a deployment that needs more binds receipts (§5) to the checkpoint.
|
||
|
||
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.
|
||
|
||
The money dimension has a **two-phase** form as well as the single-phase one
|
||
above: a priced hold reserves a money bound before the work and resolves with
|
||
what it really cost, releasing the difference (§8.1). That is what a usage-based
|
||
charge needs — a maximum authorized in advance, an amount settled afterwards —
|
||
and it is enforced by the same ordered check, with no rate invented between the
|
||
dimensions.
|
||
|
||
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
|
||
ModelIn([content-id]) ; the frame must name its model (§6), and it must be one of these
|
||
WithinWindow({secs: uint, limit: MaxUj(uint) | MaxCalls(uint) | MaxDistinctPayloads(uint)})
|
||
; a sliding window over the grant's recent use (§4.12)
|
||
```
|
||
|
||
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.
|
||
|
||
Three 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. `ModelIn` says **which model** the grant
|
||
authorizes. The capability's `resource` names the endpoint, and one endpoint can
|
||
run many models; `ModelIn` names the model content addresses this grant may
|
||
run there. It ranges over the frame's `model` field (§6), which the runtime that
|
||
dispatches the call fills with the content address of the model it will load,
|
||
and it **fails closed**: a frame that names no model does not satisfy it, so a
|
||
dispatcher that does not say what it will run cannot exercise the grant. This is
|
||
authorization by content address, not a registry (§12): the grant names the
|
||
bytes it permits, and nothing has to be looked up. A delegate narrows the set
|
||
by adding a smaller `ModelIn` beside the inherited one; both must hold, so the
|
||
intersection governs.
|
||
|
||
**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. The
|
||
receipt states the debit (§8.1), so a restart keeps it.
|
||
|
||
**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**.
|
||
|
||
### 4.10 Flow: what a value's provenance may exercise (OPTIONAL)
|
||
|
||
A grant answers *who* may do *what*, for *how many joules*. It does not answer
|
||
where the **argument** came from. An agent holding a legitimate grant, handed a
|
||
value that originated in an untrusted channel, exercises real authority on
|
||
attacker-chosen input — and every check above passes, because the grant is valid
|
||
and the action is granted.
|
||
|
||
A value therefore carries a **label**, and a grant MAY carry a ceiling the label
|
||
must sit below.
|
||
|
||
```
|
||
Label { integrity: Integrity, origins: Origins, readers: Readers }
|
||
```
|
||
|
||
The three components order independently and the label order is their product.
|
||
`⊥` is a principal-authored value from no channel, readable by all — the label of
|
||
anything in a run that has touched no untrusted source. `⊤` is untrusted, from
|
||
anywhere, readable by nobody.
|
||
|
||
**JCP consumes a label; it does not compute one.** Propagation — the label of a
|
||
result being at least the join of the labels of everything that went into it — is
|
||
the host's job, because only the host sees the data move. JCP's contribution is
|
||
the gate and, below, the rule that an *uncomputed* label is not a clean one. A
|
||
host that propagates badly will present a label JCP cannot audit; what JCP
|
||
guarantees is that a host which propagates at all gets a decision bound to what
|
||
it reported, and that one which does not gets a refusal rather than a pass.
|
||
|
||
A grant expresses its ceiling as a `FlowBelow` caveat (§4.9) carrying a
|
||
`FlowCeiling`: the ceiling label, and optionally a `tainted_budget_uj` — a
|
||
separate, smaller joule allowance for calls whose argument is not `⊥`. Where a
|
||
chain carries several, they compose to the **strictest**; attenuation never
|
||
widens one.
|
||
|
||
An enforcement point MAY also configure a **taint floor** per resource — the flow
|
||
analogue of a trust root (§7). The ceiling is what the *issuer* will permit; the
|
||
floor is what the *enforcement point* will accept regardless of who granted what.
|
||
Default-open, so an unconfigured point behaves exactly as before.
|
||
|
||
**The gate.** A decision is denied unless the argument's label sits at or below
|
||
the grant's ceiling **and** the resource's taint floor does not refuse its
|
||
origins. Three cases are distinguished, and the distinction is the point:
|
||
|
||
| facts | meaning | result |
|
||
|---|---|---|
|
||
| absent | provenance was **not computed** | **fails closed** where a ceiling is present; a ceiling-free grant is unaffected |
|
||
| `⊥` | computed, and clean | passes any ceiling |
|
||
| labelled | computed, and tainted | tested against the ceiling and the floor |
|
||
|
||
"Not computed" **MUST NOT** be read as "clean". An un-migrated caller that cannot
|
||
say where its argument came from has not established that the argument is safe,
|
||
and a grant whose issuer took the trouble to set a ceiling MUST NOT be exercised
|
||
on an unlabelled value.
|
||
|
||
**Where the check sits is normative.** It MUST run *after* capability and
|
||
signature verification — the ceiling lives inside an authenticated grant, so
|
||
judging flow before the signature would seal a verdict over attacker-chosen
|
||
policy — and *before* caveats and the budget test, so that **a tainted call costs
|
||
zero joules**. A denial on flow is the same invariant as a denial on capability:
|
||
nothing is debited and nothing runs.
|
||
|
||
**What "the argument" covers.** Where an enforcement point forwards a request to
|
||
an endpoint, the argument whose label is gated is everything the request carries
|
||
to the endpoint except the routing that names the action. That includes fields
|
||
the protocol leaves open: extension parameters, side-channel metadata, and
|
||
envelope members the enforcement point does not otherwise interpret. Any such
|
||
field it leaves unlabelled is a way past the ceiling. The enforcement point MUST
|
||
label each of these fields, and MUST seal it with the arguments, or else MUST
|
||
NOT forward it. It MAY leave out the protocol's own fixed machinery (a version, a
|
||
capability set, a progress token) where folding that machinery would taint every
|
||
request. If it does, it MUST say which fields it leaves out. A host that admits
|
||
every value it labels SHOULD label a request's parts so that combining them adds
|
||
no text of its own. Otherwise the framing becomes shared text that carries one
|
||
tainted request's label into every request after it.
|
||
|
||
A grant carrying no `FlowBelow` caveat is byte-identical to one minted before
|
||
flow existed, and an enforcement point with no taint floor configured decides
|
||
exactly as it did before. Conformance class **C25**.
|
||
### 4.11 Durable spend: a restart is not a fresh budget (REQUIRED where a run can restart)
|
||
|
||
A grant's budget is enforced against the spend an enforcement point has
|
||
accumulated, and that accumulation is state. Where the enforcement point can
|
||
restart — any long-running orchestrator, worker pool or gateway — an
|
||
implementation that holds the spend only in memory gives every grant its full
|
||
budget back on restart. A crash loop then spends a grant's budget repeatedly,
|
||
and the ceiling that was meant to bound the work bounds one process lifetime
|
||
instead.
|
||
|
||
An implementation whose enforcement point can restart therefore **MUST** record
|
||
every decision that debited a grant durably, whatever its verdict, and on restart
|
||
**MUST** re-establish the spend from that record before deciding anything new. Re-establishing it by **replaying
|
||
the recorded decisions through the gate** is RECOMMENDED over restoring a stored
|
||
total: a replay cannot produce a spend the gate would not have produced, and a
|
||
stored total is one more figure that can be wrong or tampered with.
|
||
|
||
**One writer per record (MUST).** The replay above re-establishes spend *from*
|
||
the record, so two enforcement points sharing one record each re-establish the
|
||
**same** spend, each decide against it, and each append. The budget is spent
|
||
twice and every entry still verifies — the requirement above is defeated one
|
||
layer beneath itself, and nothing in the record shows it. An implementation
|
||
**MUST** therefore ensure a single writer per record: it **MUST** take
|
||
exclusive access when it opens the record, and **MUST** fail to start rather
|
||
than proceed where another writer already holds it.
|
||
|
||
An exclusive claim an operating system releases when the holding process dies
|
||
is preferable to one an implementation must clean up, because a crash then
|
||
leaves nothing to recover. Where a platform cannot provide exclusivity at all,
|
||
the implementation **MUST** fail to open the record rather than proceed —
|
||
deciding against a record another endpoint may also be writing is the overspend
|
||
this requirement exists to prevent, and a platform limit is not a licence for
|
||
it. An implementation that instead proceeds **MUST** make that fact checkable
|
||
at run time rather than leave the type to imply a property it does not hold.
|
||
|
||
**The decision is recorded before the action runs.** Recording it afterwards
|
||
loses the debit for any action interrupted between the two — the grant was
|
||
debited when the decision was made, so a record written after the fact reports a
|
||
budget that has more left in it than it does. Where the cost is known only once
|
||
the action has run (the metered two-phase path, §7), the decision recorded before
|
||
it is a **hold** (§8.1), which states the most the action can debit and counts
|
||
against the budget until the receipt recording the action's outcome settles it.
|
||
|
||
That ordering leaves one state the record cannot resolve. A recorded decision
|
||
with no recorded outcome means the enforcement point stopped while the action
|
||
was in flight, and **whether the action took effect is not knowable from the
|
||
record** — only the party that was running it knows. An implementation **MUST
|
||
NOT** silently retry such an action, and **MUST NOT** report it as completed or
|
||
as failed. Its energy **MUST** stay debited, because the decision that debited it
|
||
was made and sealed. The honest resolution is to surface it as unresolved,
|
||
naming the party that held it, and let an operator or that party settle it:
|
||
retrying risks performing paid work twice, discarding risks losing it, and
|
||
choosing between those without asking is a decision the record does not support.
|
||
|
||
### 4.12 Windows: a bound on rate, not only on total (OPTIONAL)
|
||
|
||
A grant's budget bounds what it may spend over its whole life. It does not bound
|
||
how fast: a grant with a day's budget can spend it in a minute. `WithinWindow`
|
||
bounds a grant's use over any `secs` seconds. A call admitted at `t` is inside
|
||
the window decided at `now` while `now − t < secs`. The window bounds one of:
|
||
|
||
- `MaxUj(n)`: the joules metered for the calls inside the window;
|
||
- `MaxCalls(n)`: the calls admitted inside it;
|
||
- `MaxDistinctPayloads(n)`: the distinct `payload_hash` values among those calls,
|
||
that is, how many different things the grant is used on, not how often.
|
||
|
||
A window predicates over the clock, the frame's `payload_hash` and the metered
|
||
cost, as §4.9 requires, plus the grant's own recent history at this enforcement
|
||
point. That history is state the enforcement point keeps. A window therefore
|
||
cannot be decided from the frame alone. Like a flow ceiling, it is decided by a
|
||
gate in the runtime, after the other caveats and before the budget.
|
||
|
||
- **What counts.** A call counts once it is admitted: allowed in the one-shot path,
|
||
or authorised in the two-phase path. Its joules count from the debit, at the time
|
||
the call was admitted. A denied call counts for nothing, and is debited nothing.
|
||
- **Over a chain**, every link's windows count the call. A window on a parent
|
||
therefore bounds what all its delegates do together, as its budget does. A child
|
||
inherits its parent's windows and MAY add tighter ones (§4.9).
|
||
- **When it binds.** A call that would exceed a window is denied `window-exceeded`.
|
||
- In the one-shot path, a joule window admits a call only if its cost fits.
|
||
- In the two-phase path the cost is not yet known. A joule window admits a call
|
||
while any of the window is left, and binds at the debit, as `MaxCostUj` does.
|
||
An overrun is denied, its joules stay debited, and the window refuses the
|
||
next call until enough has slid out.
|
||
- **A debit with no time.** In the two-phase path, a debit that arrives without
|
||
the time of its call MUST be placed no earlier than the grant's most recent
|
||
admission. A debit with no admission at all MUST stay inside every window.
|
||
Placing a debit too early would let it leave a window before it should.
|
||
- **A window of zero seconds** bounds nothing, and MUST be refused
|
||
(`caveat-unsatisfied`) rather than read as no limit.
|
||
|
||
**Durability.** A window's history is spend over time, so §4.11 applies to it.
|
||
An enforcement point that rebuilds its state by replaying its decisions through
|
||
the gate, each at the time it was decided, rebuilds its windows with it. One that
|
||
rebuilds only cumulative spend, from receipts or a ledger that carries no times,
|
||
**cannot know what its windows held**. It MUST treat every window as full until
|
||
its history is complete again: until it can name a time from which it has decided
|
||
every call, and a whole window has passed since. An unknown history is not an
|
||
empty one.
|
||
|
||
**Sharding** (§4.5). A window is enforced where its grant's history is kept. A
|
||
grant whose history is split across enforcement points that do not share it is
|
||
bounded at each point separately, so `n` points admit up to `n` times the limit.
|
||
A deployment that needs one bound across points MUST route the grant's calls
|
||
through one of them, or share its history between them.
|
||
|
||
### 4.13 A retry is not a second call (RECOMMENDED where a transport can redeliver)
|
||
|
||
§4.11 makes a restart give no budget back. It leaves open the case one layer
|
||
out: the same call delivered twice.
|
||
|
||
An endpoint already recognises a repeat it has seen within its freshness window,
|
||
by the request's content address, and answers it from the record without
|
||
debiting again. That catches a redelivery which reproduced the request's bytes
|
||
exactly — and the bytes include the caller's stamp. So it catches the fast,
|
||
byte-identical redelivery, and misses the one that matters: a transport whose
|
||
retry is slow enough that the first stamp has aged out, where the caller must
|
||
**re-stamp** to be admitted at all. That retry has a different content address,
|
||
is admitted as a new call, and is debited again. The overspend §4.11 exists to
|
||
prevent happens anyway, for a reason §4.11 does not reach.
|
||
|
||
The cause is that identity is **incidental** — derived from the bytes — rather
|
||
than declared. A caller therefore **SHOULD** name its call: 32 bytes it chose,
|
||
the same across every delivery of that call and different between calls, signed
|
||
with the rest of the request. Where a request carries one, an endpoint **MUST**
|
||
recognise a repeat by it rather than by the request's bytes, and **MUST** answer
|
||
a repeat what it answered the first delivery without debiting the grant again.
|
||
|
||
Two consequences follow from the id being the caller's.
|
||
|
||
- **It is scoped to the caller.** The key is the pair, never the id alone. A
|
||
global id space would let one caller suppress another's call by guessing its
|
||
id — a denial with no trace, since a suppressed call looks exactly like a
|
||
recognised repeat.
|
||
- **An absent id is not a promise of uniqueness.** Where a request carries none,
|
||
every delivery is a distinct call and the content-address behaviour above is
|
||
all an endpoint can offer. A host **MUST NOT** infer an identity the caller
|
||
did not declare: two calls that happen to be byte-identical may be two calls
|
||
the caller meant.
|
||
|
||
**The guarantee is bounded, and says so.** An endpoint remembers a call id for a
|
||
declared **horizon**, and a redelivery arriving after it is not recognised. The
|
||
horizon **MUST** be at least as long as the longest retry of the transport in
|
||
front of it, or the mechanism does not cover the case it was added for.
|
||
Remembering every call forever would need state forever; a bounded guarantee
|
||
that names its bound is honest where an unbounded one would not be true.
|
||
|
||
**What this does not decide.** Whether an action *may* be re-dispatched at all
|
||
is a property of the action, not of the call id, and §4.11 already governs the
|
||
unresolved case: an endpoint does not silently retry, does not report an
|
||
in-flight action as completed or failed, and surfaces it naming the party that
|
||
held it. A declared id is what makes a re-dispatch *decidable* where a
|
||
dispatcher does classify its actions — "safe to re-dispatch" means nothing until
|
||
the re-dispatch can be recognised as the same call. Any such classification is
|
||
the implementing party's declaration, and the decision to act on it is the
|
||
enforcement point's. **A model is never asked whether an action already ran**:
|
||
it would be guessing about state it cannot observe, and a guess is not evidence.
|
||
|
||
And a recognised repeat is not free of cost, only free of the call's cost. The
|
||
endpoint did work to recognise it. What it must not do is debit the call twice.
|
||
|
||
Conformance class **C29** exercises the re-stamped retry with and without a
|
||
declared id, two deliberately-distinct calls, the caller scoping, survival
|
||
across a restart, and the horizon's bound.
|
||
|
||
## 5. Energy (REQUIRED)
|
||
|
||
Every metered cost is integer microjoules with a provenance tag: `HwShunt`
|
||
(hardware reading; a measured figure REQUIRED), `ModelBased` (a model
|
||
calibrated on the host or against a reading, or a real reading apportioned by
|
||
a model; measured MAY be absent), or `Estimator` (a figure nothing calibrated:
|
||
a coarse constant, or a model run at a default or assumed parameter, such as
|
||
time × a default power; 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; an interval runs from when one sample is in hand to when the next is, and
|
||
only one no longer than three of the probe's ticks, 75 ms at its 25 ms tick,
|
||
counts: a longer one counts as nothing, neither among the intervals, nor as an
|
||
advance, nor towards the 300 ms, which is the summed length of the intervals
|
||
that count). 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.
|
||
|
||
**Plausibility (REQUIRED where a bound is readable).** Liveness is necessary and
|
||
not sufficient: a counter can advance continuously and still not be counting
|
||
what it claims. An NVML total-energy counter on an RTX 4050 Laptop GPU (driver
|
||
595.84) advances in every sample interval while running at four to thirty times
|
||
the board's own power draw for as long as it is polled — perfectly live, and no
|
||
count of that board's energy. A figure from it would debit energy the device
|
||
never spent.
|
||
|
||
So where the platform exposes a bound that is read **independently of the
|
||
counter** — the board's maximum power limit, the board's instantaneous power
|
||
draw — a hardware-graded figure's implied rate MUST be checked against it, and a
|
||
counter whose rate exceeds the bound MUST NOT produce a hardware-graded figure.
|
||
An implementation SHOULD record the rate and the bound it broke, not merely that
|
||
it broke one: a counter a tenth over a bound and one twenty times over are the
|
||
same refusal and not the same finding, and re-grading a past refusal against a
|
||
revised bound is only possible if the rate survived the check.
|
||
|
||
The check is **two-sided**. Where the bound is the board's own draw, a counter
|
||
whose rate falls far below that draw MUST NOT produce a hardware-graded figure
|
||
either. This is the more dangerous of the two directions. Under-reporting is the
|
||
unsafe direction at a ceiling (§5), because it admits work the budget cannot
|
||
cover; a counter running fast fails safe, because it only spends a budget
|
||
sooner. A counter republished in lumps and sampled across its republish period
|
||
reads low rather than frozen, so liveness alone does not see it. The floor MUST
|
||
allow for the draw reading's own imprecision. It MUST NOT fire where the board
|
||
is near idle, because a correctly reporting counter accrues almost nothing there.
|
||
The reference meters refute a rate under a quarter of the board's mean draw,
|
||
after the draw's ±5 W accuracy is taken off it. Either bound refutes and never
|
||
verifies: a counter inside both is not refuted, and that is not a finding that
|
||
it is correct.
|
||
|
||
A bound reading that is itself impossible is not a reading: the same RTX 4050
|
||
reports about 590 W, past its own 65 W limit, for a second after it wakes. Such a
|
||
reading MUST be discarded — and only that reading. One broken sample is not a
|
||
broken bracket, and discarding the whole judgement on it reports an indeterminate
|
||
where the evidence supports a finding: on the reference hardware a single spike
|
||
among otherwise sane 14.8 W samples would otherwise excuse a counter running
|
||
persistently at four to thirty times the board's draw. Discarding them
|
||
individually also keeps the comparison honest, since a spike left in a mean makes
|
||
the bound it derives *more* permissive.
|
||
|
||
Only where **no usable reading remains** is the counter **unjudgeable** rather
|
||
than implausible: there is then no yardstick, so there is no finding about the
|
||
counter. It MUST NOT produce a hardware-graded figure either, because unjudgeable
|
||
is not admissible and declining is the safe direction, but an implementation MUST
|
||
NOT report it as a counter that overran. The two call for different operator
|
||
responses — replacing a counter, or waiting for a board to settle — and
|
||
conflating them loses that. A bound that is a static limit rather than a reading
|
||
(the board's maximum power limit) is never discarded, so a broken draw sample
|
||
cannot excuse a counter that overran that limit outright.
|
||
|
||
Where no such bound is readable, this requirement does not apply; liveness
|
||
stands alone.
|
||
|
||
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.
|
||
|
||
### 5.1 The metering ladder (REQUIRED behavior, RECOMMENDED backends)
|
||
|
||
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 or an
|
||
estimate (rungs 5 and 6 below).
|
||
|
||
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 and
|
||
hands nothing to another process (below). 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. Nor is work those threads hand to
|
||
another process: a system service that handles a message of theirs
|
||
(on macOS the kernel bills that service's CPU work to the process
|
||
beside the count, `ri_billed_energy`, never in it), a server reached
|
||
over a socket or a pipe. So a span over work that calls a system
|
||
service, or any other process, is not `Whole` for that work, and the
|
||
reference meter, which does not read the billed count on this rung,
|
||
does not detect it: an implementation SHOULD meter such work on another
|
||
rung, or grade its figure `Partial`. A span over a computation the
|
||
implementation knows runs on the metering thread alone and exchanges
|
||
nothing with any other process (the reference gateway's flow scan, a
|
||
pure computation over the process's own memory) is unaffected, and is
|
||
not made `Partial` by a child the process happens to have either: no
|
||
part of that work can be in the child or in a service. The count is
|
||
folded only when a thread leaves a core, and a process's read of its
|
||
own count does not fold the thread reading (its CPU time is current at
|
||
the read, its energy is not: a thread that burned 1 ms between two
|
||
reads of its own process read no advance in most tries, and up to a
|
||
quantum's worth from before the first read in others), so at each end
|
||
of a span the reading thread SHOULD leave its core just before it
|
||
reads, and an implementation that relies on that MUST check that it
|
||
did. A sleep does not always take a thread
|
||
off its core: when the sleep's timer fires before the thread has
|
||
blocked, the thread goes on without leaving (after 1 µs sleeps at load
|
||
averages of 85 to 118, 4 of about 44,000 showed no context switch, and
|
||
the read after missed the whole burn before it). The reference meter
|
||
sleeps 50 µs, then longer up to 5 ms, until the thread's own energy
|
||
count (`proc_pidinfo`'s `PROC_PIDTHREADCOUNTS`) moved across a sleep,
|
||
which it does only when the thread is folded; a process-wide
|
||
context-switch count is no such check, since another thread's switch
|
||
moves it. A read whose thread was not seen to leave its core is no
|
||
reading of that end, and the span is metered by a lower rung. Work on
|
||
the thread that opens and closes the span is then read whole, and the
|
||
figure also holds what every other thread of the process spent inside
|
||
it: over that work's CPU energy on those threads, not under it.
|
||
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.
|
||
|
||
The same count, read for every process of ANOTHER process tree the
|
||
metering process can read — the process the work runs in, such as the
|
||
server a gateway relays a call to, and its descendants — meters that
|
||
work → `HwShunt`. Its domain is the CPU energy of the tree's processes
|
||
over the span (as above, not DRAM): the advance of every process present
|
||
at both ends, plus the whole count of every process born inside the
|
||
span. A process is identified by its pid and its start time, so a reused
|
||
pid is another process. The count is folded only when a thread leaves a
|
||
core, so at each end of the span a member's count MUST be read with none
|
||
of its threads on a core or queued for one, or the member is a gap
|
||
(below). Nor is a member at rest while one of its threads is in a wait
|
||
that no signal ends (the kernel's own waits: for a block being written,
|
||
a page being brought in, a lock). That thread is off its core and
|
||
folded, and it runs again when the kernel is done, with nothing
|
||
arriving; a stop that took the member for rest there would read the
|
||
span short by the rest of its work, with nothing seen missing. So at
|
||
each end a member with a thread in such a wait MUST be waited out as a
|
||
running one is, or is the same gap. A walk of the tree reads a parent
|
||
before it lists and reads
|
||
the parent's children, so a child that exits and is reaped in between
|
||
is in that walk neither as a member nor in its parent's reaped time;
|
||
the walk at each end MUST therefore be confirmed by another, one after
|
||
the other, that agrees with it on the tree's processes, each one's
|
||
parent, reaped-descendant CPU time and exit time, with no listed child
|
||
gone by its read, or that end is a gap (below). The span SHOULD end
|
||
after the work's reply. The figure is all the tree spent inside the
|
||
span, whatever for: work the process defers past an earlier reply, or
|
||
does on a timer of its own, is read in whichever span is open when it
|
||
runs, and work after this span's stop is in no figure of this span. So
|
||
`Whole` says that nothing was seen missing from the tree's energy over
|
||
the span, not that all of it was spent on the span's own work. The
|
||
figure is `Partial` (§5.2) when the meter sees any of these over the
|
||
span; each is energy of the tree's over the span that the figure does
|
||
not hold, but for the ones noted:
|
||
- a descendant reaped inside the span (the reaper's reaped-descendant
|
||
CPU time moved; a reaped process's energy is in no counter), unless
|
||
it had already exited at the start, its energy then all spent before
|
||
the span;
|
||
- a member that exited inside the span: its record is final and is
|
||
counted, but it lacks the energy of the member's last time on a core
|
||
(an exiting thread's last slice is never folded into it: up to all of
|
||
a short-lived process's energy), and its GPU time is lost at exit, so
|
||
its GPU use is unknown;
|
||
- a member present at the start and gone at the stop (reaped, or moved
|
||
out of the tree): what it spent in the span before it went is not in
|
||
the figure;
|
||
- a member present at the stop that started before the span and was not
|
||
read at its start: its energy from before the span cannot be
|
||
separated, so none of it is counted;
|
||
- a member that could not be read (a process of another user), or
|
||
whose children could not be listed;
|
||
- a member not at rest at an end. Still running: at the stop its
|
||
reading lacks up to a scheduler quantum of energy per running thread;
|
||
at the START its reading is short by as much, so the figure may hold
|
||
up to that much energy spent just BEFORE the span. Or with a thread
|
||
in a wait no signal ends, in the middle of its work: what that thread
|
||
goes on to spend falls after the stop, or, at the START, inside the
|
||
span though the work began before it. This one can make the figure
|
||
exceed the tree's energy over the span, so a `Partial` tree figure
|
||
with it is not a lower bound, and an implementation that names the
|
||
gaps beside the figure SHOULD name the end (the reference gateway:
|
||
`unsettled-at-start`, `unsettled-at-stop`);
|
||
- an end whose walk no agreeing walk confirmed before the meter stopped
|
||
waiting: the tree kept changing, and a process that came and went may
|
||
be missing;
|
||
- a member holding a GPU client at either end: GPU time its client
|
||
recorded, or GPU use the reader cannot rule out, because a GPU
|
||
client's recorded time is per live command queue and a queue
|
||
created and released inside the span takes its time with it; or GPU
|
||
time that could not be read;
|
||
- a member that has held Neural Engine model memory in-process;
|
||
- work another process did on a member's behalf and the platform billed
|
||
to it (below): the figure holds the bill, which is short of the
|
||
span's delegated work by what no bill carries, and can hold a bill
|
||
for work done before the span that was credited inside it — so, like
|
||
a member still running at the start, it can make the figure exceed
|
||
the tree's energy over the span (the reference gateway: `delegated`);
|
||
- a member credited inside the span with service to other processes
|
||
(below): the credit is subtracted, and need not be the span's own
|
||
service — a credit for service done before the span makes the figure
|
||
short, service done in the span and credited after its stop leaves
|
||
the figure over — so it too can make the figure exceed the tree's
|
||
energy over the span (the reference gateway: `serviced`);
|
||
- a member credited inside the span with more service to other
|
||
processes than it spent and was billed (below): the figure may
|
||
subtract energy spent before the span.
|
||
|
||
**Delegated work.** Where the platform bills the CPU work one process
|
||
does on another's behalf to the other, the figure SHOULD count it. On
|
||
macOS the kernel's bank ledgers do: a service's thread that adopts a
|
||
client's voucher while it handles the client's message (an XPC or other
|
||
mach message carries one) is charged, at its next voucher change, with
|
||
the CPU time and energy it spent, which raise the client's
|
||
`ri_billed_energy` and `ri_billed_system_time` (not part of its
|
||
`ri_energy_nj`) and the servicer's `ri_serviced_energy` and
|
||
`ri_serviced_system_time` (part of its own), readable for any process of
|
||
the same user without privilege. Each member's count is then its own
|
||
energy, less its serviced energy, plus its billed energy, and the figure
|
||
the members' advances of it, summed signed: work a member did for other
|
||
processes is theirs, work others did for it is the tree's, and work one
|
||
member did for another is counted once, through the payer. A servicing
|
||
thread is credited at its next voucher change, not as it works, so a
|
||
credit inside a span need not be that span's service: a span over which
|
||
any member's serviced energy or serviced CPU time moved is `Partial`,
|
||
whether or not its net stayed above zero. A member whose net advance is
|
||
below zero — a credit for service done before the span landing inside
|
||
it — is counted as it is, never wrapped, and is a gap too. The bill over
|
||
a span is not the span's delegated work, and errs both ways. Over: a
|
||
bill is credited at the servicing thread's next voucher change, so a
|
||
bill for work done before the span — an earlier call's late credit —
|
||
lands in whichever span is open, and is added to its figure. Short: it
|
||
carries none of the GPU or Neural Engine energy spent in the service
|
||
(one on-device inference request on the reference hardware drew 6.73 J
|
||
on the Neural Engine and billed 1.79 J); none of the service's work on
|
||
threads that never adopt the voucher, or before they do (starting a
|
||
per-client service process); no credit that lands after the span (a
|
||
servicing thread is credited at its next voucher change, which can be
|
||
a second and more after the reply); nothing billed to another
|
||
originator (a scheduling daemon's background work, a policy daemon's
|
||
scan of the client's binary); and nothing sent over a socket or a
|
||
pipe, which carries no voucher, or off the host. So a span over which
|
||
any member's billed energy or billed CPU time moved is `Partial`, its
|
||
figure holding the bill, and an implementation that names the gaps
|
||
beside the figure SHOULD name it (the reference gateway: `delegated`).
|
||
Such a span's stop SHOULD wait, briefly and within a bound, for the
|
||
billed counts to hold, so that a credit landing just after the reply
|
||
falls in its own span rather than the next (the reference meter reads
|
||
them every 5 ms until two reads agree, at most 20 times). The billed
|
||
counts take no part in deciding that a member is settled: another
|
||
process's thread credits them, and the member's run state says nothing
|
||
of when.
|
||
|
||
An implementation MUST NOT grade a tree's figure `Whole` on an empty
|
||
list alone. `Whole` further requires the operator's declaration that the
|
||
process hands no work to a process outside its tree — a system service,
|
||
on-device model inference run by a system daemon, a database server, a
|
||
remote API — because a reader of the tree cannot see all such
|
||
delegation: billing shows only work done on a voucher, and the rest of
|
||
that work's energy is charged to the process that does it, which may be
|
||
another user's and unreadable. Without the declaration the figure is
|
||
`Partial`. The declaration does not outweigh evidence: a span with a
|
||
bill (above) is `Partial` under it, the bill showing work done outside
|
||
the tree. A span whose root is gone or unreadable at either end, or
|
||
whose figure did not advance, is metered by a lower rung. The reference
|
||
gateway (`jcp-gateway`) meters each gated call this way where the
|
||
upstream server is a local process it can read.
|
||
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 an active power
|
||
calibrated on the host (a measurement of that host's draw, such as its
|
||
draw under load read from a shunt or a live counter) → `ModelBased`.
|
||
6. **Estimate** → `Estimator`: a figure no host calibrated. Either wall-clock
|
||
duration times a default or assumed active power — a documented default,
|
||
a nameplate or datasheet figure, another host's calibration — or a
|
||
constant per-operation figure. Always available. Time × power is graded by
|
||
its power: the power is the whole of the model, so a power not calibrated
|
||
on the host makes the figure a constant scaled by time, and grading it
|
||
`ModelBased` would claim better provenance than how it was obtained. A
|
||
counter rung's span that degrades because its counter did not read the
|
||
span (above) lands here, unless the implementation holds a host-calibrated
|
||
power, in which case it lands on rung 5.
|
||
|
||
### 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.
|
||
|
||
The `efficiency/1` outcome receipt (§5.3) carries it as `energy_coverage`,
|
||
under the same one-value rule. Its headline figures are ratios of a task total:
|
||
energy per accepted token on the receipt, and energy per completed unit over a
|
||
ledger. So a `Partial` receipt MUST NOT carry `uj_per_accepted_token`, and a
|
||
verifier MUST refuse one that does. A total summed from receipts of which any
|
||
is `Partial` is `Partial` and gives no per-unit energy figure. A partial figure
|
||
divided by tokens is not what the work cost per token, nor a bound on it,
|
||
because a partial figure may fall short of the work or exceed it. The vectors
|
||
are in [`conformance/v1/efficiency.json`](conformance/v1/efficiency.json).
|
||
|
||
### 5.3 Traceability across operation, activity and outcome (REQUIRED where an outcome claims energy)
|
||
|
||
§5.1 grades a figure and §5.2 says how much of the work it covers. Neither says
|
||
which *level* the figure belongs to, and a deployed system is answerable at
|
||
three that are not interchangeable:
|
||
|
||
| level | artifact | what it answers |
|
||
|---|---|---|
|
||
| **operation** | the `jcp/2` work receipt (§8) | what one unit of work cost |
|
||
| **activity** | the AR-1 action receipt | what one *authorised* thing did — grant checked, environment attested, work performed, effect committed, bytes egressed |
|
||
| **outcome** | `efficiency/1` | whether any of it bought anything |
|
||
|
||
Two rules follow, and both exist because the levels were previously joined by
|
||
nothing.
|
||
|
||
**An outcome's energy MUST name its activities by content-id.** An outcome that
|
||
identifies its unit of work by a label can assert any total it likes, and the
|
||
level where the number is most load-bearing is then the level with no binding
|
||
at all. A verifier MUST be able to resolve each named activity and re-add the
|
||
figures. An outcome naming no activity is not traceable and MUST NOT be
|
||
presented as though it were.
|
||
|
||
**The difference MUST be named, not absorbed.** The sum of an outcome's
|
||
operations is not the outcome's energy. Authorising, attesting, verifying and
|
||
pushing bytes off the device all cost joules that no operation receipt claims —
|
||
AR-1 carries exactly one energy figure and it belongs to the work. That
|
||
difference is the cost of being accountable, and it is reported as a signed
|
||
residual in exact integer microjoules:
|
||
|
||
```text
|
||
residual_uj = outcome_total_uj − Σ activity.joules_uj
|
||
```
|
||
|
||
The residual MUST NOT be clamped. Negative means the bound activities claim more
|
||
than the outcome admits — double-counted work, or an activity bound to the wrong
|
||
outcome — which is a finding. An activity bound twice MUST be refused before the
|
||
sum: counting it twice inflates the accounted figure and shrinks the residual,
|
||
flattering the trace in the one direction that matters.
|
||
|
||
The composite provenance is the **floor** across the outcome and every activity,
|
||
never an average and never the best: one `Estimator` part makes the whole trace
|
||
estimated, because a trace cannot be more measured than the part of it that was
|
||
not measured. Coverage takes the floor the same way. Aggregation MUST NOT be a
|
||
step at which a grade improves.
|
||
|
||
### 5.4 Dynamism: what a deviation establishes, and what it never does (OPTIONAL)
|
||
|
||
A total cannot distinguish a throttled part from a contended host from a cold
|
||
cache, and on silicon nobody has profiled it cannot say what normal was. Where
|
||
an implementation carries energy as a time-resolved series, the following hold.
|
||
|
||
**Provenance and coverage are per-window.** Counters do not have uniform
|
||
resolution — a package counter may be continuous while an accelerator counter is
|
||
live only while a queue is — so one grade for a series is a grade for its best
|
||
window.
|
||
|
||
**A baseline MUST declare where it was measured, and MUST NOT be applied
|
||
outside it.** An expectation is a fit, and a fit is only a fit somewhere.
|
||
Comparing against a baseline taken on different silicon, at a different
|
||
occupancy, or at a coarser sampling window manufactures deviations that say
|
||
nothing about the workload. An undeclared baseline is inapplicable, so
|
||
forgetting to declare under-claims rather than inventing findings.
|
||
|
||
**A cause explains a deviation only with evidence.** A named operational cause
|
||
— thermal limit, host contention, cold start, frequency scaling, a meter
|
||
coverage gap — MUST carry a content-id for the reading that supports it.
|
||
Otherwise "explained" becomes the laundering channel: anything inconvenient is
|
||
labelled and disappears.
|
||
|
||
**Absence of detection is not absence.** A phenomenon shorter than one sampling
|
||
window is invisible to it. That case is *unresolvable*, never *within band*.
|
||
Reporting nothing found at a resolution that could not have found it is the
|
||
quietest available way to be wrong.
|
||
|
||
**There MUST NOT be a verdict of intent.** Energy does not carry intent. A
|
||
deviation says the joules did not match the expectation; it cannot say why, and
|
||
on an uncharted stack it cannot say the expectation was right. An unexplained
|
||
window is a request for a look, not a finding of wrongdoing, and a conformant
|
||
implementation MUST NOT collapse unexplained, inapplicable and unresolvable
|
||
windows into a single score — they mean "look at this", "we have no yardstick"
|
||
and "we could not have seen it", which call for different responses.
|
||
|
||
### 5.5 Conservation, not anomaly detection (REQUIRED where a grant is reconciled)
|
||
|
||
The strong instrument is conservation, and it needs no model of normal.
|
||
|
||
Because every figure is integer microjoules and the sums are exact, the smallest
|
||
representable discrepancy against a grant of `N` µJ is `1_000_000 / N` parts per
|
||
million — on a one-joule grant, **1 ppm**. A drift of a fraction of a percent is
|
||
three orders of magnitude above that floor. The question "do these joules fit
|
||
anywhere" is therefore answerable arithmetically, where "is this unusual" is not.
|
||
|
||
This property does not survive the usual construction, and that is the point.
|
||
Where an energy figure is *derived from a performance proxy* — utilisation times
|
||
a thermal design figure, tokens times a coefficient, instructions times
|
||
picojoules-per-operation — its model error is tens of percent, and a
|
||
fractional-percent drift is invisible by construction: it sits far inside the
|
||
error bar of the estimator that produced it. Deriving energy from performance
|
||
does not merely lose accuracy, it destroys the ability to notice that the books
|
||
do not balance. A conformant implementation MUST report whether the floor it
|
||
quotes rests on a measured figure or a modelled one; quoting a 1 ppm floor over
|
||
`Estimator` windows is exact arithmetic over a model's opinion.
|
||
|
||
### 5.6 The physical bound: refutation, never verification (OPTIONAL)
|
||
|
||
The joules were spent and the heat left the die; that quantity is not
|
||
negotiable. The *measurement* of it is exactly as forgeable as any other report
|
||
— a counter can be lied about, a figure fabricated, a `HwShunt` grade stamped on
|
||
a number from nowhere.
|
||
|
||
What energy has that a synthetic unit does not is an **external referent**. A
|
||
token count, a call count, a request rate can be any number whatsoever: nothing
|
||
outside the accounting system constrains them, so a fabricated one is
|
||
indistinguishable from a true one. A joule count is bounded by the part's power
|
||
envelope and the window's duration.
|
||
|
||
An implementation MAY therefore refute a figure that exceeds what the part could
|
||
have drawn, or falls below what a powered, executing part must draw — the second
|
||
being the shape a frozen counter takes, and the reason §5's liveness rule
|
||
exists. Bounds SHOULD be deliberately permissive (burst rating for the ceiling,
|
||
absolute idle for the floor) so that a refutation is unassailable.
|
||
|
||
Passing a physical bound MUST NOT be reported as verification. A fabricated
|
||
figure inside the envelope passes, and must: the bound establishes only that the
|
||
figure is not impossible.
|
||
|
||
**The one thing that cannot be faked is how the energy was consumed.** A report
|
||
of a profile is just numbers, but a part cannot draw the profile of one
|
||
computation while performing another. This closes the hole conservation leaves:
|
||
a total can be balanced by padding — invent an operation and attribute the
|
||
surplus to it — but a fictional operation contributes no draw, so the observed
|
||
character stops matching the declared work mix.
|
||
|
||
Character is also what survives unfamiliar hardware. The magnitudes move with
|
||
the process node, the power envelope and the memory hierarchy, which is why a
|
||
band baseline fails there; a compute-bound steady state is flat on any part, a
|
||
cold start ramps on any part, a queue-driven workload alternates on any part. A
|
||
declared shape MUST be declared in advance — classifying the observed shape and
|
||
then deciding what it must have been is post-hoc pattern matching. An
|
||
inconsistency refutes the declaration. It attributes no cause and names no
|
||
intent.
|
||
|
||
## 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
|
||
model: content-id? ; OPTIONAL: the model the action runs (§4.9 ModelIn)
|
||
}
|
||
```
|
||
|
||
`model` is filled by the runtime that dispatches the call, which knows the
|
||
content address of the model it will load. It is absent for an action that runs
|
||
no model, and an absent `model` is omitted from the canonical encoding, so such
|
||
a frame, and every receipt and vector over it, is byte-identical to one built
|
||
before the field existed.
|
||
|
||
`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`.
|
||
|
||
### 6.1 Continuations: work that answers later (REQUIRED where an enforcement point relays it)
|
||
|
||
An endpoint may answer a request before its work is done. It may return a
|
||
**handle** to work that goes on, whose outcome the client fetches in later
|
||
exchanges that name the handle (MCP's tasks; an A2A task). Or it may return an
|
||
**interim result** asking the client for input, which the client answers by
|
||
repeating the request with its answers attached (MCP's `input_required`). Each
|
||
later exchange moves the first request's work and the first request's kind of
|
||
payload: the outcome is the action's output, and the answers go to the
|
||
endpoint as its arguments do. So an enforcement point that relays such
|
||
exchanges and treats the later ones as discovery has a door beside its gate.
|
||
Such an enforcement point:
|
||
|
||
1. MUST decide each later exchange as a **continuation** of the first: against
|
||
the grant the first was decided on, at the moment the later exchange is
|
||
made, so a grant revoked, expired or spent since refuses it (§7), and the
|
||
outcome of work the grant no longer covers is not fetched under it. It MUST
|
||
seal each continuation's decision (§8) over a payload that commits to the id
|
||
of the receipt it continues (the first's, or the previous continuation's),
|
||
so that one piece of work's receipts form a chain within the stream.
|
||
2. MUST refuse, and seal the refusal of, a later exchange naming a handle it did
|
||
not see returned to a request it decided. A handle is a bearer string the
|
||
endpoint minted, and no grant covers it until the enforcement point has bound
|
||
it to one. A handle it has closed (item 4) or forgotten (a restart, a bound on
|
||
what it keeps) is refused the same way: fail closed.
|
||
3. MUST flow-check what the client authored in a later exchange (the answers)
|
||
as it does the first request's arguments (§4.10), and MUST admit what comes
|
||
back — status text and the outcome alike — as `data`, labelled as the output
|
||
of the first request's action and joined with the label of everything the
|
||
client has sent the work. An exact echo of a handle or state the endpoint
|
||
minted MAY be left out of the flow check, being the endpoint's own text;
|
||
anything else the client sends, a handle it made up included, MUST be
|
||
checked. What the enforcement point forwards SHOULD be built from the fields
|
||
it decided over, so that nothing it did not check reaches the endpoint.
|
||
4. MUST meter and debit each later exchange (§5), and, when the outcome is
|
||
final, MUST seal one **closing record**: a `tool-result` (or `agent-reply`)
|
||
frame tagged `data`, debiting nothing, whose payload is the content address
|
||
(§3) of a record naming the first receipt, every continuation's receipt, the
|
||
joules metered across them, and the outcome's digest. The endpoint works
|
||
between exchanges, inside no exchange's span, so the record MUST mark that
|
||
sum `Partial` (§5.2) unless the meter read the work between them.
|
||
|
||
The reference gateway (`jcp-gateway`) does this for MCP's `tasks/get`,
|
||
`tasks/update` and `tasks/cancel`, and for a `tools/call`, `resources/read` or
|
||
`prompts/get` retried with `inputResponses`; it advertises it in its
|
||
`server/discover` and `initialize` responses as `continuations`.
|
||
|
||
## 7. The decision (REQUIRED)
|
||
|
||
Given a Frame, its [SignedGrant](#4-the-grant-required), 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
|
||
debited_uj = uint ; omitted where the verdict says it (§8.1)
|
||
chain_ids = [hex] ; a multi-hop chain only, root first (§8.1)
|
||
chain_debited_uj = [uint] ; where those links were debited differently
|
||
debited_minor = uint ; the money debited, where any was (§4.8, §8.1)
|
||
chain_debited_minor = [uint] ; where those links were debited different sums
|
||
tainted_uj = uint ; a tainted accrual no flow block says (§4.10, §8.1)
|
||
held_uj = uint ; a hold: the most its work can debit (§8.1)
|
||
held_minor = uint ; a hold's money reservation, omitted when none (§4.8, §8.1)
|
||
settles = hex ; the hold whose work this decision accounts for (§8.1)
|
||
minted_uj = uint ; a firing: the budget of the child it minted (§4.6, §8.1)
|
||
seq, prev = uint, hex ; a hash-chained log only (§8.1)
|
||
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`](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 a budget needs to remember: the grant, the verdict,
|
||
the debit in joules and in money, what a tainted call accrued against its
|
||
sub-budget, and each firing of a standing grant. 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: each
|
||
receipt re-applies what it debited to every grant it names, ancestors included,
|
||
with no external index, so a restart restores exactly what was debited.
|
||
Durability and audit are the same artifact; there is no second database to drift
|
||
from the evidence.
|
||
|
||
A log commits to its order. Its entries carry `seq`, counting from zero, and
|
||
`prev`, the content id of the previous entry's envelope (64 zeros for the first),
|
||
so an entry cannot be removed or moved without breaking the link after it.
|
||
|
||
**What a receipt debited.** The verdict does not settle it. A denial from the
|
||
debit point (§4.9, §4.12) debited work that ran; the same reason sealed before any
|
||
work debited nothing, though it may record the `cost_uj` it refused. And a debit in
|
||
the two-phase path saturates at the ceiling, each link of a chain at its own, so it
|
||
can be less than `cost_uj`. A receipt is therefore read in this order:
|
||
|
||
1. With `chain_debited_uj`, link `i` of `chain_ids` was debited entry `i`. The two
|
||
have the same length.
|
||
2. Otherwise, with `debited_uj`, every grant the receipt names was debited that
|
||
figure.
|
||
3. Otherwise an allow debited `cost_uj` from every grant it names. A denial
|
||
debited nothing, unless it names `caveat-unsatisfied` or `window-exceeded`:
|
||
such a receipt was sealed before `debited_uj` existed and is read as the next
|
||
paragraph says.
|
||
|
||
A sealer MUST state `debited_uj` wherever rule 3 would misread the decision, and
|
||
on every denial naming `caveat-unsatisfied` or `window-exceeded`, the two reasons
|
||
the debit point seals, as `0` when nothing was debited. It MUST state
|
||
`chain_debited_uj` where a chain's links were debited different figures; `debited_uj`
|
||
is then the leaf's. Every other receipt omits both and keeps the bytes it always
|
||
had, and the `jcp/2` tag does not change. A refusal debits nothing (C3), and a
|
||
receipt that says so restores nothing.
|
||
|
||
**Money and the tainted sub-budget.** A priced allow debits money beside its joules
|
||
(§4.8), and a tainted call accrues its joules against its grant's tainted
|
||
sub-budget (§4.10). A receipt says both:
|
||
|
||
- With `chain_debited_minor`, link `i` of `chain_ids` was debited entry `i` in money;
|
||
otherwise, with `debited_minor`, every grant the receipt names was debited that
|
||
sum. The sum is in the minor units of the grant's money ceiling, and a receipt with
|
||
neither member debited no money.
|
||
- With `tainted_uj`, every grant the receipt names accrued that figure. Otherwise a
|
||
receipt carrying a `flow` block that debited, by the rules above, accrued its
|
||
`cost_uj`, which is what a tainted call accrues, and any other receipt accrued
|
||
nothing.
|
||
|
||
A sealer MUST state `debited_minor` wherever a decision debited money, and
|
||
`chain_debited_minor` where a chain's links were debited different sums (a link with
|
||
no money ceiling is debited none); `debited_minor` is then the leaf's. It MUST state
|
||
`tainted_uj` wherever the reading above would misread the accrual: on a tainted
|
||
decision sealed without a flow block, as a chained one is, and as `0` beside a flow
|
||
block whose decision accrued nothing. A tainted call sealed with its flow block
|
||
omits it and keeps the bytes it always had.
|
||
|
||
**A hold: the decision recorded before the work.** In the two-phase path an
|
||
enforcement point authorizes, lets the work run, and only then knows what to debit
|
||
(§7). A receipt sealed after the work therefore leaves the work out of the stream
|
||
until it is sealed, and an enforcement point that stops between the two restores
|
||
less than was spent (§4.11). So it seals the allow as a **hold** before the work
|
||
starts, stating `held_uj`, and records the hold durably before the work starts; the
|
||
receipt it seals after the work states `settles`, the content id of the hold's
|
||
envelope. A stream reads holds so:
|
||
|
||
- A hold, an allow stating `held_uj`, debits nothing itself.
|
||
- A hold that a receipt in the stream settles restores nothing; that receipt
|
||
restores what the work debited, by the rules above.
|
||
- A hold that no receipt in the stream settles was interrupted: its work was in
|
||
flight when the enforcement point stopped. Every grant it names reads as debited
|
||
`held_uj`, and, where the hold carries a `flow` block, as having accrued `held_uj`
|
||
against its tainted sub-budget.
|
||
|
||
A sealer MUST state a `held_uj` no smaller than what the work's decision can debit
|
||
from any grant the hold names: in the two-phase path, what the grant has left, since
|
||
that debit saturates at the ceiling, and for a tainted call also what its tainted
|
||
sub-budget has left. A tainted call accrues its whole cost, which nothing bounds
|
||
before it runs, but past what the sub-budget has left the gate refuses tainted work
|
||
whatever was accrued, so a hold covering that leaves the sub-budget as spent as any
|
||
accrual could. A refusal lets nothing run and debits nothing (C3), so it is never
|
||
sealed as a hold, and `held_uj` on a refusal holds nothing. A hold carries its
|
||
call's flow block, which the settling receipt carries again; the monitor's
|
||
`flow_uj` was spent once. An enforcement point that restores a stream holding a hold
|
||
no receipt settles treats it as §4.11 says of a decision with no recorded outcome:
|
||
it neither retries the work nor reports it as completed or failed, it names the hold
|
||
as unresolved, and the hold's grants stay debited `held_uj` while nothing settles it.
|
||
|
||
**A hold counts while its work is out (MUST).** The reading above is a replay's,
|
||
but a hold's work is in flight from the hold until the receipt that settles it, and
|
||
the enforcement point goes on deciding meanwhile. An enforcement point **MUST**
|
||
count every hold it has sealed and not yet settled against the budget it decides
|
||
from, live as well as on replay: every grant the hold names counts as debited its
|
||
`held_uj`, and, where the hold carries a `flow` block, as having accrued `held_uj`
|
||
against its tainted sub-budget, until the settling receipt replaces the hold with
|
||
what the work debited. It is prior spend in §7's check 9. In the two-phase path a
|
||
hold states what its grant has left, so while it is unsettled the grant has nothing
|
||
left: every other two-phase call under it, and every one-shot call that would debit
|
||
anything, is refused `budget-exceeded` by check 9 wherever no earlier check refuses
|
||
it first. A hold through a delegation chain names every link in `chain_ids`, and since
|
||
a chained debit saturates at each link's own ceiling, its `held_uj` is what the link
|
||
with the most left has left. Counted against every link, it leaves each with
|
||
nothing, so a sibling delegate's call through a held ancestor is refused as the
|
||
holder's own next call is. A point that
|
||
counted holds only on replay would decide each call that arrived while a held call's
|
||
work ran against the whole remainder: each settle saturates at the ceiling, so the
|
||
stream would show no more than the ceiling debited, while the work admitted could
|
||
draw up to the remainder once for each call.
|
||
|
||
A point that decides one call at a time, settling each before it decides the next,
|
||
makes no decision while one of its holds is unsettled, provided no other point
|
||
decides against its record. The reference gateway (`jcp-gateway`) is such a point:
|
||
it holds, calls upstream and settles inside one exclusive call, and §4.11's one
|
||
writer per record keeps a second enforcement point off its record. It counts its
|
||
holds all the same, so a call that unwinds before its settle and leaves the gateway
|
||
serving leaves its hold counted. A point that admits calls concurrently **MUST**
|
||
count each outstanding hold's `held_uj` before it admits the next call, which means
|
||
placing the hold in the same step as the allow it records: two decisions that read
|
||
the budget before either hold is counted each see the whole remainder.
|
||
|
||
**A hold resolves exactly once (MUST).** A hold is resolved by the receipt that
|
||
settles it, and by nothing else. An enforcement point **MUST** refuse a settle
|
||
naming a hold that is not outstanding — one already resolved, or never placed —
|
||
as `already-resolved`, and **MUST** debit nothing for it.
|
||
|
||
**A resolution names *which* hold it resolves (MUST).** Where a point admits
|
||
calls concurrently, a grant can carry more than one outstanding hold, and a
|
||
resolution that names only the grant cannot say which of them it accounts for. A
|
||
point that resolved "the grant's hold" releases every reservation on it: the
|
||
reference implementation did, at two 300-unit holds where resolving one left
|
||
zero reserved instead of 300, so the next call was admitted against money and
|
||
joules another call was still holding — the precise bypass holds exist to close,
|
||
reintroduced by the resolution rather than the admission. A resolution
|
||
**MUST** therefore identify one hold and leave the grant's others outstanding.
|
||
How it is identified is the point's own business — the receipt names the hold by
|
||
its envelope's content id, which does not exist until the hold is sealed, so a
|
||
point deciding before it seals needs a name of its own for the hold in
|
||
flight. A point that settled
|
||
unconditionally would debit the same work twice whenever a settle is retried or
|
||
replayed, and the stream would show it: the reference implementation did, at 80
|
||
µJ recorded for 40 µJ of work, because removing an absent hold removes nothing
|
||
while the debit lands all the same.
|
||
|
||
**What a hold reserves (MUST).** A hold states an upper bound the work is not
|
||
permitted to exceed, never an estimate of its outcome; an estimate is a figure
|
||
the work may exceed, so holding one restores the bypass in a quieter form. Where
|
||
the grant states a **per-invocation** ceiling in advance — §4.9's `MaxCostUj`,
|
||
the tightest where more than one link states one — that ceiling is the bound, and
|
||
a hold reserves it. Where the grant states none, the only upper bound in
|
||
existence is the grant's **whole free remainder**, computed from the committed
|
||
spend so that two holds cannot each size themselves against the same room.
|
||
|
||
The difference is concurrency, and it is not cosmetic. A grant stating a per-call
|
||
ceiling admits a second call while the first is still out, each bounded; a grant
|
||
stating none has one outstanding hold reserve everything left, so it is bounded
|
||
by **serialization** — correct, but one call at a time. A deployment that wants
|
||
concurrent metered work states a per-call ceiling; one that does not, accepts the
|
||
queue. A call that overruns its stated ceiling has under-reserved, and the debit
|
||
at resolution still stands: the overrun is recorded as a `caveat-unsatisfied`
|
||
verdict over a real debit, exactly as a budget overrun is, rather than absorbed.
|
||
|
||
**A hold reserves money too (REQUIRED where §4.8 applies).** On a grant carrying
|
||
a money ceiling, a hold placed through the priced two-phase path reserves a
|
||
**money** bound beside its joule bound, stated as `held_minor`, and the rules
|
||
above apply to it unchanged: it is an upper bound the payer authorized, not an
|
||
estimate of the charge; it is checked against the **committed** money spend, so
|
||
concurrent holds bound one another; and it debits nothing until the hold
|
||
resolves.
|
||
|
||
This is the shape usage-based payment has. A charge whose final amount is known
|
||
only after the work can still be bounded before it: the payer authorizes a
|
||
maximum, the work runs, and the resolution debits what it actually cost — the
|
||
remainder released by the hold's removal. Without the release a ceiling is
|
||
consumed by the worst case of every call, and a session settling at a tenth of
|
||
its maxima exhausts a ceiling ten times too fast; with it, the same ceiling is
|
||
re-usable across a session. A resolution naming more than its hold reserved
|
||
**MUST** debit what settled and **MUST** deny `funds-exceeded`: the money has
|
||
already moved on the rail, so capping it silently would absorb the breach, and
|
||
the sealed receipt is the evidence that a settlement exceeded its authorization.
|
||
A resolution denominated in a currency the grant is not **MUST** be refused
|
||
`currency-mismatch` **and leave the hold outstanding** — a malformed resolution
|
||
must not be the one move that loses the bound — and a grant with no money ceiling
|
||
reserves no money, the dimension being absent rather than zero. A hold that
|
||
reserves no money omits `held_minor`, so a joules-only hold's receipt is
|
||
byte-identical to one sealed before this paragraph existed. Conformance class
|
||
**C31**.
|
||
|
||
**A cancellation is a decision, not an absence (REQUIRED where work can be
|
||
abandoned).** A hold whose work will never complete — cancelled, abandoned,
|
||
timed out, lost with its caller — is otherwise unresolvable: by the paragraph
|
||
above the point names it unresolved and its grants stay debited `held_uj` while
|
||
nothing settles it, so the grant refuses every later call for good, and the
|
||
joules the abandoned work really burned are never recorded as spent. Neither is
|
||
honest: the budget is stranded at an upper bound rather than at a measurement.
|
||
|
||
A **cancellation** is the resolution for that case, and it settles the hold
|
||
rather than releasing it. It seals a receipt as every other decision does,
|
||
distinguished by its `frame_kind`, so an auditor tells an abandoned run from a
|
||
completed one without recomputing a cost; this needs no new verdict and no
|
||
profile bump. Its `cost_uj` is what the metering ladder yields for the work that
|
||
did run (§5.1), and since the ladder always yields a figure (C9), "it could not
|
||
be measured" is never a licence to charge zero. A cancellation is **not a
|
||
refund**: energy already turned to heat cannot be returned to a budget, and a
|
||
cancellation that released its hold without debiting would make cancelling a way
|
||
to spend for nothing — an agent that cancels every action just before it
|
||
completes would then spend without limit, since its recorded spend would never
|
||
grow. Through a delegation chain a cancellation debits **every** link, as a
|
||
settle does (§4.2's conservation); otherwise a delegate could spend its
|
||
ancestors' joules by always cancelling.
|
||
|
||
A cancellation **MUST** be signed, as every act in this protocol is — there are
|
||
no anonymous actors (§2). The grant's `subject`, whose work it is, may cancel it,
|
||
and so may any `issuer` along its chain, which can already revoke the grant
|
||
outright (§4.3) and therefore holds the strictly greater power. Any other caller
|
||
**MUST** be refused `wrong-subject`, **and the hold MUST stay outstanding**: a
|
||
refused cancellation that consumed the hold would let a stranger strand a budget
|
||
it could not itself spend. Because a hold resolves once, a completion that
|
||
crosses a cancellation is refused `already-resolved` and debits nothing, which is
|
||
what lets a point report one outcome for work whose end raced its abandonment.
|
||
|
||
**A firing: the record of a standing grant's mint.** A firing of a standing grant
|
||
(§4.6) runs no work; it mints a child. Its receipt is an allow on the standing
|
||
grant with action `mint`, `cost_uj` 0, the mint policy's content id as `resource`
|
||
and the trigger event's payload id as `payload_hash`, stating `minted_uj`, the
|
||
budget the child carries. Its `budget_remaining_uj` is what the mint ledger leaves
|
||
of the standing grant's budget after the firing. A stream reads firings so:
|
||
|
||
- A firing, an allow stating `minted_uj`, debits nothing and accrues nothing. The
|
||
work its child does is debited on the child's own receipts, whose `chain_ids`
|
||
name the standing grant.
|
||
- Each firing adds one firing and its `minted_uj` to the mint ledger of the
|
||
standing grant it names. The ledger a stream rebuilds for a standing grant is
|
||
the number of firings naming it and the sum of their `minted_uj`.
|
||
|
||
A refusal mints nothing (C3), so `minted_uj` on a refusal fires nothing. A firing
|
||
is recorded before its child leaves the enforcement point, so a stop between the
|
||
two leaves counted a firing whose child nobody received: the rebuilt ledger can
|
||
count a firing that was never handed out, and never misses one that was.
|
||
|
||
**Receipts sealed before `debited_uj`.** They keep verifying: a verifier that
|
||
recomputes a core MUST recompute it without these members for an envelope that
|
||
omits them. What they can no longer prove is exact spend. A denial naming
|
||
`caveat-unsatisfied` or `window-exceeded` with no `debited_uj` cannot show that it
|
||
debited nothing, and a reader MUST NOT read it as zero: it reads as having debited
|
||
its `cost_uj` from every grant it names, a figure no link's debit can exceed. An
|
||
allow with no member reads as having debited `cost_uj`, which overstates one that
|
||
was capped.
|
||
A replay of such a log therefore restores at least what was debited, and can
|
||
restore more, past a ceiling: it can leave a grant with less budget than it has,
|
||
never with more. On a complete single-grant stream `budget_remaining_uj` can tell
|
||
the two apart, since it stands before the debit on a refusal and after it on a
|
||
breach; on a chained receipt it cannot, since the core holds one remainder for
|
||
every link.
|
||
|
||
**Receipts sealed before `debited_minor` and `tainted_uj`.** They keep verifying: a
|
||
verifier recomputes the core without these three members for an envelope that omits
|
||
them, and without `debited_uj` and `chain_debited_uj` as well for one older still.
|
||
What they cannot prove is a money debit, or an accrual no flow block shows. A
|
||
priced allow sealed before `debited_minor` reads as having debited no money, though
|
||
it may have: the core holds no price, so there is no figure to read instead. A
|
||
tainted decision sealed without a flow block before `tainted_uj` reads as having
|
||
accrued nothing. An enforcement point that restores such a log MUST NOT take the
|
||
money spend or tainted accrual it restores as complete: it re-establishes them by
|
||
replaying its recorded decisions through the gate (§4.11), or holds those grants'
|
||
money ceilings and tainted sub-budgets as spent. A tainted call sealed with its flow
|
||
block reads as at least what it accrued, whatever its age: its accrual follows its
|
||
debit, which an old receipt reads as at least what was debited.
|
||
|
||
**Streams sealed before `held_uj` and `settles`.** Every receipt in them reads as it
|
||
did: neither member changes how a receipt that lacks it is read. What such a stream
|
||
cannot show is work interrupted between its decision and its receipt. An
|
||
enforcement point that sealed only after the work left nothing in the stream for
|
||
such work, so a replay restores less than was spent by that work's debit, and
|
||
nothing in the stream marks where it happened.
|
||
|
||
**Streams sealed before `minted_uj`.** No firing was sealed before the member
|
||
existed, so such a stream holds no record of a standing grant's firings, and a
|
||
replay rebuilds no mint ledger from it. What it cannot show is how many times a
|
||
standing grant fired. An enforcement point that restores such a stream MUST NOT
|
||
take the ledger it rebuilds as complete: it restores the ledger it stored beside
|
||
the rebuilt one, keeping the higher figures, or, holding none it can trust, holds
|
||
the standing grant as fired out.
|
||
|
||
**Receipts that carry the members, read by an earlier verifier.** The tag stays
|
||
`jcp/2`, so a verifier written before these members existed recomputes a core
|
||
without them and refuses a receipt that carries one, though its signature verifies
|
||
and its other members read as before. Only the receipts this section requires the
|
||
members on are affected: a capped debit, a denial naming `caveat-unsatisfied` or
|
||
`window-exceeded`, a decision that debited money, a tainted decision sealed
|
||
without a flow block, a hold, the receipt that settles one, and a firing. A replay
|
||
written before `held_uj` reads a hold as an allow of `cost_uj` 0, so a hold no
|
||
receipt settles restores nothing: what the stream would restore had no hold been
|
||
sealed, short of the spend by the interrupted work's debit. A replay written
|
||
before `minted_uj` reads a firing as an allow of `cost_uj` 0 on the standing grant,
|
||
which restores no spend, as a firing restores none, and rebuilds no mint ledger.
|
||
|
||
**What a receipt replay does not restore.** The core records what a decision
|
||
debited, in joules and in money, what it accrued against a tainted sub-budget, and
|
||
each firing of a standing grant.
|
||
It does not record the times a window needs (§4.12). An enforcement point that uses
|
||
windows re-establishes them by replaying its recorded decisions through the gate
|
||
(§4.11), which a receipt stream alone does not do.
|
||
|
||
### 8.2 Group accounting (RECOMMENDED profile)
|
||
|
||
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. The joules a receipt attributes are what it debited from the
|
||
grant it names, read as §8.1 reads them, so a denial from the debit point counts
|
||
its debit and a refusal counts none. A firing of a standing grant (§4.6) is not an
|
||
action and is not counted: the work it lets run is counted on its child's receipts.
|
||
Nor is a hold (§8.1): the work it lets run is one action, counted once, on the
|
||
receipt that settles it. A hold that no receipt settles is not counted either, as
|
||
an action or in joules: whether its work ran, and what it debited, the stream
|
||
cannot tell (§4.11). Here attribution and replay part. A replay restores such a
|
||
hold's `held_uj`, the most its work can have debited, so that a restart cannot
|
||
hand budget back; attribution counts only what a receipt shows a worker debited.
|
||
On a stream holding a hold no receipt settles, the joules attributed to its grant
|
||
therefore fall short of the replayed spend by that hold's `held_uj`.
|
||
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; a chained
|
||
decision enforces an inherited caveat; and a chain whose child dropped one does
|
||
not verify.
|
||
- **C8 — wire binding:** a request round-trips the `jcp/1` framing canonically
|
||
and an enforcement endpoint decides + seals over a live socket; a request
|
||
presented again gets its first response unchanged and is not debited again,
|
||
and one stamped outside the window is refused `stale-request`.
|
||
- **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 from the verified
|
||
receipt stream exactly what was debited: a denial from the debit point keeps its
|
||
debit, a refusal restores nothing, and a capped debit restores the capped figure;
|
||
the money a priced allow debited and what a tainted call accrued are restored
|
||
beside the joules, so a spent money ceiling and a spent tainted sub-budget stay
|
||
spent; a hold no receipt settles restores its `held_uj`, so the held grant
|
||
refuses the next call, and a settled hold restores nothing beside its settle;
|
||
live, a hold counts against its grant from the step that allows its call, so a
|
||
call decided under it in the meantime, two-phase or one-shot, is decided against
|
||
the remainder less the hold, and once settled the hold counts only as its work's
|
||
debit;
|
||
per-worker attribution matches the original ledger; a tampered receipt contributes
|
||
nothing; and a receipt sealed before `debited_uj` verifies and restores its
|
||
`cost_uj`.
|
||
- **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, a denial's and a capped debit's
|
||
included, and keeps conserving. A hold through a chain names every link and
|
||
counts against each, live, so a sibling delegate's call through the held root is
|
||
refused; a stream ending at it restores its `held_uj` on every link, and with its
|
||
settle each link's own debit.
|
||
- **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;
|
||
- **C19 — injection resistance:** a tool-poisoning corpus presented through an untrusted frame cannot steer a decision; the trust tag is structural, so data cannot become control. (Implemented in the harness since before this entry: §9 listed every other clause but omitted this one.)
|
||
one share cannot.
|
||
- **C30 — resolution of a hold:** a hold resolves exactly once, so a second
|
||
settle is refused `already-resolved` and debits nothing; a resolution names
|
||
one hold, so resolving one of two outstanding on a grant leaves the other's
|
||
reservation standing in both dimensions; a cancellation
|
||
resolves a hold whose work will never complete, debiting the joules that ran
|
||
rather than refunding them; a completion crossing a cancellation loses;
|
||
a cancellation by neither subject nor issuer is refused `wrong-subject` and
|
||
leaves the hold outstanding; and an abandoned chained run debits every link.
|
||
- **C31 — a hold's money dimension:** a priced hold reserves a money bound
|
||
beside its joule bound and debits neither; the reservation counts against the
|
||
next priced decision, so a second call for money the first hold reserves is
|
||
refused `funds-exceeded` while nothing is yet spent; resolving below the
|
||
reservation debits what settled and releases the rest, so the same ceiling
|
||
admits a later charge the unreleased reservation would have blocked;
|
||
resolving above it debits what settled and denies `funds-exceeded`; a
|
||
resolution in another currency is refused `currency-mismatch` and leaves the
|
||
hold outstanding; a hold in another currency is refused and reserves nothing;
|
||
a grant with no money ceiling reserves no money and resolves for any amount;
|
||
and a hold sized by a stated per-call ceiling admits a second concurrent call
|
||
where a whole-remainder hold serializes.
|
||
- **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/`](conformance/v1/) (`cargo run -p
|
||
jcp-conformance --bin gen-vectors`). [`replay.json`](conformance/v1/replay.json) is a
|
||
hash-chained stream holding each way a receipt states its debit, its money and its
|
||
tainted accrual, holds settled and not, and two firings of a standing grant with a
|
||
call by their child between them, with what it restores as the runtime recorded it
|
||
(for a hold nothing settles, the most its work could debit) and the mint ledger the
|
||
firings left, and
|
||
[`verify_replay.py`](conformance/verify_replay.py) is a second reader of it that
|
||
shares no code with the reference implementation.
|
||
|
||
## 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 |
|
||
| Abandoned work: a hold nothing settles | §8.1 a cancellation debits what ran; a hold resolves once |
|
||
| Mutable / optional audit | §8 signed JCR-1 receipts |
|
||
| Fragmentation across protocols | §6 one frame for tools and agents |
|
||
| Results fetched later, outside the gate (task handles, interim input requests) | §6.1 continuations, chained to the receipt they continue |
|
||
|
||
## 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), challenge?: bytes(32) }
|
||
WireResponse { decision, receipt: bytes, evidence?: { kind: text, report: bytes } }
|
||
```
|
||
|
||
`chain` is the full delegation chain, root first (a single un-delegated grant is
|
||
a one-link chain). The response carries the deterministic decision and the
|
||
sealed JCR-1 receipt, deny and allow alike.
|
||
|
||
**Time is the enforcement point's.** The endpoint MUST decide at its own clock.
|
||
`now` is the time the caller signed the request, a freshness stamp: the endpoint
|
||
MUST deny a request whose `now` differs from its clock by more than its
|
||
freshness window as `stale-request`, and MUST NOT decide any request as of the
|
||
time it names. A caller that chose the decision time could stamp a request
|
||
before its grant expired, or inside a time caveat it has since left.
|
||
|
||
**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.
|
||
|
||
**A request is decided once.** The wire never carries a secret: grants are
|
||
signed public objects, and a request is a signed public object too. The endpoint
|
||
MUST remember every request it decided while that request's stamp is inside the
|
||
window, together with its response. A request presented again MUST get that same
|
||
response, the same decision and the same receipt, and MUST NOT be decided,
|
||
debited or recorded a second time. That holds across a restart, which replays
|
||
the record (below) and so recovers each response: the decision is deterministic
|
||
and so is the seal. A captured request is therefore worth nothing beyond what
|
||
its first exchange carried. A caller whose response was lost recovers it by
|
||
sending the same request again. Refusing the repeat instead would leave that
|
||
caller only one way to learn the outcome: stamp the request again, which is a
|
||
new request and decides and debits the same action a second time. A request
|
||
refused before it was decided (stale, unauthenticated, or `unrecorded`) is not
|
||
remembered. Once a request's stamp has left the window, a repeat of it is stale.
|
||
Two byte-identical requests are one request, so a caller that means the same
|
||
action twice stamps the second later.
|
||
|
||
**The cost is the holder's measurement.** `cost` is the requester's own figure,
|
||
and the endpoint debits it as given, so a budget binds only as honestly as that
|
||
figure was measured. The requester MUST therefore be the party that metered the
|
||
action: the grant's holder is the enforcement point that ran it, as a steward is
|
||
in Mesh, rather than the agent the grant governs reporting its own consumption.
|
||
The endpoint behind the enforcement point is invoked only on Allow.
|
||
|
||
**Endpoint evidence (OPTIONAL).** A caller that wants evidence of the platform
|
||
the endpoint runs on puts a `challenge` in its request: 32 fresh bytes it chose.
|
||
The challenge is covered by `signature` like every other field, and is omitted
|
||
when absent, so a request without one has the bytes it always had. An endpoint
|
||
that can attest answers such a request with `evidence`: a report of `kind`, for
|
||
example a TEE attestation report, whose bound data is
|
||
|
||
```text
|
||
report_data = BLAKE3-XOF-64(derive_key = "jcp/1 wire evidence v1",
|
||
challenge[32] ‖ BLAKE3(receipt))
|
||
```
|
||
|
||
- **What it binds.** The challenge makes the evidence the caller's, made for its
|
||
request rather than replayed from another. The receipt makes it this answer's.
|
||
- **Checking it.** A caller checks the evidence against the challenge **it sent**,
|
||
never one read back from the response, and against the receipt it received,
|
||
with a verifier for `kind`.
|
||
- **When there is none.** An endpoint that cannot attest omits `evidence`, and a
|
||
caller that requires it treats the response as unattested.
|
||
- **Unauthenticated requests.** An endpoint MUST NOT give evidence for a request
|
||
denied `bad-signature`, which would let anyone have its platform produce
|
||
reports on demand.
|
||
- **Repeats and restarts.** A repeat of a challenged request gets the evidence it
|
||
got first, like the rest of its answer, and an endpoint that restarts rebuilds
|
||
it with the response.
|
||
- **Transport profiles.** A transport that carries platform evidence of its own,
|
||
such as an exported authenticator on a TLS session, is a profile of this slot:
|
||
the same challenge, the same binding.
|
||
|
||
**What the endpoint holds survives it.** An endpoint that can restart is bound
|
||
by §4.11: it MUST record each request it admits before deciding it, and each
|
||
revocation it honours before applying it, and on restart MUST replay them
|
||
through the gate, each request at the time it was decided, before deciding
|
||
anything new. That re-establishes the spend, the revocations, and the memory of
|
||
requests still inside the window. A request it cannot record is denied
|
||
`unrecorded`.
|
||
|
||
## 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. Each firing is sealed as a receipt stating `minted_uj` and a refused one is not; a restart rebuilds the ledger from the receipt stream, and a stale stored ledger restored beside it cannot lower it, so the policy stays rate-limited.
|
||
- **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, and only up to what the grant has **left** at that checkpoint; an agent steering itself, a non-issuer amending, a widening amendment, an amendment above the remainder of a checkpoint that stated its spend, 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; `ModelIn` admits only a frame naming one of its models, and denies a frame naming another or none; 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.
|
||
- **C25 — flow:** a value's propagated label must sit at or below the grant's ceiling and clear the resource's taint floor; an argument whose provenance was **not computed** fails closed against a ceilinged grant rather than passing as clean; several ceilings in a chain compose to the strictest and attenuation never widens one; a flow denial debits nothing, so a tainted call costs zero joules; and a grant with no flow caveat encodes byte-identically to one minted before flow 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. A priced chained decision applies the parent's money ceiling to its child's spend, and a delegated grant decided without its chain is denied `chain-broken`.
|
||
- **C26 — one writer per record:** a durable decision record admits one writer. A second holder of the same record is refused rather than opening it, because two would each replay it, each re-establish the same remaining budget and each debit against it — spending it twice while every entry still verifies (§5). The exclusive claim is released when the holder goes away, so a restart can reopen the record it previously held, and a platform that cannot provide exclusivity fails the open rather than proceeding.
|
||
- **C27 — windows:** a `WithinWindow` caveat bounds a grant's use over any window of its length (§4.12). A call window admits its limit and refuses the next, `window-exceeded`, without debiting it, and admits again once the earliest call has slid out. A distinct-payload window admits a payload already inside the window and refuses a new one beyond the limit. A joule window refuses a one-shot call whose cost would overfill it, and in the two-phase path binds at the debit, where an overrun is denied and stays debited. A parent's window counts its delegates' calls. A window survives a restart replayed through the gate. A runtime restored from a timeless ledger counts every window as full until its history is complete. A zero-second window is refused.
|
||
- **C28 — endpoint evidence:** a request's `challenge` is covered by the caller's signature, so swapping or stripping it is denied `bad-signature`, and a request without one keeps its bytes. An endpoint that can attest answers a challenged request with evidence whose bound data is the challenge and the receipt (§11). The evidence verifies against the caller's own challenge and is refused for another challenge or another answer. An unauthenticated request gets none, and a repeat gets the evidence it got first.
|