mesh: a grant is decided by JCP's runtime, so expiry, caveats, the model and spend bind #294

Merged
dcharlot merged 2 commits from fix/mesh-grant-decided-by-jcp into main 2026-10-17 17:07:15 -04:00
10 changed files with 497 additions and 43 deletions

View file

@ -537,6 +537,7 @@ version = "0.1.0"
dependencies = [
"ed25519-dalek",
"jcp-grant",
"jcp-runtime",
"jcp-schema",
"mesh-engine",
"mesh-meter",

View file

@ -20,17 +20,20 @@ inflate: the joule.
## The shape
```
ServeRequest ──▶ authorise (JCP grant) ──▶ route (cheapest-sufficient)
│
▼
meter ◀── serve (engine) ──▶ tokens
│
▼
ServeRequest ──▶ route (cheapest-sufficient) ──▶ authorise (JCP grant)
│
▼
meter + debit ◀── serve (engine) ──▶ tokens
│
▼
sealed JCR-1 receipt: µJ / accepted token
```
Four roles — **caller**, **router**, **engine**, **steward** — and four phases
per serve: authorise, route, meter, seal. See [`SPEC.md`](SPEC.md) §2.
per serve: route, authorise, meter, seal. The grant is decided by JCP's own
runtime before any work runs, so its expiry, caveats and remaining budget bind
here as at any JCP enforcement point, and the metered joules are debited
against it afterwards. See [`SPEC.md`](SPEC.md) §2.
## The reference workspace
@ -44,7 +47,7 @@ per serve: authorise, route, meter, seal. See [`SPEC.md`](SPEC.md) §2.
| `mesh-engine-gguf` | a **real** backend (behind `gguf`): wraps the family's pure-Rust GGUF/Metal inference substrate so the mesh routes and meters actual model tokens |
| `mesh-meter` | the meter window; an always-available estimator + an optional bridge to the family hardware meter (RAPL / NVML / Apple) behind `hw-meter`, and `ProcessMeter`: this process's own CPU and GPU energy for an engine that runs in process, measured when a span covers the whole request |
| `mesh-receipt` | the `mesh/1` receipt, JCR-1 sealing (det-CBOR + COSE_Sign1), and a hash-chained log |
| `mesh-core` | the orchestration that ties authorise → route → meter → seal together |
| `mesh-core` | the orchestration that ties route → authorise → meter → seal together |
| `mesh-conformance` | the deterministic reference scenario, its checks, and the `gen-vectors` bin |
Mesh **rides on** [JCP](../jcp/) (a serve request is authorised by a JCP grant;

View file

@ -46,16 +46,31 @@ A Mesh deployment has four roles:
One serve proceeds in four phases, in order:
1. **Authorise.** If the request rides on a JCP `SignedGrant`, the steward MUST
verify the issuer signature, confirm the capability permits the action
`infer` on the request's content id, and confirm the grant's joule budget is
≥ the request's joule ceiling. A failure at any step aborts the serve before
any work is done.
2. **Route.** The router selects the cheapest admissible backend (§4). If none is
admissible, the serve fails with the router's recorded reason.
1. **Route.** The router selects the cheapest admissible backend (§4). If none is
admissible, the serve fails with the router's recorded reason. When the
request rides on a JCP `SignedGrant` carrying a `ModelIn` caveat (JCP §4.9),
a backend whose model the grant does not permit is inadmissible, and so is a
backend that names no model.
2. **Authorise.** If the request rides on a grant, the steward is a JCP
enforcement point and MUST decide the grant in JCP's two-phase metered path
(JCP §7, §4.9) before any work is done. The frame it decides over names the
action `infer` on the request's content id, the request's `prompt_hash` as
payload, and, as `model`, the content address of the weights the chosen
backend runs, when the backend names one. Every check of that decision
applies: signature, expiry, revocation, and every cost-independent caveat.
The steward MUST also refuse the serve when what the grant has left (its
budget less what earlier serves on it were debited) is below the request's
joule ceiling. A failure aborts the serve before any work is done.
3. **Meter.** The steward opens an energy window, calls the engine, and closes
the window. Energy MUST be metered **around** the engine; an engine MUST NOT
self-report energy.
self-report energy. With a grant, the steward then debits the metered joules
against it (JCP's second phase), where a cost-dependent caveat such as
`MaxCostUj` binds. The work has run, so the joules are debited even when the
debit breaches a caveat, and the breach is recorded on the receipt
(`grant_breach`, §6.1). An engine that names its weights before serving MUST
report the same weights after; the steward MUST NOT seal a serve whose
engine reports others, since they are not the weights the grant was decided
over.
4. **Seal.** The steward builds the receipt (§6), stamps it into its hash-chained
log, and seals it as a JCR-1 envelope.
@ -333,6 +348,7 @@ and nulls; booleans are encoded as `0|1` uints and absent options dropped.
| `uj_per_decision` | **the wedge metric** for a decision — `energy.uj() / questions_answered` |
| `uj_per_option_scored` | the finer decision figure — `energy.uj() / options_scored` |
| `energy_provenance` | `HwShunt | ModelBased | Estimator` |
| `grant_breach` | the JCP denial label when debiting the metered joules breached the grant, e.g. `caveat-unsatisfied` for a per-call cost caveat (OPTIONAL — §2; absent means the debit was within the grant, or the serve rode on none) |
| `sequence`, `prev` | hash-chain position and prior envelope's content id |
| `nonce`, `steward` | per-receipt nonce and the sealing identity (COSE `kid`) |
@ -356,8 +372,11 @@ any envelope breaks verification of that receipt and the chain link from the nex
A conforming implementation MUST:
1. Reject a serve whose grant fails verification, does not permit `infer` on the
request, or carries a budget below the request's joule ceiling (§2.1).
1. Decide a serve's grant as a JCP enforcement point before any work (§2): reject
one that fails verification, has expired or been revoked, does not permit
`infer` on the request, carries a cost-independent caveat that does not hold,
or has less left than the request's joule ceiling; debit the metered joules
against it after the work, recording a breach on the receipt.
2. Choose the cheapest admissible backend and record every candidate weighed
(§4).
3. Meter energy around the engine and tag provenance honestly, enforcing the
@ -383,9 +402,16 @@ Mesh composes proven primitives: Ed25519 signatures, BLAKE3 content addressing,
RFC 9052 COSE_Sign1, and the JCP capability model. It introduces no new
cryptography. Two properties are correctness, not style:
- **Authorisation happens outside the model.** The grant check (§2.1) is enforced
- **Authorisation happens outside the model.** The grant check (§2) is enforced
by the steward before any inference runs; a prompt cannot grant itself budget
or capability.
- **The caller is not authenticated.** Mesh honours a grant as a bearer token
of its subject: whoever presents it is decided as the subject. A grant that
must not be usable by anyone who obtains it needs a binding Mesh can check
without an identity, such as `PayloadIs` or a short `NotAfter`.
- **A grant's spend is per steward.** The debit ledger is the steward's own, in
memory. A grant presented to several stewards, or to one that restarts, is
spent separately at each (JCP §4.5, §4.11).
- **Honesty is enforced at seal time.** A receipt cannot claim a measurement it
does not have (§3.3). An operator without a hardware meter produces honest
`Estimator` receipts.

View file

@ -432,6 +432,15 @@ dependencies = [
"thiserror 2.0.20",
]
[[package]]
name = "jcp-runtime"
version = "0.1.0"
dependencies = [
"jcp-grant",
"jcp-schema",
"serde",
]
[[package]]
name = "jcp-schema"
version = "0.1.0"
@ -560,6 +569,7 @@ dependencies = [
"ed25519-dalek",
"hex",
"jcp-grant",
"jcp-runtime",
"jcp-schema",
"jouleclaw-backend-apple",
"mesh-engine",

View file

@ -31,6 +31,7 @@ mesh-core = { path = "crates/mesh-core" }
# Family standards Mesh rides on (cross-workspace path deps).
jcp-schema = { path = "../../jcp/jcp-rs/crates/jcp-schema" }
jcp-grant = { path = "../../jcp/jcp-rs/crates/jcp-grant" }
jcp-runtime = { path = "../../jcp/jcp-rs/crates/jcp-runtime" }
# Optional: the family's honest energy meter (RAPL / NVML / Apple), behind `hw-meter`.
jouleclaw-energy = { path = "../../jouleclaw/jouleclaw-rs/crates/jouleclaw-energy" }
jouleclaw-backend-apple = { path = "../../jouleclaw/jouleclaw-rs/crates/jouleclaw-backend-apple" }

View file

@ -15,6 +15,7 @@ mesh-engine = { workspace = true }
mesh-router = { workspace = true }
jcp-schema = { workspace = true }
jcp-grant = { workspace = true }
jcp-runtime = { workspace = true }
ed25519-dalek = { workspace = true }
thiserror = { workspace = true }

View file

@ -2,12 +2,17 @@
//!
//! [`Mesh`] ties the four moving parts together for one metered inference:
//!
//! 1. **Authorise** — if the request rides on a JCP [`SignedGrant`], verify the
//! issuer signature, confirm the capability permits `infer` on this exact
//! request, and confirm the grant's joule budget covers the request ceiling.
//! 2. **Route** — pick the cheapest-sufficient backend (`mesh-router`).
//! 1. **Route** — pick the cheapest-sufficient backend (`mesh-router`). With a
//! JCP [`SignedGrant`], a backend whose model the grant does not permit is
//! inadmissible.
//! 2. **Authorise** — decide the grant in JCP's two-phase metered path
//! (`jcp-runtime`), over a frame naming `infer` on this exact request and
//! the chosen backend's model: signature, expiry, revocation, every
//! cost-independent caveat, and whether what the grant has left covers the
//! request ceiling. All of it before any work runs.
//! 3. **Meter** — open an energy window around the engine's `serve` call
//! (`mesh-meter`), so the energy figure is measured, not self-reported.
//! (`mesh-meter`), so the energy figure is measured, not self-reported, and
//! debit the grant with it, where a per-call cost caveat binds.
//! 4. **Seal** — build the [`MeshReceipt`], append it to the steward's
//! hash-chained log, and return the sealed JCR-1 envelope.
//!
@ -27,7 +32,8 @@ compile_error!("a hw-meter-* feature must enable mesh-core's `hw-meter` feature"
use std::collections::HashMap;
use ed25519_dalek::SigningKey;
use jcp_schema::{ContentId, SignedGrant};
use jcp_runtime::Runtime;
use jcp_schema::{Caveat, ContentId, DenyReason, Frame, FrameKind, SignedGrant, Trust, Verdict};
use mesh_engine::ServingEngine;
use mesh_meter::{Meter, Session};
use mesh_receipt::{MeshReceipt, ReceiptError, ReceiptLog};
@ -49,9 +55,20 @@ pub enum MeshError {
/// The grant does not authorise inference on this request.
#[error("grant does not permit `{INFER_ACTION}` on this request")]
Unauthorized,
/// The grant's budget is below the request's joule ceiling.
#[error("grant budget {budget_uj} µJ below request ceiling {ceiling_uj} µJ")]
/// What the grant has left at this steward is below the request's joule
/// ceiling. `budget_uj` is the remainder: the grant's budget less what
/// earlier serves riding on it were debited.
#[error("grant has {budget_uj} µJ left, below request ceiling {ceiling_uj} µJ")]
BudgetTooLow { budget_uj: u64, ceiling_uj: u64 },
/// The JCP decision refused the grant for a reason the variants above do
/// not name: it expired, was revoked, carries a caveat that does not hold,
/// or carries a flow ceiling that Mesh computes no provenance for.
#[error("grant denied: {}", .0.label())]
GrantDenied(DenyReason),
/// The engine reported running other weights than the ones it named
/// before serving, which are the ones the grant was decided over.
#[error("backend `{backend}` named model {named} before serving but reported {reported}")]
ModelMismatch { backend: String, named: String, reported: String },
/// The chosen backend has no registered engine (internal invariant).
#[error("internal: no engine registered for backend `{0}`")]
NoEngine(String),
@ -93,6 +110,14 @@ pub struct Mesh<'m> {
engines: HashMap<String, Box<dyn ServingEngine>>,
meter: &'m dyn Meter,
log: ReceiptLog,
/// The steward's JCP enforcement point. Every grant a serve rides on is
/// decided here, in JCP's two-phase metered path: authorised before any
/// work, then debited with the metered joules. Expiry, revocation, every
/// caveat and the grant's cumulative spend therefore bind at Mesh as they
/// do at any other JCP enforcement point.
grants: Runtime,
/// Unix seconds, for expiry and time caveats.
clock: fn() -> u64,
}
impl<'m> Mesh<'m> {
@ -104,9 +129,23 @@ impl<'m> Mesh<'m> {
engines: HashMap::new(),
meter,
log: ReceiptLog::new(steward_sk, steward),
grants: Runtime::new(),
clock: system_clock,
}
}
/// Decide grants against `clock` (unix seconds) instead of the system
/// clock.
pub fn with_clock(mut self, clock: fn() -> u64) -> Self {
self.clock = clock;
self
}
/// The microjoules debited so far against a grant at this steward.
pub fn spent(&self, grant_id: &ContentId) -> u64 {
self.grants.spent(grant_id)
}
/// Register a backend: its routing spec and the engine that serves it.
pub fn register(&mut self, spec: BackendSpec, engine: Box<dyn ServingEngine>) -> &mut Self {
self.engines.insert(spec.id.0.clone(), engine);
@ -129,23 +168,43 @@ impl<'m> Mesh<'m> {
self.log.head()
}
/// Authorise a request against an optional JCP grant. Returns the grant id
/// to record on the receipt (or `None` for an ungranted request).
/// Authorise a request against an optional JCP grant, over the frame that
/// names the model the chosen backend runs. Returns the grant id to record
/// on the receipt (or `None` for an ungranted request). Debits nothing.
fn authorize(
&self,
&mut self,
req: &ServeRequest,
grant: Option<&SignedGrant>,
model: Option<ContentId>,
) -> Result<Option<ContentId>, MeshError> {
let Some(sg) = grant else {
return Ok(None);
};
jcp_grant::verify(sg).map_err(|e| MeshError::GrantInvalid(e.to_string()))?;
if !sg.grant.capability.permits(&req.id(), INFER_ACTION) {
return Err(MeshError::Unauthorized);
// Mesh does not authenticate who presents a grant (SPEC §8): it honours
// one as a bearer token of its subject, so the subject is the caller.
let d = self.grants.authorize(
&infer_frame(sg, req, model),
sg,
sg.grant.subject,
(self.clock)(),
);
match d.verdict {
Verdict::Allow => {}
Verdict::Deny(DenyReason::BadSignature) => {
let why = jcp_grant::verify(sg).err().map(|e| e.to_string());
return Err(MeshError::GrantInvalid(why.unwrap_or_else(|| "bad-signature".into())));
}
Verdict::Deny(DenyReason::WrongResource | DenyReason::ActionNotGranted) => {
return Err(MeshError::Unauthorized);
}
Verdict::Deny(DenyReason::BudgetExceeded) => {
return Err(MeshError::BudgetTooLow { budget_uj: 0, ceiling_uj: req.joule_ceiling_uj });
}
Verdict::Deny(r) => return Err(MeshError::GrantDenied(r)),
}
if sg.grant.budget_uj < req.joule_ceiling_uj {
if d.budget_remaining_uj < req.joule_ceiling_uj {
return Err(MeshError::BudgetTooLow {
budget_uj: sg.grant.budget_uj,
budget_uj: d.budget_remaining_uj,
ceiling_uj: req.joule_ceiling_uj,
});
}
@ -164,15 +223,35 @@ impl<'m> Mesh<'m> {
prompt: &[u32],
grant: Option<&SignedGrant>,
) -> Result<ServeResponse, MeshError> {
// 1. Authorise.
let grant_id = self.authorize(req, grant)?;
// 2. Route.
let route = self.router.route(req);
// 1. Route. With a grant, a backend whose model the grant does not
// permit is inadmissible, so the route is the cheapest backend the
// grant allows rather than one the decision would then refuse. This
// only steers: the decision in step 2 is what authorises.
let route = match grant {
None => self.router.route(req),
Some(sg) => {
let engines = &self.engines;
self.router.route_where(req, |b| {
let model = engines.get(&b.0).and_then(|e| e.weights_digest());
if model_permitted(sg, &infer_frame(sg, req, model)) {
Ok(())
} else {
Err(match model {
Some(m) => format!("model {} not permitted by the grant", m.to_hex()),
None => "names no model, and the grant permits only named models".into(),
})
}
})
}
};
let backend = route
.chosen
.clone()
.ok_or_else(|| MeshError::NoRoute(route.justification.clone()))?;
let model = self.engines.get(&backend.0).and_then(|e| e.weights_digest());
// 2. Authorise, before any work runs.
let grant_id = self.authorize(req, grant, model)?;
let chosen_fidelity = route
.chosen_fidelity
.expect("a chosen route carries its fidelity");
@ -190,7 +269,31 @@ impl<'m> Mesh<'m> {
let served = engine.serve(req, prompt);
let energy = session.stop();
// 3a. Decide whether the floor was met, from what came back rather than
// 3a. Debit the grant with what was metered: JCP's phase two, where a
// per-call cost caveat binds. The work has run and the joules are
// real, so they are debited whatever comes next, including a
// refusal to seal below; a breach is recorded on the receipt.
let grant_breach = match grant {
Some(sg) => match self.grants.charge(&sg.grant, jcp_energy(&energy)).verdict {
Verdict::Allow => None,
Verdict::Deny(r) => Some(r.label().to_string()),
},
None => None,
};
// 3b. The engine must have run the weights it named before serving:
// those are the ones the grant was decided over.
if let (Some(named), Some(reported)) = (model, served.outcome.weights_digest) {
if named != reported {
return Err(MeshError::ModelMismatch {
backend: backend.0.clone(),
named: named.to_hex(),
reported: reported.to_hex(),
});
}
}
// 3c. Decide whether the floor was met, from what came back rather than
// from the route table. The old code computed this at step 2, before
// the engine ran, so a backend that degraded at serve time still
// sealed `fidelity_met: 1`.
@ -246,6 +349,7 @@ impl<'m> Mesh<'m> {
fidelity_delivered: delivered,
fidelity_met,
energy,
grant_breach,
sequence: 0, // stamped by the log
prev: None, // stamped by the log
nonce,
@ -263,6 +367,54 @@ impl<'m> Mesh<'m> {
}
}
/// The JCP frame a serve exercises its grant with: the action `infer` on the
/// request's content id, the prompt as payload, and the model the backend
/// runs, if it named one.
fn infer_frame(sg: &SignedGrant, req: &ServeRequest, model: Option<ContentId>) -> Frame {
Frame {
kind: FrameKind::ToolCall,
trust: Trust::Control,
grant_id: sg.grant.id(),
action: INFER_ACTION.into(),
resource: req.id(),
payload_hash: req.prompt_hash,
model,
}
}
/// Whether the grant's model caveats admit `frame`'s model. `ModelIn` is the
/// only caveat that depends on which backend serves, so it is the only one
/// that steers the route.
fn model_permitted(sg: &SignedGrant, frame: &Frame) -> bool {
sg.grant
.caveats
.iter()
.filter(|c| matches!(c, Caveat::ModelIn(_)))
.all(|c| c.holds(frame, sg.grant.subject, 0, 0))
}
/// The metered span as JCP's energy type, for the debit. Only the debited
/// figure is read from it; Mesh's receipt carries the span itself.
fn jcp_energy(e: &Energy) -> jcp_schema::Energy {
let provenance = match e.provenance {
mesh_schema::Provenance::HwShunt => jcp_schema::Provenance::HwShunt,
mesh_schema::Provenance::ModelBased => jcp_schema::Provenance::ModelBased,
mesh_schema::Provenance::Estimator => jcp_schema::Provenance::Estimator,
};
jcp_schema::Energy {
measured_uj: e.measured_uj,
estimated_uj: e.estimated_uj,
provenance,
coverage: jcp_schema::Coverage::Whole,
}
}
fn system_clock() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
/// Derive a deterministic per-receipt nonce from the request id (first 8 bytes).
fn nonce_from(id: &ContentId) -> [u8; 8] {
let b = id.0;
@ -308,6 +460,221 @@ mod tests {
m
}
/// A grant over exactly `req`, held by a fixed subject.
fn grant_over(req: &ServeRequest, budget_uj: u64, expiry: u64, caveats: Vec<Caveat>) -> SignedGrant {
let issuer = SigningKey::from_bytes(&[42u8; 32]);
let holder = SigningKey::from_bytes(&[43u8; 32]);
let g = jcp_schema::Grant {
issuer: jcp_schema::Did::from_verifying_key(&issuer.verifying_key()),
subject: jcp_schema::Did::from_verifying_key(&holder.verifying_key()),
capability: jcp_schema::Capability::new(req.id(), [INFER_ACTION]),
budget_uj,
budget_funds: None,
caveats,
expiry,
parent: None,
delegate_depth: None,
nonce: [5u8; 16],
};
jcp_grant::issue(&issuer, g).expect("issue grant")
}
fn at_1000() -> u64 {
1_000
}
/// Expiry and every cost-independent caveat bind before any work runs.
/// Mesh used to check only the signature, the capability and the budget,
/// so each of these grants was served.
#[test]
fn a_grant_is_decided_whole_before_any_work() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let mut mesh = mesh_with(&meter).with_clock(at_1000);
let req = request(8, Fidelity::F1, 10_000_000);
let refused = [
(grant_over(&req, 20_000_000, 999, vec![]), DenyReason::Expired),
(grant_over(&req, 20_000_000, 0, vec![Caveat::NotAfter(999)]), DenyReason::CaveatUnsatisfied),
(grant_over(&req, 20_000_000, 0, vec![Caveat::NotBefore(1_001)]), DenyReason::CaveatUnsatisfied),
(
grant_over(&req, 20_000_000, 0, vec![Caveat::PayloadIs(ContentId::of_bytes(b"another prompt"))]),
DenyReason::CaveatUnsatisfied,
),
];
for (sg, reason) in &refused {
match mesh.serve(&req, &[], Some(sg)) {
Err(MeshError::GrantDenied(r)) => assert_eq!(r, *reason),
other => panic!("expected {reason:?}, got {other:?}"),
}
assert_eq!(mesh.spent(&sg.grant.id()), 0);
}
assert_eq!(mesh.head(), None, "nothing ran, so nothing was sealed");
let fine = grant_over(&req, 20_000_000, 1_001, vec![Caveat::NotAfter(1_000), Caveat::PayloadIs(req.prompt_hash)]);
let resp = mesh.serve(&req, &[], Some(&fine)).expect("serve");
assert_eq!(resp.receipt.grant_id, Some(fine.grant.id()));
assert_eq!(mesh.spent(&fine.grant.id()), 1_000);
}
/// The refusals Mesh made before keep their errors.
#[test]
fn signature_capability_and_budget_refusals_keep_their_errors() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let mut mesh = mesh_with(&meter);
let req = request(8, Fidelity::F1, 10_000_000);
let mut forged = grant_over(&req, 20_000_000, 0, vec![]);
forged.grant.budget_uj = 1_000_000_000;
assert!(matches!(mesh.serve(&req, &[], Some(&forged)), Err(MeshError::GrantInvalid(_))));
let elsewhere = grant_over(&request(9, Fidelity::F1, 10_000_000), 20_000_000, 0, vec![]);
assert!(matches!(mesh.serve(&req, &[], Some(&elsewhere)), Err(MeshError::Unauthorized)));
let starved = grant_over(&req, 1, 0, vec![]);
assert!(matches!(
mesh.serve(&req, &[], Some(&starved)),
Err(MeshError::BudgetTooLow { budget_uj: 1, ceiling_uj: 10_000_000 })
));
assert_eq!(mesh.head(), None);
}
/// A grant's budget is what it may ever spend, so serves riding on it spend
/// it down. Before, each serve checked the whole budget against its own
/// ceiling and debited nothing, so one grant paid for any number of them.
#[test]
fn a_grant_is_spent_down_across_serves() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 3_000_000);
let mut mesh = mesh_with(&meter);
let req = request(8, Fidelity::F1, 4_000_000);
let sg = grant_over(&req, 10_000_000, 0, vec![]);
for n in 1..=3 {
mesh.serve(&req, &[], Some(&sg)).expect("within what the grant has left");
assert_eq!(mesh.spent(&sg.grant.id()), 3_000_000 * n);
}
// 1 J left, below the 4 J ceiling: refused before any work.
assert!(matches!(
mesh.serve(&req, &[], Some(&sg)),
Err(MeshError::BudgetTooLow { budget_uj: 1_000_000, ceiling_uj: 4_000_000 })
));
assert_eq!(mesh.spent(&sg.grant.id()), 9_000_000);
}
/// `ModelIn` makes a backend whose model the grant does not permit
/// inadmissible, so the route is the cheapest permitted one, and the
/// candidates record why the others were refused.
#[test]
fn model_in_routes_to_the_cheapest_backend_the_grant_permits() {
let (a, b) = (ContentId::of_bytes(b"ssm-1b weights"), ContentId::of_bytes(b"frontier weights"));
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let (sk, steward) = steward_key();
let mut mesh = Mesh::new(&meter, sk, steward);
mesh.register(
BackendSpec::new("anon", "unnamed", Fidelity::F1, 10_000),
Box::new(ReferenceEngine::new("anon", "unnamed", Fidelity::F1, 10_000, 64)),
);
mesh.register(
BackendSpec::new("tiny", "ssm-1b", Fidelity::F1, 50_000),
Box::new(ReferenceEngine::new("tiny", "ssm-1b", Fidelity::F1, 50_000, 64).reporting_model_identity(a, "fp16")),
);
mesh.register(
BackendSpec::new("frontier", "wire-rpc", Fidelity::F4, 5_000_000),
Box::new(
ReferenceEngine::new("frontier", "wire-rpc", Fidelity::F4, 5_000_000, 64).reporting_model_identity(b, "fp16"),
),
);
let req = request(8, Fidelity::F1, 100_000_000);
let open = grant_over(&req, 1_000_000_000, 0, vec![]);
assert_eq!(mesh.serve(&req, &[], Some(&open)).unwrap().receipt.backend, BackendId::new("anon"));
let only_b = grant_over(&req, 1_000_000_000, 0, vec![Caveat::ModelIn(vec![b])]);
let resp = mesh.serve(&req, &[], Some(&only_b)).expect("serve");
assert_eq!(resp.receipt.backend, BackendId::new("frontier"));
assert_eq!(resp.receipt.weights_digest, Some(b));
let reason = |id: &str| {
let c = resp.route.candidates.iter().find(|c| c.backend.0 == id).unwrap();
assert!(!c.admissible);
c.reason.clone().unwrap()
};
assert!(reason("tiny").contains("not permitted"), "{}", reason("tiny"));
assert!(reason("anon").contains("names no model"), "{}", reason("anon"));
let head = mesh.head();
let absent = grant_over(&req, 1_000_000_000, 0, vec![Caveat::ModelIn(vec![ContentId::of_bytes(b"absent")])]);
assert!(matches!(mesh.serve(&req, &[], Some(&absent)), Err(MeshError::NoRoute(_))));
assert_eq!(mesh.head(), head);
assert_eq!(mesh.spent(&absent.grant.id()), 0);
}
/// Names one model before serving and reports another after.
struct SwappingEngine {
inner: ReferenceEngine,
named: ContentId,
}
impl ServingEngine for SwappingEngine {
fn backend_id(&self) -> BackendId {
self.inner.backend_id()
}
fn recipe_id(&self) -> String {
self.inner.recipe_id()
}
fn fidelity(&self) -> Fidelity {
self.inner.fidelity()
}
fn est_uj_per_token(&self) -> u64 {
self.inner.est_uj_per_token()
}
fn weights_digest(&self) -> Option<ContentId> {
Some(self.named)
}
fn serve(&mut self, req: &ServeRequest, prompt: &[u32]) -> mesh_engine::Served {
self.inner.serve(req, prompt)
}
}
/// An engine that runs other weights than the ones the grant was decided
/// over is refused a receipt. The joules it spent are still debited.
#[test]
fn an_engine_that_swaps_its_model_is_refused_a_receipt() {
let (named, ran) = (ContentId::of_bytes(b"approved weights"), ContentId::of_bytes(b"other weights"));
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 2_000);
let (sk, steward) = steward_key();
let mut mesh = Mesh::new(&meter, sk, steward);
mesh.register(
BackendSpec::new("swap", "approved", Fidelity::F1, 50_000),
Box::new(SwappingEngine {
inner: ReferenceEngine::new("swap", "approved", Fidelity::F1, 50_000, 64).reporting_model_identity(ran, "fp16"),
named,
}),
);
let req = request(8, Fidelity::F1, 10_000_000);
let sg = grant_over(&req, 20_000_000, 0, vec![Caveat::ModelIn(vec![named])]);
assert!(matches!(mesh.serve(&req, &[], Some(&sg)), Err(MeshError::ModelMismatch { .. })));
assert_eq!(mesh.head(), None);
assert_eq!(mesh.spent(&sg.grant.id()), 2_000);
}
/// A per-call cost caveat binds at the debit: the work has run, so the
/// serve is debited and sealed, and the receipt carries the breach.
#[test]
fn a_per_call_cost_breach_is_debited_and_sealed_on_the_receipt() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 3_000_000);
let mut mesh = mesh_with(&meter);
let req = request(8, Fidelity::F1, 10_000_000);
let capped = grant_over(&req, 20_000_000, 0, vec![Caveat::MaxCostUj(1_000_000)]);
let resp = mesh.serve(&req, &[], Some(&capped)).expect("the work ran, so the serve seals");
assert_eq!(resp.receipt.grant_breach.as_deref(), Some("caveat-unsatisfied"));
assert_eq!(mesh.spent(&capped.grant.id()), 3_000_000);
assert_eq!(resp.receipt.to_jcr1_wire(mesh.steward())["grant_breach"], "caveat-unsatisfied");
assert!(resp.receipt.verify_envelope(&resp.envelope, mesh.steward(), &mesh.verifying_key()).is_ok());
let roomy = grant_over(&req, 20_000_000, 0, vec![Caveat::MaxCostUj(5_000_000)]);
let ok = mesh.serve(&req, &[], Some(&roomy)).expect("serve");
assert_eq!(ok.receipt.grant_breach, None);
assert!(ok.receipt.to_jcr1_wire(mesh.steward()).get("grant_breach").is_none());
}
#[test]
fn measured_end_to_end_seals_and_verifies() {
// A HwShunt stepping meter yields a deterministic 3_200_000 µJ span,

View file

@ -39,6 +39,19 @@ pub trait ServingEngine {
fn fidelity(&self) -> Fidelity;
/// Modeled microjoules per token (the router's a-priori cost estimate).
fn est_uj_per_token(&self) -> u64;
/// The content address of the weights this engine runs, when it knows
/// them before serving. Mesh names this model in the JCP frame a grant is
/// decided over, so a grant's `ModelIn` caveat can admit or refuse the
/// backend before any work runs (SPEC §2). `None`, the default, names no
/// model, and a grant that permits only named models refuses it.
///
/// An engine that reports one here MUST report the same digest in
/// [`ServeOutcome::weights_digest`]: Mesh refuses to seal a serve whose
/// engine ran other weights than the ones the grant was decided over.
fn weights_digest(&self) -> Option<ContentId> {
None
}
/// Serve the request. `prompt` is the input token payload — passed
/// alongside the request, never sealed into the receipt (the receipt records
/// only the content-addressed `prompt_hash`, §3.1). Implementations MUST do
@ -189,6 +202,10 @@ impl ServingEngine for ReferenceEngine {
self.uj_per_token
}
fn weights_digest(&self) -> Option<ContentId> {
self.weights_digest
}
fn serve(&mut self, req: &ServeRequest, _prompt: &[u32]) -> Served {
// The reference engine is synthetic: its output is a deterministic
// function of the request's content id, independent of the prompt

View file

@ -110,6 +110,14 @@ pub struct MeshReceipt {
pub fidelity_met: bool,
/// The honest, provenance-tagged energy span.
pub energy: Energy,
/// The JCP denial label when debiting the metered joules against the grant
/// breached it (SPEC §2): a per-call cost caveat the measured cost
/// exceeded. The work had already run, so the joules were debited anyway;
/// this field is what keeps the breach visible instead of absorbed.
/// `None`, skipped on the wire, means the debit was within the grant, or
/// the serve rode on no grant.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grant_breach: Option<String>,
/// Monotonic sequence in the steward's receipt log.
pub sequence: u64,
/// Content id of the previous envelope (hash chain); `None` at genesis.
@ -211,6 +219,11 @@ impl MeshReceipt {
"energy_provenance".into(),
json!(self.energy.provenance.jcr1_label()),
);
// Emitted only on a breach, so every receipt whose debit was within its
// grant seals to the bytes it sealed to before the field existed.
if let Some(b) = &self.grant_breach {
obj.insert("grant_breach".into(), json!(b));
}
obj.insert("sequence".into(), json!(self.sequence));
if let Some(p) = &self.prev {
obj.insert("prev".into(), json!(p.to_hex()));
@ -345,6 +358,7 @@ mod tests {
fidelity_delivered: Some(Fidelity::F2),
fidelity_met: true,
energy,
grant_breach: None,
sequence: 0,
prev: None,
nonce: [1, 2, 3, 4, 5, 6, 7, 8],

View file

@ -404,10 +404,24 @@ impl Router {
/// Route a request: weigh every backend, choose the cheapest admissible one,
/// and return an auditable decision.
pub fn route(&self, req: &ServeRequest) -> RouteDecision {
self.route_where(req, |_| Ok(()))
}
/// [`route`](Self::route), with one more admissibility test: `permit` may
/// refuse a backend, giving the reason the decision records. Mesh uses it
/// to admit only backends whose model a JCP grant permits, so the route
/// is the cheapest backend the grant allows, not the cheapest overall.
pub fn route_where(
&self,
req: &ServeRequest,
permit: impl Fn(&BackendId) -> Result<(), String>,
) -> RouteDecision {
let mut candidates = Vec::with_capacity(self.backends.len());
for b in &self.backends {
let est = b.estimate(req);
let (admissible, reason) = if b.fidelity < req.fidelity_floor {
let (admissible, reason) = if let Err(why) = permit(&b.id) {
(false, Some(why))
} else if b.fidelity < req.fidelity_floor {
(
false,
Some(format!(
@ -468,7 +482,7 @@ impl Router {
}
None => {
let justification = format!(
"no admissible backend: all {} candidate(s) below fidelity floor {} or over ceiling {} µJ",
"no admissible backend: all {} candidate(s) refused, below fidelity floor {} or over ceiling {} µJ",
candidates.len(),
req.fidelity_floor.label(),
req.joule_ceiling_uj