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

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

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

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

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

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

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

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

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

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

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

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

19 KiB
Raw Permalink Blame History

JCP — The Joule Context Protocol

A unified, energy-native, capability-secured protocol for agentic systems. Where today's stack splits across MCP (tools), A2A/ACP (agent-to-agent), an identity layer, and an authorization layer — and standardizes energy nowhere — JCP is one spec, built on one idea:

A capability and an energy budget are the same object: a signed, bounded, debited grant. A capability you can't pay for is denied; an expenditure you're not capable of is denied. One check, made outside the model, sealed in a receipt.

The design answers the documented failure modes of the current agentic ecosystem, not hypotheticals. Tool descriptions are untrusted input → JCP refuses to take control from a tool's output. Prompts can't enforce access control (and a more capable model only follows injected instructions better) → JCP decides every action in the runtime, never in the LLM. Agent loops have no cost ceiling → every grant carries a joule budget that's debited and enforced before the side effect. Audit is a mutable telemetry span → JCP seals every decision, allow and deny, as a non-repudiable signed receipt.

  • Licence: Apache-2.0 for reference code (see LICENSE); CC-BY-4.0 for specification text (see LICENSE-SPEC).
  • Status: v0.3.0 draft. Specification core published (SPEC.md + conformance/v1/); reference workspace jcp-rs/.

What's here

jcp/
├── SPEC.md            — the normative standard (§1–§12)
├── conformance/v1/    — published vectors (a signed grant + allow/deny receipts)
└── jcp-rs/            — the Apache-2.0 reference implementation

