mesh: a grant rides over the OpenAI-compatible HTTP binding, and a receipt can be shown back as a witness #312

Merged
dcharlot merged 1 commit from feat/mesh-http-grants into main 2026-10-18 07:20:05 -04:00
8 changed files with 439 additions and 23 deletions

View file

@ -163,6 +163,26 @@ short and is dropped: an authorisation never recorded never started its serve, a
debit never recorded leaves its serve unresolved, and a seal never recorded was
never handed out.
### §2.3 Grants over the OpenAI-compatible binding (informative)
The reference binding (`mesh-openai`) serves `/v1/chat/completions` and
`/v1/responses`. A request rides on a grant by carrying its presentation (§2.1)
in the body field `jcp_presentation`. The presentation is signed for a request
id, so a caller must be able to compute the request a body is served as: a
granted request is served as exactly what the binding derives from the body
alone, with the prompt-token figure left as the body's estimate rather than
replaced by the server's tokenizer count, and it MUST name its own
`metadata.joule_ceiling_uj`, since a server default would otherwise enter the id
unseen. The binding exports that derivation as `granted_serve_request`.
`GET /v1/mesh/steward` returns the steward's `did:key`, the subject a grant must
name. `POST /v1/mesh/witness`, with a body of the `receipt` and `envelope_hex`
a response's `usage.energy` carried, answers 200 when the journal holds that
receipt and 409 when it does not, after which grants are refused (§2.2). A
refused grant answers 401 when the caller is not its issuer, 403 when the
steward refuses the grant, and 503 once the steward has stopped honouring grants.
---
## §3 The request and the energy record

View file

