JCR-1 — the **JouleClaw Receipt Specification** — is the propagation
wedge the 5-lens cross-talk audit named as the single highest-leverage
positioning move. A signed, per-resolution thermodynamic receipt
third-party-verifiable with the spec, the public key, and standard
CBOR + COSE tooling. No JouleClaw dependency required to *check* a
receipt; this crate is needed only to *mint* one.
This commit lands the complete propagation package — spec, suite,
producer, two independent verifiers — as a single workspace crate.
## What's in the crate
```
crates/jouleclaw-jcr1/
├── README.md
├── run.sh ← Rust mints, Python + Go each verify
├── docs/
│ ├── JCR-1-spec.md ← the receipt standard (CC-BY-4.0)
│ └── JCR-1-v1.1-intent.md ← additive proposal: intent in signed core
├── conformance/
│ ├── README.md
│ ├── vectors.json ← 10 signed canonical vectors
│ ├── test-keys.json ← published TEST keys
│ ├── verify.py ← reference verifier (cbor2 + cryptography)
│ └── verify.go ← independent Go verifier (stdlib only)
├── examples/
│ └── jcr1_mint.rs ← Rust producer: read energy → sign → emit
└── src/lib.rs ← det-CBOR encoder, COSE_Sign1, HMAC, projection
```
## Surface (Rust)
- `canonical_cbor(&Value)` — RFC 8949 §4.2.1 length-first-then-bytewise
key ordering, no floats/bools/nulls. Matches the Python
`cbor2.dumps(canonical=True)` and the Go from-scratch encoder
byte-for-byte across all 10 conformance vectors.
- `to_jcr1_wire(&Receipt) -> Value` — projects a Rust `Receipt` to
the JCR-1 wire form: `CascadeTier::L0Cache` becomes wire tag `"L0"`,
`Provenance` renders PascalCase, wave-8 fields `model_id` + `intent`
are intentionally dropped (deferred to JCR-1 v1.1 — see
`docs/JCR-1-v1.1-intent.md`).
- `signed_core_bytes(&Value)` — drops `token_attributions` + null
fields per JCR-1 §3.1, then canonically encodes.
- `canonical_signed_core(&Receipt)` — convenience wrapping both.
- `cose_sign1_envelope` / `verify_cose_sign1` — COSE_Sign1 (RFC 9052)
envelope with EdDSA / Ed25519 only. Build the protected header,
Sig_structure, signature; verify by parsing the tagged CBOR,
re-building Sig_structure, checking kid == expected_steward, and
Ed25519-verifying.
- `hmac_tool_hex` — gateway tool-receipt HMAC per JCR-1 §5
(NUL-delimited canonical payload, HMAC-SHA-256, lowercase hex).
HMAC implemented inline (30 LOC) since `hmac = 0.12` doesn't
satisfy the workspace's `sha2 = 0.11` CoreProxy bound.
- `check_honesty(&Value)` — JCR-1 §6 step 5: worst-counter rule,
joule sum vs tools_touched, mJ resolution floor for non-HwShunt.
- `VECTORS_JSON` / `TEST_KEYS_JSON` — the 10 vectors and test keys
embedded via `include_str!` so `cargo test` re-verifies the suite
on every workspace run.
## The decisive test: byte-equal re-encode across three encoders
`vector_signed_cores_re_encode_byte_identical` re-encodes each of
the 10 vectors' receipts via the Rust canonical encoder and asserts
the bytes equal the suite's `signed_core_cbor_hex` — which was
produced by Python's `cbor2.dumps(canonical=True)` and independently
re-produced by the Go verifier's hand-written encoder.
Three encoders, three languages, byte-for-byte identical output on
all 10 vectors — that is what makes the spec the source of truth.
## V5 ↔ V10 the promotion-collapse demo
`vector_5_and_10_share_input_hash_promotion_collapse` asserts the
two vectors share the same `input_hash` and that V5 (L3, 2,500,000
µJ) → V10 (L1, 9 µJ) is a ≥100,000× collapse. The "yang→yin"
turnover claim made auditable in a single diff.
## Tamper detection
`tampering_with_joules_uj_is_caught_by_re_encode` proves an
understated-joules forgery produces different canonical bytes —
matching the negative-test surface documented in the Python and Go
README. The 4 failure modes (re-encode mismatch, payload mismatch,
HMAC mismatch, Ed25519 mismatch) all surface different errors.
## Quickstart
`./crates/jouleclaw-jcr1/run.sh` runs the full demo: Rust mints,
Python verifier accepts the 10 canonical vectors, Go verifier
accepts the same 10 vectors, then BOTH verifiers accept the
freshly Rust-minted receipt.
## Honest scope (v0.1)
- JCR-1 v1 wire form only. Wave-8 `model_id` + `intent` are NOT in
the signed core; the additive v1.1 proposal in
`docs/JCR-1-v1.1-intent.md` describes the path.
- Ed25519 (`alg = -8`) only. ES256 (`alg = -7`) permitted by the
spec but not implemented here — `verify_cose_sign1` rejects other
algs with `UnsupportedAlg`.
- Key transparency / rotation is out of scope; KERI AID rotation is
recommended by §4 but resolution is the consumer's job.
- The 10 vectors are TOY joule values exercising every spec path.
Real evidence requires RAPL/NVML/IOReport cross-validated against
Zeus — see strategy-and-landscape.md from the source drop.
## Lineage
This commit ingests a May 31 conversation drop at
`joule-claw-web/notes-05312026/` into the open-standards workspace,
restructured per the 4-move plan: (1) adopt the 10 vectors as
fixtures, (2) decide on `steward` + adopt det-CBOR, (3) propose
v1.1 with `intent`, (4) move the producer + verifiers into the
workspace. All four closed in two commits (b39fe21 + this one).
Tests: 25 lib + 1 example smoke + 10 Python + 10 Go = the full suite
across three languages with byte-identical canonical CBOR.
Workspace regression: 2,427 lib tests pass, 0 failures.
206 lines
7.7 KiB
Markdown
206 lines
7.7 KiB
Markdown
# JCR-1 v1.1 — Proposal: query-intent in the signed core
|
||
|
||
**Status:** Proposal (draft).
|
||
**Target:** Additive bump of [JCR-1](./JCR-1-spec.md), `jc_receipt: "1"` →
|
||
optional `intent` field MAY appear in the signed core. v1.0 verifiers
|
||
ignore it (per §8 additive evolution).
|
||
|
||
---
|
||
|
||
## 1. Why
|
||
|
||
Priscilla (Sentry) ran 116 of her own Claude sessions through a
|
||
post-hoc classifier and found **67% comprehension / 2% generation** —
|
||
the rest split across modification / process / review / other. The
|
||
finding generalises: *every workload that uses an LLM-shaped runtime
|
||
distributes across the same six categories, and the comprehension
|
||
slice is consistently dominant.*
|
||
|
||
JouleClaw already classifies queries at runtime
|
||
(`jouleclaw-intent::HeuristicClassifier` + the `LlmFallbackClassifier`
|
||
seam) and tags `Receipt.intent` on the way to the ledger
|
||
(`jouleclaw-ledger::CostSummary::by_intent`). What's missing for a
|
||
deployment to reproduce Priscilla's analysis on its OWN signed
|
||
receipts is: **the intent verdict needs to sit inside the signed core
|
||
so it survives across third-party audit.** An external grader can
|
||
then run
|
||
|
||
> *"prove to me that ≥60% of your `Receipt` records carried
|
||
> intent=`comprehension` and a tier in {L0, L0.x, L1, L2}, signed by
|
||
> the steward I trust."*
|
||
|
||
without trusting the runtime's separate dashboard.
|
||
|
||
This proposal pins how the intent verdict is canonicalised so the
|
||
audit is mechanical, not interpretive.
|
||
|
||
---
|
||
|
||
## 2. What's added
|
||
|
||
One optional top-level field on the JCR-1 receipt:
|
||
|
||
```
|
||
intent: { intent: enum, classifier_id: string } // OPTIONAL
|
||
```
|
||
|
||
### 2.1 `intent.intent` — taxonomy (frozen for v1.x)
|
||
|
||
A stable string enum mirroring `jouleclaw_intent::QueryIntent`:
|
||
|
||
| Wire form | Meaning | Verifiable-zone? |
|
||
|-------------------|---------------------------------------------------|------------------|
|
||
| `comprehension` | Reading / understanding existing code or state | yes |
|
||
| `generation` | Producing new artefacts | no |
|
||
| `modification` | Editing existing artefacts in place | partial |
|
||
| `process` | Operational / workflow tasks | partial |
|
||
| `review` | Reviewing a candidate artefact | yes |
|
||
| `other` | Unclear / refused | n/a |
|
||
|
||
Names are `snake_case` text strings — *not* small-int enums — so the
|
||
det-CBOR form stays human-inspectable and matches the Rust
|
||
`#[serde(rename_all = "snake_case")]` already shipping.
|
||
|
||
### 2.2 `intent.classifier_id` — provenance for drift
|
||
|
||
A stable string identifying the classifier that produced the verdict
|
||
(`"heuristic:v1"`, `"llm:claude-haiku-4-5"`, `"override:operator"`,
|
||
…). Required when `intent` is present.
|
||
|
||
A receipt grader that sees a dashboard shift from 67% → 50%
|
||
`comprehension` over a week needs to know whether the workload moved
|
||
or the classifier got changed. The `classifier_id` is that signal.
|
||
The grader correlates the shift against a `classifier_id` change in
|
||
the audit log; if it tracks, the workload didn't change.
|
||
|
||
### 2.3 What is NOT added
|
||
|
||
- **`confidence` is intentionally excluded.** `IntentVerdict.confidence`
|
||
is a `f64`; JCR-1 §3.1 already excludes floats from the signed core
|
||
because they're not reproducible across encoders, and confidence is
|
||
classifier-specific (no shared scale). Confidence stays on the
|
||
full `Receipt` JSON outside the signed core, alongside
|
||
`token_attributions`.
|
||
- **No new `intent_categories` map or sub-types.** The six-variant
|
||
taxonomy is the wire form. Subdivisions are caller-side concerns.
|
||
|
||
---
|
||
|
||
## 3. Canonical encoding
|
||
|
||
The `intent` field, when present, is a 2-entry CBOR map under the top-
|
||
level receipt map. Both subfields are required when `intent` is
|
||
present (omitting either is a structural violation).
|
||
|
||
Order under RFC 8949 §4.2.1 (length-first then bytewise) places the
|
||
keys as:
|
||
|
||
```
|
||
intent (6) → comes between "claims" (6) and "tier" (4)? No — length-first.
|
||
4-byte keys: tier
|
||
6-byte keys: claims, intent, ...
|
||
```
|
||
|
||
`"intent"` and `"claims"` both have length 6; bytewise: `c` (0x63) <
|
||
`i` (0x69), so `claims` comes before `intent`. The inner map
|
||
`{intent, classifier_id}` sorts to `intent` (6) before
|
||
`classifier_id` (13) by length.
|
||
|
||
Wire example (canonical CBOR, hex):
|
||
|
||
```
|
||
A2 # map(2)
|
||
66 69 6E 74 65 6E 74 # text(6) "intent"
|
||
6D 63 6F 6D 70 72 65 68 65 # text(13) "comprehension"
|
||
65 6E 73 69 6F 6E
|
||
6D 63 6C 61 73 73 69 66 69 # text(13) "classifier_id"
|
||
65 72 5F 69 64
|
||
6C 68 65 75 72 69 73 74 69 # text(12) "heuristic:v1"
|
||
63 3A 76 31
|
||
```
|
||
|
||
(Hand-wrapped above; an encoder MUST sort the inner map's keys
|
||
length-first → `intent` before `classifier_id`.)
|
||
|
||
---
|
||
|
||
## 4. Verification flow change
|
||
|
||
JCR-1 §6 gains one optional step between step 4 (tool HMACs) and step 5
|
||
(honesty consistency):
|
||
|
||
> **4.5 — Intent structural.** If `intent` is present, both
|
||
> `intent.intent` and `intent.classifier_id` MUST be present;
|
||
> `intent.intent` MUST be one of the six taxonomy values. Unknown
|
||
> intent values are rejected (the spec is `#[non_exhaustive]` at the
|
||
> Rust level but the wire enum is frozen for v1.x).
|
||
|
||
The drift bands of §7.1 are unchanged — intent doesn't enter joule
|
||
re-validation.
|
||
|
||
---
|
||
|
||
## 5. Backward-compatibility
|
||
|
||
- v1.0 verifiers: per §8 ("Unknown fields MUST be ignored, never
|
||
rejected"), v1.0 verifiers see the extra `intent` field and skip
|
||
it. The 10 canonical vectors continue to verify under v1.0 verifiers.
|
||
- v1.1 receipts produced without `intent` are valid v1.0 receipts.
|
||
- The canonical CBOR re-encode test continues to pass byte-for-byte —
|
||
`intent` just adds a new (key, value) pair to the map, with its
|
||
position determined by RFC 8949 §4.2.1.
|
||
|
||
This is an additive change. `jc_receipt` stays `"1"`.
|
||
|
||
---
|
||
|
||
## 6. Conformance vectors
|
||
|
||
A v1.1-ratifying conformance suite SHOULD add three vectors on top of
|
||
the existing ten:
|
||
|
||
11. **L0 cache hit + intent=comprehension** — verifies the canonical
|
||
map order with `intent` present and no `tools_touched`.
|
||
12. **L3 model + intent=generation + ModelBased** — verifies intent
|
||
coexists with `tools_touched` and ModelBased honesty floor.
|
||
13. **Promoted skill (collapses V5) + intent=comprehension** — the
|
||
Priscilla 67% case: comprehension intent now resolves at L1.
|
||
|
||
A v1.1 verifier that round-trips vectors 1–13 byte-identically is
|
||
**v1.1 vector-conformant**.
|
||
|
||
---
|
||
|
||
## 7. Why not add `model_id` too?
|
||
|
||
`model_id` (also a wave-8 `Receipt` field) is a candidate but raises
|
||
distinct questions JCR-1 v1.1 deliberately defers:
|
||
|
||
- Where does `model_id` resolve? KERI AID? OCI subject digest? A
|
||
registry the spec has to pin? A stable identifier scheme is a
|
||
wider call than adding the intent taxonomy.
|
||
- Per-receipt `model_id` overlaps with the wave-6 `jouleclaw-oci` +
|
||
`jouleclaw-slsa` provenance chain — those already pin the model
|
||
binary's identity via in-toto Statement subject + ORAS referrer.
|
||
Carrying it on the cascade receipt may duplicate that surface.
|
||
|
||
A separate v1.2 proposal (or a sidecar binding) would resolve
|
||
`model_id` once the wider model-identity question is settled.
|
||
|
||
---
|
||
|
||
## 8. Implementation in jouleclaw-jcr1
|
||
|
||
The Rust reference (this crate) implements v1.1 behind an opt-in
|
||
constructor. The current `to_jcr1_wire` projection drops `intent`
|
||
(v1.0 behaviour). When v1.1 ratifies:
|
||
|
||
```rust
|
||
// proposed v1.1 surface
|
||
pub fn to_jcr1_wire_v1_1(receipt: &Receipt) -> Result<Value, JcrError>;
|
||
pub fn canonical_signed_core_v1_1(receipt: &Receipt) -> Result<Vec<u8>, JcrError>;
|
||
```
|
||
|
||
Both paths share the canonical CBOR encoder; v1.1 just stops dropping
|
||
`intent` during the projection. Roughly five additional lines of code
|
||
plus three vectors.
|