The reference implementation (jcp-rs/)

  • jcp-schema — the wire objects: the did:key actor, the Grant (the metered capability — capability ⊗ joule budget, content-addressed and signed), the Frame (every message, with its control/data trust tag), the family Energy/Provenance model, canonical det-CBOR content addressing.
  • jcp-grant — issuance, signature verification, attenuation (object-capability delegation where a child grant can only narrow — capability ⊆, budget ≤, expiry no later, delegation depth − 1), chain verification (root-first, hop-by-hop: parent links, holder-issued hops, narrowing, signatures, depth bounds), revocation (issuer-only, signed, admitted into a RevocationSet only after authority checks), sharding (shard() splits a grant into siblings whose budgets must sum within the parent — distributed enforcement with no consensus, SPEC §4.5), and identity continuity (KeyLog: KERI-style pre-committed rotation, SPEC §2.1 — a thief with the current key can't rotate, an owner who lost it still can).
  • jcp-runtime — the deterministic enforcer. One ordered check fuses authorization and energy, decided outside any model: untrusted-control, revocation, configured trust roots (an enforcement point honors only the root-issuer keys it was configured with — authorized_keys shape, no CA), audience binding, signature, expiry, capability, and budget — then debit. The first failure names the reason. decide_chained enforces full delegation chains: a revoked ancestor kills the subtree, and the cost must fit — and is debited from — every link, so sibling delegations share their root's budget and delegation can never mint joules.
  • jcp-receipt — the jcp/1 JCR-1 receipt: canonical det-CBOR + COSE_Sign1/EdDSA, byte-compatible with the family verifiers. Allow and deny both seal. The receipt stream is the write-ahead log (SPEC §8.1): a chained receipt records chain_ids — every link the decision debited — so replay_spend rebuilds an enforcement point's budget state, ancestors included, from its own verified output with no external index; aggregate keys a team's receipts by (grant, subject) for per-worker cost attribution computed from evidence (SPEC §8.2).
  • jcp-bridge — a reference gateway that wraps existing MCP tool calls and A2A tasks in Grants and Frames without rewriting the endpoint. It maps each outbound call to a Frame (the tool/method is the action; the (server, tool) identity is the content-addressed resource), decides it against the caller's grant before the call reaches the endpoint, and seals a receipt — so an existing server keeps its transport while every call becomes capability-scoped, joule-budgeted, decided outside the model, and sealed. Inbound results are framed as untrusted data.
  • jcp-a2a — the A2A + AP2 profile: ingests an open A2A Agent Card (each advertised skill becomes a content-addressed JCP capability, bound to the a2a:<agent>:<skill> audience) and an AP2 payment mandate (its authorization, read in joules, becomes the grant's budget — a currency mandate yields no budget, because the profile invents no exchange rate), mints one capability-scoped joule-budgeted grant per skill, and gates every A2A task against them through the bridge. A message/send targeting a skill the card never advertised — or one past the AP2 budget — is denied before it reaches the peer, and the denial seals like an allow. The open agent-interop wire keeps its transport; JCP adds the capability, the energy budget, and the cryptographic audit it lacks.
  • jcp-policy — contextual service policies: the content-keyed, require-approval layer a static grant audience can't express. A Policy matches a call on its action, content (a secret marker, a sensitive path, a forbidden recipient), or resource and either denies it or requires approval — a human consent resolved by an Ed25519 signature over the call's context digest (which binds the content hash, never the content, so consent is to one exact call). The verdict composes with the grant decision — the policy is the second, content-aware gate after the capability allows — and seals as a hash-chained policy/1 receipt: the allow, the deny, and the held/approved consent in one audited ledger. The "service policy" shape — allow / deny / require-approval over tool+args+identity, decided outside the model — with the joule budget (on the grant) and the cryptographic audit the incumbent's table-row log lacks.
  • jcp-orchestrate — a cross-harness orchestration profile: a deterministic reconcile loop (poll → route → dispatch → reconcile → retry) that runs work items across a heterogeneous fleet of Harness backends, where every dispatch is capability-gated (a harness is granted only the task kinds it may handle), joule-budgeted (an exhausted harness fails over to the next one with capacity), and sealed as a JCR-1 decision receipt before it runs. A task whose kind no harness is granted is denied; a flaky harness retries up to a bound; the whole run is an ordered, verifiable ledger of sealed decisions. The compose-many-harnesses shape a meta-harness offers — with the capability, energy budget, and audit it leaves to the platform.
  • jcp-prompt — a signed, content-addressed prompt registry: immutable prompt versions (double-brace {{ variable }} templates) with mutable aliases (dev / staging / production) resolved by prompts:/<name>@<alias> (or prompts:/<name>/<version>) URIs — the registry shape the serving ecosystem already uses. The difference: each registered version and each alias promotion seals as a hash-chained prompt/1 receipt, and a version's id is its template, so swapping the text under a name changes the id and re-pointing an alias is a signed entry in an ordered log. The exact instructions an agent ran under — and which version was live when — become tamper-evident artifacts, not a row someone with write access can edit.
  • jcp-trace — ingests an agent run's OpenTelemetry GenAI trace (gen_ai.* semantic conventions: chat / tool / retriever spans with token usage) and its evaluation assessments (judge / human / code feedback), attaches per-span joules (measured where a meter is present, else Estimator from token counts — provenance never silently upgrades), and seals one eval/1 JCR-1 receipt: the per-span token + joule breakdown with the headline uj_per_accepted_token at the weakest provenance present, the assessments, a deterministic quality-gate verdict (the sealed, declarative analog of a CI assert), and a readiness projection an ARL validation-axis claim consumes. The observability stacks record a trace and log a judge score in a table; this is the interop projection that seals the same trace, puts a joule on it, and hash-chains it via EvalLog — no new cryptography. The gate is content-addressed, so the sealed receipt commits to exactly which bar it was judged against (a tamper-evident artifact, not a mutable in-code assert), and align_threshold / Judge calibrate a judge's decision threshold to human pass/fail labels — judge alignment as a reproducible number, not a UI click. Behind the server feature, a std-only OTLP /v1/traces receiver lets any OpenTelemetry exporter POST a gen_ai.* trace (with its assessments) and get the sealed eval/1 receipt back the instant the run ends.
  • jcp-intercept — attested interceptor chains: a composable, ordered pipeline of transforms around each model and tool call — the before_model / after_model / before_tool seams an agent's middleware stack runs, composed onion-order (forward in, reverse out) and stopping at the first refusal or human-approval hold. An interceptor may inspect or rewrite the request (redact a secret before it reaches the model, trim stale context), refuse it, or hold it for a human. The difference from the incumbent's arbitrary in-process hooks: every step seals an intercept/1 JCR-1 receipt carrying the request digest before and after the step (so a redaction or a trim is provable — the content itself never sealed, only its hash), the seam, the outcome, and the metered joules at their honest provenance, hash-chained — the exact transform pipeline that produced a model request becomes reconstructable and verifiable by anyone, and a step cannot run un-sealed. Three reference interceptors — Redact (a content transform), CallLimit (a guardrail), Approve (a human-in-the-loop hold) — each reuse a primitive the family already holds rather than reinventing it.
  • jcp-efficiency — the energy-efficiency spec, surfaced as a sealed receipt. The dark-factory economics turn on four figures — joules per accepted token, tokens per second, value per token, and the tokens/joules/dollars spent per COMPLETED, VERIFIED unit of work — and the incumbents publish, at best, a rate (cost per token) that can't tell you whether the spend bought a result. An efficiency/1 JCR-1 receipt records all four at honest provenance, hash-chained, with the one thing the rates omit: the outcome anchor (completed + a verdict). A measurement that didn't complete is spend that bought nothing, and the receipt says so. aggregate then splits value-delivered from total spend across a chain — charging every attempt (including the failed ones) against the results actually delivered — so tokens_per_completed_unit / uj_per_completed_unit / value_per_joule become the honest efficiency a "cost per token" number is designed to hide. Integers and strings only (JCR-1 forbids bools/floats); the rate figures recompute from the sealed inputs on read.
  • jcp-energy — the metering ladder (SPEC §5.1): RAPL package counters (Linux powercap, wraparound-corrected, HwShunt, only while every package counter is live — rapl_status()), NVML GPU energy behind the nvml feature (HwShunt and Partial, since it does not read the CPU's share; only while live — nvml_status()), the macOS kernel's per-process CPU energy count (proc_pid_rusage's ri_energy_nj; HwShunt, whole for in-process CPU work, only while live — process_energy_status(); it covers this process's threads, not children, the GPU or DRAM, so a span that reaps a child process or ends with one is Partial; it is live on macOS 27.0.1 where IOReport's CPU rails are not), Apple Silicon IOReport (HwShunt, only while every CPU energy rail, each die's on a multi-die part, is live — on macOS 27.0.1 that rail is republished in lumps, so there the rung is declined and ioreport_status() says why; Meter::detect_explained() names the rung selected and why each one above it was declined), a time×calibrated-power model (ModelBased, always available), and a constant per-op estimator (Estimator). A vendor counter's reading is real but vendor-opaque and relayed by the platform, and it counts as a reading only while the counter is live (SPEC §5). Measured when the platform exposes it, estimated when it doesn't — and the provenance always says which; a hardware backend that fails mid-reading degrades honestly to the model.
  • jcp-gateway — the energy-native MCP gateway: a JCP enforcement point that sits in front of an MCP server so an existing client/server pair keeps its transport while every tools/call becomes capability-scoped, joule-budgeted, and sealed. The novel mechanic is two-phase metered enforcement: the call is authorized before it runs (no debit, because the cost isn't yet known), the upstream call is metered by the sibling Sandbox standard's energy probe (Apple Silicon IOReport, RAPL, NVML — vendor energy counters, whose readings are real but vendor-opaque and relayed by the platform, and which count as readings only while the counter is live for the work, SPEC §5; otherwise the figure is an estimate and is graded as one), and the metered joules are debited from the grant — so the budget that stops a runaway agent is denominated in joules, not a token estimate. Every decision, allow and deny, seals into a hash-chained receipt log: excise, reorder, or splice a receipt and the chain breaks at that point. A denied call never reaches the server (the live 2025–26 MCP exploits land on tool output, which the gateway frames as untrusted data that can never exercise a capability), and the jcp-gateway binary runs it over stdio in front of any MCP-server subprocess. This is the chokepoint the AI-gateway field occupies, on the two axes nobody there holds: the unit of account is the joule, and the audit is cryptographic and tamper-evident rather than a dollar figure and a span.
  • jcp-wire — the jcp/1 wire binding (SPEC §11): 4-byte length prefix + canonical det-CBOR over any byte stream, with TCP and Unix-socket endpoints and a client. The decoder rejects non-canonical bodies, so a message has exactly one wire form. Requests carry the full delegation chain and are caller-signed — the endpoint authenticates from the request itself and denies an impersonation bad-signature, sealed like any other decision. Responses carry the deterministic decision and the sealed receipt.
  • jcp-threshold — threshold actors (SPEC §2.2): a did:key whose private key is split k-of-n with FROST (RFC 9591); the group key is an ordinary Ed25519 key, so a 2-of-3 team issues, holds, and signs wire requests with nothing else in JCP changing. Shares come from a trusted dealer or distributed key generation — with DKG the group's private key never exists anywhere, not even momentarily. The two-agent rule for irreversible actions, refused by arithmetic rather than policy.
  • jcp-channel — receipt-settled budget channels (SPEC §4.5, the OPTIONAL profile): N enforcement points spend optimistically against one grant and settle by exchanging signed statements of cumulative spend — forged, stale, and regressing statements refused; between settlements the exposure is bounded by the window, and after settlement any overdraft is provable from the signed statements. Where bounded overdraft is unacceptable, shard() remains the baseline.
  • jcp-conformance — C1–C18 + a negative pack, all checks real (C8 decides over a live socket; C13 drives a FROST 2-of-3 caller; C16 replays ancestor debits from a chained receipt; C17 settles a budget channel by signed statement; C18 mints a dealer-free group via DKG), published vectors in conformance/v1/.
  • jcp-cli — the jcp demo: one metered grant driven through an agent loop — legitimate calls allowed and debited, a poisoned tool-result refused structurally, out-of-scope actions and resources denied, the budget stopping a runaway loop, a sub-agent getting a strictly-weaker delegated grant, a signed receipt verified, the detected energy meter reporting its honest provenance, and the issuer revoking the grant — after which the runtime goes dark.

cargo test --workspace green; cargo clippy --workspace --all-targets warning-free.

The demo, abbreviated

frame                                  verdict  reason                rem µJ
read the db (legit)                    allow    −20000 µJ              80000
tool-result says 'delete db' (poison)  DENY     untrusted-control      80000
try to delete (not granted)            DENY     action-not-granted     80000
read prod-secrets (wrong audience)     DENY     wrong-resource         80000
read again — budget runs out           DENY     budget-exceeded        30000

  amplification refused: capability exceeds parent
  receipt  allow · 50000 µJ · verified true

The model never gates itself; the runtime does, and the receipt proves it.

Consumers in this repository

JCP is not only specified — it is consumed. Three sibling standards gate their real dispatch through it, each with its own adapter crate and tests:

  • JouleClaw (jouleclaw-rs/crates/jouleclaw-jcp) — every MCP tool dispatch is decided against a Grant before a byte reaches the tool; an ungranted tool named by an injected instruction is refused and the refusal sealed.
  • Sandbox (sandbox-rs/crates/sandbox-jcp) — a unit's execution is authorized against a Grant bound to its image by content address; one flipped image byte is a different audience and is denied before the executor is ever invoked. Two receipts per run: the JCP authorization and the sandbox's metered, attested execution.
  • Joule Code (joule-code-rs/crates/joule-code-jcp) — every cascade-stage consultation is decided pre-dispatch; the L4 escape hatch to a remote frontier agent exists only when a signed grant names it and can pay for it.

Relationship to the family

JCP composes the primitives the Transaction Science family already proved: did:key/Ed25519 identity, capability-gated execution (sandbox), the honest Energy/Provenance model (proof), and JCR-1 signed receipts (family-wide). It is where they stop being per-tool adapters and become the protocol.