@ -622,6 +622,8 @@ version = "0.1.0"
dependencies = [
"ed25519-dalek",
"hex",
"jcp-grant",
"jcp-schema",
"jouleclaw-backend-apple",
"mesh-core",
"mesh-engine",

View file

@ -275,10 +275,35 @@ impl<'m> Mesh<'m> {
receipt
.verify_envelope(envelope, self.log.steward(), &self.log.verifying_key())
.map_err(|e| MeshError::WitnessInvalid(e.to_string()))?;
if self.sealed.get(receipt.sequence as usize) == Some(&ContentId::of_bytes(envelope)) {
self.held(receipt.sequence, envelope)
}
/// [`witness`](Self::witness) for a receipt in its `mesh/1` wire form, the
/// form the HTTP binding hands out: the envelope's signature must verify
/// under this steward, and its payload must be exactly the wire receipt's
/// signed core, whose `sequence` then names the record to look for.
pub fn witness_wire(&mut self, receipt: &serde_json::Value, envelope: &[u8]) -> Result<(), MeshError> {
let invalid = |e: String| MeshError::WitnessInvalid(e);
let payload = mesh_receipt::jcr1::verify_cose_sign1(envelope, self.log.steward(), &self.log.verifying_key())
.map_err(|e| invalid(e.to_string()))?;
let core = mesh_receipt::jcr1::signed_core_bytes(receipt).map_err(|e| invalid(e.to_string()))?;
if payload != core {
return Err(invalid("the envelope does not seal this receipt".into()));
}
let sequence = receipt
.get("sequence")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| invalid("the receipt carries no sequence".into()))?;
self.held(sequence, envelope)
}
/// Whether the journal holds the receipt sealed at `sequence` as
/// `envelope`; if not, the journal was cut, and grants are refused.
fn held(&mut self, sequence: u64, envelope: &[u8]) -> Result<(), MeshError> {
if self.sealed.get(sequence as usize) == Some(&ContentId::of_bytes(envelope)) {
return Ok(());
}
let cut = (self.sealed.len() as u64, receipt.sequence);
let cut = (self.sealed.len() as u64, sequence);
self.cut = Some(cut);
Err(MeshError::JournalCut { holds: cut.0, witnessed: cut.1 })
}
@ -1195,6 +1220,31 @@ mod tests {
assert!(resume(&recorded).is_none());
}
/// A receipt in its wire form, as the HTTP binding hands it out, is a
/// witness too: the envelope must seal exactly that wire receipt.
#[test]
fn a_wire_receipt_is_a_witness() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let req = request(8, Fidelity::F1, 4_000_000);
let journal = SharedJournal::default();
let mut before = mesh_with(&meter).with_journal(journal.clone(), &[]).unwrap();
let first = before.serve(&req, &[], None).unwrap();
let second = before.serve(&req, &[], None).unwrap();
let wire = |r: &ServeResponse| r.receipt.to_jcr1_wire(before.steward());
let (w1, w2) = (wire(&first), wire(&second));
before.witness_wire(&w1, &first.envelope).expect("its own wire receipt");
let mut altered = w1.clone();
altered["sequence"] = serde_json::json!(5);
assert!(matches!(before.witness_wire(&altered, &first.envelope), Err(MeshError::WitnessInvalid(_))));
assert!(matches!(before.witness_wire(&w2, &first.envelope), Err(MeshError::WitnessInvalid(_))));
drop(before);
let recorded = journal.0.borrow().clone();
let mut cut = mesh_with(&meter).with_journal(MemoryJournal::default(), &recorded[..1]).unwrap();
cut.witness_wire(&w1, &first.envelope).expect("the journal holds the first");
assert!(matches!(cut.witness_wire(&w2, &second.envelope), Err(MeshError::JournalCut { holds: 1, witnessed: 1 })));
}
/// A witness must be a receipt this steward sealed, unaltered.
#[test]
fn a_witness_must_be_this_stewards_receipt() {

View file

@ -72,6 +72,19 @@ fn main() -> std::io::Result<()> {
Box::new(engine),
);
// With `MESH_JOURNAL` set, grant decisions and seals are recorded in that
// file and replayed before serving, so a restart gives no budget back and
// the receipt chain continues (Mesh SPEC §2.2).
if let Some(path) = std::env::var_os("MESH_JOURNAL") {
let (journal, recorded) = mesh_core::FileJournal::open(&path)?;
mesh = mesh
.with_journal(journal, &recorded)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
for u in mesh.unresolved() {
eprintln!("unresolved: request {} on backend {}, debited {} µJ", u.request_id.to_hex(), u.backend, u.debited_uj);
}
}
let listener = TcpListener::bind(&addr)?;
println!("OpenAI-compatible Mesh endpoint: http://{addr}/v1/chat/completions");
println!("steward: {} | backend: falcon-h1-0.5b (F3) | receipt in usage.energy", mesh.steward());

View file

@ -22,6 +22,8 @@ jouleclaw-backend-apple = { workspace = true, optional = true }
[dev-dependencies]
mesh-engine = { workspace = true }
mesh-meter = { workspace = true }
jcp-schema = { workspace = true }
jcp-grant = { workspace = true }
mesh-router = { workspace = true }
mesh-receipt = { workspace = true }
ed25519-dalek = { workspace = true }

View file

@ -92,6 +92,24 @@ fn main() -> std::io::Result<()> {
let listener = TcpListener::bind(&addr)?;
let meter = meter();
eprintln!("mesh OpenAI server: http://{addr}/v1/chat/completions (meter: {})", meter.name());
let mut mesh = mesh(meter.as_ref());
let mut mesh = journalled(mesh(meter.as_ref()))?;
server::run(&listener, &mut mesh, &ByteBackend, 100_000_000)
}
/// With `MESH_JOURNAL` set, record grant decisions and seals in that file and
/// replay what it already holds before serving, so a restart gives no budget
/// back and the receipt chain continues (Mesh SPEC §2.2). Without it the
/// journal is in memory, which a steward that restarts must not rely on.
pub fn journalled(mesh: mesh_core::Mesh<'_>) -> std::io::Result<mesh_core::Mesh<'_>> {
let Some(path) = std::env::var_os("MESH_JOURNAL") else {
return Ok(mesh);
};
let (journal, recorded) = mesh_core::FileJournal::open(&path)?;
let mesh = mesh
.with_journal(journal, &recorded)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
for u in mesh.unresolved() {
eprintln!("unresolved: request {} on backend {}, debited {} µJ", u.request_id.to_hex(), u.backend, u.debited_uj);
}
Ok(mesh)
}

View file

@ -23,6 +23,11 @@
//! served result returned as a Responses `output_text` item with the sealed
//! receipt under `usage.energy`. The reference `server` exposes both routes.
//!
//! - A request may ride on a JCP grant: its body carries a
//! [`Presentation`] in `jcp_presentation` ([`presentation_in`]), signed for
//! exactly the request [`granted_serve_request`] computes from the body, and
//! the steward decides it before any work (Mesh SPEC §2.1).
//!
//! Ingest, not compete: the OpenAI shape is the surface; Mesh contributes the
//! metered receipt. Token counts in/out are the engine's tokenizer's job; where
//! a request omits them this crate makes an explicit ~chars/4 estimate (honestly
@ -47,6 +52,71 @@ pub enum OpenAiError {
/// The mesh refused or failed to serve the request.
#[error("serve failed: {0}")]
Serve(#[from] MeshError),
/// A request that rides on a grant must name its own joule ceiling: the
/// request its caller signs for is computed from the body alone, and a
/// server default would enter it unseen.
#[error("a request that rides on a grant names its own `metadata.joule_ceiling_uj`")]
GrantedNeedsCeiling,
/// The body's `jcp_presentation` is not a presentation.
#[error("`{PRESENTATION_FIELD}` is not a presentation: {0}")]
BadPresentation(String),
}
/// The body field a request carries its grant's [`Presentation`] in.
pub const PRESENTATION_FIELD: &str = "jcp_presentation";
/// The grant a request body carries, if any: its [`PRESENTATION_FIELD`].
pub fn presentation_in(req: &Value) -> Result<Option<Presentation>, OpenAiError> {
match req.get(PRESENTATION_FIELD) {
None => Ok(None),
Some(v) => serde_json::from_value(v.clone()).map(Some).map_err(|e| OpenAiError::BadPresentation(e.to_string())),
}
}
/// The exact [`ServeRequest`] a granted `/v1/chat/completions` body is served
/// as: the request whose id the grant's capability names and the presentation
/// is signed for. It is computed from the body alone, so a caller can compute
/// it too. The body must name its joule ceiling, and the prompt-token figure
/// stays the body's chars/4 estimate rather than the server's tokenizer count,
/// which a caller cannot reproduce. That figure is only the router's cost
/// estimate: the energy is metered, and `tokens_in` is what the engine reports.
pub fn granted_serve_request(req: &Value) -> Result<ServeRequest, OpenAiError> {
names_ceiling(req)?;
Ok(to_serve_request(req, 0)?.0)
}
/// [`granted_serve_request`] for a `/v1/responses` body.
pub fn granted_serve_request_responses(req: &Value) -> Result<ServeRequest, OpenAiError> {
names_ceiling(req)?;
Ok(to_serve_request_responses(req, 0)?.0)
}
fn names_ceiling(req: &Value) -> Result<(), OpenAiError> {
match req.get("metadata").and_then(|m| m.get("joule_ceiling_uj")).and_then(Value::as_u64) {
Some(_) => Ok(()),
None => Err(OpenAiError::GrantedNeedsCeiling),
}
}
/// The HTTP status a failed request answers with: 401 for a caller that is
/// not the grant's issuer, 403 for a grant the steward refuses, 503 for a
/// steward that has stopped honouring grants because its journal was cut,
/// 500 for a journal it cannot write, and 400 for everything else.
pub fn http_status(e: &OpenAiError) -> (u16, &'static str) {
match e {
OpenAiError::Serve(MeshError::CallerNotAuthenticated(_)) => (401, "Unauthorized"),
OpenAiError::Serve(
MeshError::Replayed
| MeshError::NotThisSteward
| MeshError::Unauthorized
| MeshError::GrantInvalid(_)
| MeshError::GrantDenied(_)
| MeshError::BudgetTooLow { .. },
) => (403, "Forbidden"),
OpenAiError::Serve(MeshError::JournalCut { .. }) => (503, "Service Unavailable"),
OpenAiError::Serve(MeshError::Journal(_)) => (500, "Internal Server Error"),
_ => (400, "Bad Request"),
}
}
/// Parse an OpenAI `/v1/chat/completions` request into a Mesh [`ServeRequest`]
@ -209,8 +279,13 @@ pub fn complete_chat<B: ChatBackend>(
) -> Result<Value, OpenAiError> {
let (mut serve, prompt) = to_serve_request(req, default_ceiling_uj)?;
let ids = backend.encode(&prompt);
// Replace the chars/4 estimate with the tokenizer's real input count.
serve.prompt_tokens = ids.len() as u32;
if grant.is_some() {
// Served as exactly the request its caller signed for.
serve = granted_serve_request(req)?;
} else {
// Replace the chars/4 estimate with the tokenizer's real input count.
serve.prompt_tokens = ids.len() as u32;
}
let model = req
.get("model")
@ -415,7 +490,11 @@ pub fn complete_responses<B: ChatBackend>(
) -> Result<Value, OpenAiError> {
let (mut serve, prompt) = to_serve_request_responses(req, default_ceiling_uj)?;
let ids = backend.encode(&prompt);
serve.prompt_tokens = ids.len() as u32;
if grant.is_some() {
serve = granted_serve_request_responses(req)?;
} else {
serve.prompt_tokens = ids.len() as u32;
}
let model = req.get("model").and_then(Value::as_str).unwrap_or("mesh").to_string();
let resp = mesh.serve(&serve, &ids, grant)?;
@ -463,6 +542,106 @@ mod tests {
assert_eq!(serve.max_tokens, 256); // default when omitted
}
struct Bytes;
impl ChatBackend for Bytes {
fn encode(&self, text: &str) -> Vec<u32> {
text.bytes().map(u32::from).collect()
}
fn decode(&self, ids: &[u32]) -> String {
format!("{} tokens", ids.len())
}
}
fn steward_mesh(meter: &dyn mesh_meter::Meter) -> Mesh<'_> {
let mut mesh = Mesh::new(meter, ed25519_dalek::SigningKey::from_bytes(&[8u8; 32]), "did:example:mesh-test");
mesh.register(
mesh_router::BackendSpec::new("tiny", "ssm-1b", Fidelity::F1, 50_000),
Box::new(mesh_engine::ReferenceEngine::new("tiny", "ssm-1b", Fidelity::F1, 50_000, 32)),
);
mesh
}
/// `body` with a presentation of a grant, issued by `caller` to `mesh`'s
/// steward over exactly the request the body is served as, signed by
/// `signer`.
fn granted(body: &Value, mesh: &Mesh, signer: &ed25519_dalek::SigningKey, nonce: u8) -> Value {
let caller = ed25519_dalek::SigningKey::from_bytes(&[42u8; 32]);
let req = granted_serve_request(body).unwrap();
let g = jcp_schema::Grant {
issuer: jcp_schema::Did::from_verifying_key(&caller.verifying_key()),
subject: mesh.steward_did(),
capability: jcp_schema::Capability::new(req.id(), [mesh_core::INFER_ACTION]),
budget_uj: 1_000_000_000,
budget_funds: None,
caveats: Vec::new(),
expiry: 0,
parent: None,
delegate_depth: None,
nonce: [1u8; 16],
};
let sg = jcp_grant::issue(&caller, g).unwrap();
let mut out = body.clone();
out[PRESENTATION_FIELD] = serde_json::to_value(Presentation::sign(vec![sg], &req, signer, [nonce; 16])).unwrap();
out
}
fn chat_body() -> Value {
json!({
"model": "gpt-mini",
"messages": [{ "role": "user", "content": "capital of France?" }],
"max_tokens": 8,
"metadata": { "joule_ceiling_uj": 50_000_000, "fidelity_floor": 1 }
})
}
/// A request over HTTP can ride on a grant: its body carries the
/// presentation, signed for the request the body alone determines, and
/// the receipt names the grant it was spent on.
#[test]
fn a_granted_request_is_served_as_the_request_its_caller_signed_for() {
let meter = mesh_meter::EstimatorMeter::default();
let mut mesh = steward_mesh(&meter);
let caller = ed25519_dalek::SigningKey::from_bytes(&[42u8; 32]);
let body = granted(&chat_body(), &mesh, &caller, 1);
let p = presentation_in(&body).unwrap().expect("a presentation");
let resp = complete_chat(&mut mesh, &Bytes, Some(&p), &body, 1, 0, "id").unwrap();
let receipt = &resp["usage"]["energy"]["receipt"];
assert_eq!(receipt["grant_id"], p.leaf().unwrap().grant.id().to_hex());
assert_eq!(receipt["request_id"], granted_serve_request(&body).unwrap().id().to_hex());
// Without a grant the server swaps in its tokenizer's count, so the
// request it serves is not one a caller could have signed for.
let plain = complete_chat(&mut mesh, &Bytes, None, &chat_body(), 1, 0, "id").unwrap();
assert_ne!(plain["usage"]["energy"]["receipt"]["request_id"], receipt["request_id"]);
}
/// The ceiling enters the request id, so a granted request must name it.
#[test]
fn a_granted_request_must_name_its_ceiling() {
let mut body = chat_body();
body["metadata"].as_object_mut().unwrap().remove("joule_ceiling_uj");
assert!(matches!(granted_serve_request(&body), Err(OpenAiError::GrantedNeedsCeiling)));
assert!(matches!(presentation_in(&json!({ PRESENTATION_FIELD: 7 })), Err(OpenAiError::BadPresentation(_))));
assert!(presentation_in(&chat_body()).unwrap().is_none());
}
/// A caller that is not the grant's issuer is 401, a replay 403.
#[test]
fn grant_refusals_carry_their_http_status() {
let meter = mesh_meter::EstimatorMeter::default();
let mut mesh = steward_mesh(&meter);
let thief = ed25519_dalek::SigningKey::from_bytes(&[66u8; 32]);
let forged = granted(&chat_body(), &mesh, &thief, 1);
let e = complete_chat(&mut mesh, &Bytes, presentation_in(&forged).unwrap().as_ref(), &forged, 1, 0, "id").unwrap_err();
assert_eq!(http_status(&e).0, 401, "{e}");
let caller = ed25519_dalek::SigningKey::from_bytes(&[42u8; 32]);
let body = granted(&chat_body(), &mesh, &caller, 2);
let p = presentation_in(&body).unwrap();
complete_chat(&mut mesh, &Bytes, p.as_ref(), &body, 1, 0, "id").unwrap();
let e = complete_chat(&mut mesh, &Bytes, p.as_ref(), &body, 1, 0, "id").unwrap_err();
assert_eq!(http_status(&e).0, 403, "{e}");
}
#[test]
fn model_fidelity_heuristic() {
assert_eq!(fidelity_for_model("gpt-mini"), Fidelity::F2);

View file

@ -6,6 +6,11 @@
//! `usage.energy`. It is single-connection-at-a-time by design, so the steward's
//! receipt chain stays coherent (one ordered ledger of what was served).
//!
//! A request rides on a JCP grant by carrying its presentation in the body's
//! `jcp_presentation` field (Mesh SPEC §2.1); `GET /v1/mesh/steward` names the
//! identity a grant must be issued to, and `POST /v1/mesh/witness` checks a
//! receipt this steward sealed against its journal (§2.2).
//!
//! ```no_run
//! # #[cfg(feature = "server")] {
//! use std::net::TcpListener;
@ -26,7 +31,12 @@ use std::time::{SystemTime, UNIX_EPOCH};
use mesh_core::Mesh;
use serde_json::{json, Value};
use crate::{complete_chat, complete_responses, ChatBackend};
use crate::{complete_chat, complete_responses, http_status, presentation_in, ChatBackend, OpenAiError};
/// The largest request body read, in bytes. A larger one is refused before
/// it is read, so a client cannot grow the server's memory by declaring a
/// length.
const MAX_BODY: usize = 4 << 20;
/// Serve exactly `n` connections, then return. `run` is `serve_n(.., usize::MAX)`.
/// Useful for tests and bounded demos.
@ -83,7 +93,12 @@ fn handle_conn<B: ChatBackend>(
}
};
if req.method == "POST" && req.path.starts_with("/v1/chat/completions") {
if req.too_large {
write_json(stream, 413, "Payload Too Large", &error_json("request body over the server's limit"));
return;
}
let completion = req.path.starts_with("/v1/chat/completions");
if req.method == "POST" && (completion || req.path.starts_with("/v1/responses")) {
let body: Value = match serde_json::from_str(&req.body) {
Ok(v) => v,
Err(e) => {
@ -91,25 +106,28 @@ fn handle_conn<B: ChatBackend>(
return;
}
};
let id = format!("chatcmpl-mesh-{seq}");
match complete_chat(mesh, backend, None, &body, default_ceiling_uj, now_secs(), &id) {
Ok(resp) => write_json(stream, 200, "OK", &resp),
// A refused serve (no route, budget, …) maps to a 400 with the reason.
Err(e) => write_json(stream, 400, "Bad Request", &error_json(&e.to_string())),
}
} else if req.method == "POST" && req.path.starts_with("/v1/responses") {
let body: Value = match serde_json::from_str(&req.body) {
Ok(v) => v,
Err(e) => {
write_json(stream, 400, "Bad Request", &error_json(&format!("invalid JSON: {e}")));
return;
let served = presentation_in(&body).and_then(|grant| {
if completion {
let id = format!("chatcmpl-mesh-{seq}");
complete_chat(mesh, backend, grant.as_ref(), &body, default_ceiling_uj, now_secs(), &id)
} else {
let id = format!("resp-mesh-{seq}");
complete_responses(mesh, backend, grant.as_ref(), &body, default_ceiling_uj, now_secs(), &id)
}
};
let id = format!("resp-mesh-{seq}");
match complete_responses(mesh, backend, None, &body, default_ceiling_uj, now_secs(), &id) {
});
match served {
Ok(resp) => write_json(stream, 200, "OK", &resp),
Err(e) => write_json(stream, 400, "Bad Request", &error_json(&e.to_string())),
// A refused serve answers with the status its reason calls for.
Err(e) => {
let (code, reason) = http_status(&e);
write_json(stream, code, reason, &error_json(&e.to_string()));
}
}
} else if req.method == "GET" && req.path.starts_with("/v1/mesh/steward") {
write_json(stream, 200, "OK", &json!({ "steward": mesh.steward(), "did": mesh.steward_did().to_did_key() }));
} else if req.method == "POST" && req.path.starts_with("/v1/mesh/witness") {
let (code, reason, body) = witness(mesh, &req.body);
write_json(stream, code, reason, &body);
} else if req.method == "GET" && req.path.starts_with("/v1/models") {
write_json(stream, 200, "OK", &models_json(mesh));
} else {
@ -138,6 +156,8 @@ struct Request {
method: String,
path: String,
body: String,
/// The declared body was over [`MAX_BODY`] and was not read.
too_large: bool,
}
/// Read one HTTP/1.1 request: the request line, headers up to the blank line,
@ -176,6 +196,9 @@ fn read_request(stream: &mut TcpStream) -> std::io::Result<Option<Request>> {
}
}
if content_length > MAX_BODY {
return Ok(Some(Request { method, path, body: String::new(), too_large: true }));
}
let body_start = header_end + 4;
let mut body: Vec<u8> = buf[body_start..].to_vec();
while body.len() < content_length {
@ -191,9 +214,34 @@ fn read_request(stream: &mut TcpStream) -> std::io::Result<Option<Request>> {
method,
path,
body: String::from_utf8_lossy(&body).to_string(),
too_large: false,
}))
}
/// `POST /v1/mesh/witness`: a body `{"receipt": <mesh/1 wire receipt>,
/// "envelope_hex": "..."}`, as a response's `usage.energy` carries them. 200
/// if the journal holds the receipt; 409 if it does not, after which the
/// steward refuses grants; 400 if it is not a receipt this steward sealed.
fn witness(mesh: &mut Mesh, body: &str) -> (u16, &'static str, Value) {
let parsed: Result<(Value, Vec<u8>), String> = serde_json::from_str::<Value>(body)
.map_err(|e| format!("invalid JSON: {e}"))
.and_then(|v| {
let hex_env = v.get("envelope_hex").and_then(Value::as_str).ok_or("no `envelope_hex`")?;
let envelope = hex::decode(hex_env).map_err(|e| format!("`envelope_hex`: {e}"))?;
let receipt = v.get("receipt").cloned().ok_or("no `receipt`")?;
Ok((receipt, envelope))
});
let (receipt, envelope) = match parsed {
Ok(p) => p,
Err(e) => return (400, "Bad Request", error_json(&e)),
};
match mesh.witness_wire(&receipt, &envelope) {
Ok(()) => (200, "OK", json!({ "held": true })),
Err(e @ mesh_core::MeshError::JournalCut { .. }) => (409, "Conflict", error_json(&OpenAiError::Serve(e).to_string())),
Err(e) => (400, "Bad Request", error_json(&e.to_string())),
}
}
fn write_json(stream: &mut TcpStream, code: u16, reason: &str, body: &Value) {
let payload = serde_json::to_string(body).unwrap_or_else(|_| "{}".to_string());
let resp = format!(
@ -242,6 +290,90 @@ mod tests {
}
}
/// One HTTP exchange: the status code and the JSON body.
fn call(addr: std::net::SocketAddr, method: &str, path: &str, body: &str, declared: Option<usize>) -> (u16, serde_json::Value) {
let mut conn = TcpStream::connect(addr).unwrap();
let req = format!(
"{method} {path} HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
declared.unwrap_or(body.len()),
);
conn.write_all(req.as_bytes()).unwrap();
// Nothing more is coming, so a server waiting on a declared length
// reads to the end rather than waiting forever.
conn.shutdown(std::net::Shutdown::Write).unwrap();
let mut raw = String::new();
conn.read_to_string(&mut raw).unwrap();
let code = raw[9..12].parse().unwrap();
let json = serde_json::from_str(raw.split("\r\n\r\n").nth(1).unwrap_or("null")).unwrap_or(serde_json::Value::Null);
(code, json)
}
/// The whole grant flow over HTTP: learn the steward's identity, issue a
/// grant to it over exactly the request the body determines, present it in
/// the body, then show the receipt back as a witness.
#[test]
fn a_grant_rides_over_http_and_its_receipt_witnesses_the_journal() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let server = thread::spawn(move || {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 4_800_000);
let mut mesh = Mesh::new(&meter, SigningKey::from_bytes(&[8u8; 32]), "did:example:mesh-openai-srv");
mesh.register(
BackendSpec::new("tiny", "ssm-1b", Fidelity::F2, 50_000),
Box::new(ReferenceEngine::new("tiny", "ssm-1b", Fidelity::F2, 50_000, 32)),
);
super::serve_n(&listener, &mut mesh, &StubBackend, 100_000_000, 5).unwrap();
});
let (code, steward) = call(addr, "GET", "/v1/mesh/steward", "", None);
assert_eq!(code, 200);
let holder = jcp_schema::Did::parse(steward["did"].as_str().unwrap()).unwrap();
let mut body = serde_json::json!({
"model": "gpt-mini",
"messages": [{ "role": "user", "content": "hi" }],
"max_tokens": 16,
"metadata": { "joule_ceiling_uj": 50_000_000 }
});
let req = crate::granted_serve_request(&body).unwrap();
let caller = SigningKey::from_bytes(&[42u8; 32]);
let grant = jcp_grant::issue(
&caller,
jcp_schema::Grant {
issuer: jcp_schema::Did::from_verifying_key(&caller.verifying_key()),
subject: holder,
capability: jcp_schema::Capability::new(req.id(), [mesh_core::INFER_ACTION]),
budget_uj: 1_000_000_000,
budget_funds: None,
caveats: Vec::new(),
expiry: 0,
parent: None,
delegate_depth: None,
nonce: [1; 16],
},
)
.unwrap();
let presented = mesh_core::Presentation::sign(vec![grant.clone()], &req, &caller, [3; 16]);
body[crate::PRESENTATION_FIELD] = serde_json::to_value(&presented).unwrap();
let (code, resp) = call(addr, "POST", "/v1/chat/completions", &body.to_string(), None);
assert_eq!(code, 200, "{resp}");
let energy = &resp["usage"]["energy"];
assert_eq!(energy["receipt"]["grant_id"], grant.grant.id().to_hex());
let witness = serde_json::json!({ "receipt": energy["receipt"], "envelope_hex": energy["envelope_hex"] });
let (code, held) = call(addr, "POST", "/v1/mesh/witness", &witness.to_string(), None);
assert_eq!((code, &held["held"]), (200, &serde_json::json!(true)), "{held}");
let mut tampered = witness.clone();
tampered["receipt"]["sequence"] = serde_json::json!(9);
let (code, _) = call(addr, "POST", "/v1/mesh/witness", &tampered.to_string(), None);
assert_eq!(code, 400, "a receipt the envelope does not seal");
let (code, _) = call(addr, "POST", "/v1/chat/completions", "{}", Some(super::MAX_BODY + 1));
assert_eq!(code, 413, "a body over the limit is refused unread");
server.join().unwrap();
}
#[test]
fn http_roundtrip_returns_sealed_receipt() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();