Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ Verb routes return a JSON object with a signed `receipt` and optional unsigned `

```json
{
"trace_id": "cltrace_...",
"steps": [{ "step": 1, "receipt": { "...": "signed receipt" } }],
"final_receipt": { "...": "same signed receipt" },
"receipt": { "...": "signed receipt" },
"runtime_metadata": {
"trace": { "...": "optional" },
"trace": { "trace_id": "cltrace_...", "...": "optional" },
"actor": { "...": "optional" },
"delegation_result": { "...": "optional" }
}
Expand All @@ -60,6 +63,8 @@ Verb routes return a JSON object with a signed `receipt` and optional unsigned `

The signed receipt is produced by `@commandlayer/runtime-core`. The runtime sets proof fields under `receipt.metadata.proof`, including:

- `trace_id`
- `receipt_id`
- `alg`
- `canonical`
- `signer_id`
Expand Down
35 changes: 32 additions & 3 deletions runtime/tests/runtime-signing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ test("makeReceipt production path emits signed receipts with runtime kid and can
const { receipt, runtimeMetadata } = unwrapReceiptResponse(response);

assert.equal(runtimeMetadata?.trace?.provider, "runtime");
assert.match(response?.trace_id || "", /^cltrace_[a-f0-9]{32}$/);
assert.equal(response?.trace_id, runtimeMetadata?.trace?.trace_id);
assert.equal(response?.final_receipt?.metadata?.receipt_id, receipt.metadata?.receipt_id);
assert.equal(response?.steps?.[0]?.receipt?.metadata?.receipt_id, receipt.metadata?.receipt_id);
assert.equal(receipt.metadata?.trace_id, response?.trace_id);
assert.equal(receipt.metadata?.proof?.trace_id, response?.trace_id);
assert.match(receipt.metadata?.receipt_id || "", /^clrcpt_[a-f0-9]{32}$/);
assert.equal(receipt.metadata?.proof?.receipt_id, receipt.metadata?.receipt_id);
assert.equal(receipt.metadata?.proof?.alg, "ed25519-sha256");
assert.equal(receipt.metadata?.proof?.signer_id, "runtime.commandlayer.eth");
assert.equal(receipt.metadata?.proof?.kid, keys.kid);
Expand Down Expand Up @@ -442,7 +450,7 @@ test("full chain clean -> summarize -> classify verifies with schema using parti
SCHEMA_VALIDATE_BUDGET_MS: "3000",
});

async function runVerb(base, verb, content) {
async function runVerb(base, verb, content, traceId = null) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

Expand All @@ -453,6 +461,7 @@ test("full chain clean -> summarize -> classify verifies with schema using parti
body: JSON.stringify({
x402: { verb, version: "1.1.0" },
input: { content },
...(traceId ? { trace: { trace_id: traceId } } : {}),
}),
signal: controller.signal,
});
Expand Down Expand Up @@ -508,19 +517,36 @@ test("full chain clean -> summarize -> classify verifies with schema using parti
const clean = await runVerb(srv.base, "clean", source);
console.log("[chain] after clean response");
assert.ok(clean?.receipt?.result?.cleaned_content, "clean step missing cleaned_content");
assert.match(clean?.trace_id || "", /^cltrace_[a-f0-9]{32}$/);
assert.match(clean?.receipt?.metadata?.receipt_id || "", /^clrcpt_[a-f0-9]{32}$/);
const cleanText = clean.receipt.result.cleaned_content;
const traceId = clean.trace_id;
const receiptIds = new Set([clean.receipt.metadata.receipt_id]);

console.log("[chain] before summarize request");
const summarize = await runVerb(srv.base, "summarize", cleanText);
const summarize = await runVerb(srv.base, "summarize", cleanText, traceId);
console.log("[chain] after summarize response");
assert.ok(summarize?.receipt?.result?.summary, "summarize step missing summary");
assert.equal(summarize?.trace_id, traceId);
assert.equal(summarize?.receipt?.metadata?.trace_id, traceId);
assert.equal(summarize?.receipt?.metadata?.proof?.trace_id, traceId);
assert.match(summarize?.receipt?.metadata?.receipt_id || "", /^clrcpt_[a-f0-9]{32}$/);
receiptIds.add(summarize.receipt.metadata.receipt_id);
const summary = summarize.receipt.result.summary;

console.log("[chain] before classify request");
const classify = await runVerb(srv.base, "classify", summary);
const classify = await runVerb(srv.base, "classify", summary, traceId);
console.log("[chain] after classify response");
assert.ok(classify?.receipt, "classify step missing receipt");
assert.equal(classify?.trace_id, traceId);
assert.equal(classify?.receipt?.metadata?.trace_id, traceId);
assert.equal(classify?.receipt?.metadata?.proof?.trace_id, traceId);
assert.match(classify?.receipt?.metadata?.receipt_id || "", /^clrcpt_[a-f0-9]{32}$/);
receiptIds.add(classify.receipt.metadata.receipt_id);
const finalReceipt = classify.receipt;
assert.equal(receiptIds.size, 3, "each chain step must have a unique receipt_id");
assert.equal(classify?.steps?.[0]?.receipt?.metadata?.receipt_id, finalReceipt.metadata?.receipt_id);
assert.equal(classify?.final_receipt?.metadata?.receipt_id, finalReceipt.metadata?.receipt_id);

assert.equal(finalReceipt.x402.entry, "x402://classifyagent.eth/classify/v1.1.0");

Expand Down Expand Up @@ -570,6 +596,9 @@ test("full chain clean -> summarize -> classify verifies with schema using parti
assert.equal(verifyJson.checks.signature_valid, true);
assert.equal(verifyJson.checks.hash_matches, true);
assert.equal(verifyJson.checks.schema_valid, true);
assert.equal(finalReceipt.metadata?.proof?.signer_id, "runtime.commandlayer.eth");
assert.ok(finalReceipt.metadata?.proof?.hash_sha256);
assert.ok(finalReceipt.metadata?.proof?.signature_b64);
} catch (err) {
console.error("CHAIN FAILURE DEBUG:");
console.error(err);
Expand Down
45 changes: 41 additions & 4 deletions server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1007,22 +1007,33 @@ function startWarmWorker() {
// -----------------------
// receipts (runtime-core: single source of truth)
// -----------------------
function makeReceipt({ x402, result, status = "success", error = null }) {
function makeTraceId() {
return `cltrace_${crypto.randomUUID().replace(/-/g, "")}`;
}

function makeFlowReceiptId() {
return `clrcpt_${crypto.randomUUID().replace(/-/g, "")}`;
}

function makeReceipt({ x402, result, status = "success", error = null, traceId, receiptId }) {
let receipt = {
status,
x402,
...(error ? { error } : {}),
...(status === "success" ? { result } : {}),
metadata: {
...(traceId ? { trace_id: traceId } : {}),
proof: {
alg: "ed25519-sha256",
canonical: runtimeConfig.canonicalId,
signer_id: runtimeConfig.signerId,
kid: runtimeConfig.kid,
...(traceId ? { trace_id: traceId } : {}),
...(receiptId ? { receipt_id: receiptId } : {}),
hash_sha256: null,
signature_b64: null,
},
receipt_id: "",
receipt_id: receiptId || "",
},
};

Expand All @@ -1042,17 +1053,39 @@ function makeReceipt({ x402, result, status = "success", error = null }) {
receipt.metadata.proof.canonical_id = receipt.metadata.proof.canonical;
}

if (receiptId) {
receipt.metadata.receipt_id = receiptId;
if (receipt.metadata?.proof && !receipt.metadata.proof.receipt_id) {
receipt.metadata.proof.receipt_id = receiptId;
}
}

if (traceId) {
receipt.metadata.trace_id = traceId;
if (receipt.metadata?.proof && !receipt.metadata.proof.trace_id) {
receipt.metadata.proof.trace_id = traceId;
}
}

return receipt;
}

function wrapReceiptResponse(receipt, { trace = null, actor = null, delegation_result = null } = {}) {
const traceId = trace?.trace_id || receipt?.metadata?.trace_id || receipt?.metadata?.proof?.trace_id || null;
const runtime_metadata = {
...(trace ? { trace } : {}),
...(actor ? { actor } : {}),
...(delegation_result ? { delegation_result } : {}),
};

return {
...(traceId ? { trace_id: traceId } : {}),
...(traceId
? {
steps: [{ step: 1, receipt }],
final_receipt: receipt,
}
: {}),
receipt,
...(Object.keys(runtime_metadata).length ? { runtime_metadata } : {}),
};
Expand Down Expand Up @@ -1496,8 +1529,12 @@ async function handleVerb(verb, req, res) {

const rawParent = req.body?.trace?.parent_trace_id ?? req.body?.x402?.extras?.parent_trace_id ?? null;
const parentTraceId = typeof rawParent === "string" && rawParent.trim().length ? rawParent.trim() : null;
const rawTraceId =
req.body?.trace_id ?? req.body?.trace?.trace_id ?? req.body?.x402?.extras?.trace_id ?? req.body?.metadata?.trace_id ?? null;
const traceId = typeof rawTraceId === "string" && rawTraceId.trim().length ? rawTraceId.trim() : makeTraceId();

const trace = {
trace_id: traceId,
provider: process.env.RAILWAY_SERVICE_NAME || "runtime",
...(parentTraceId ? { parent_trace_id: parentTraceId } : {}),
};
Expand All @@ -1522,7 +1559,7 @@ async function handleVerb(verb, req, res) {


try {
const receipt = makeReceipt({ x402, result, status: "success" });
const receipt = makeReceipt({ x402, result, status: "success", traceId, receiptId: makeFlowReceiptId() });
return res.json(wrapReceiptResponse(receipt, { trace, actor }));
} catch (signErr) {
return respondSigningError(res, signErr);
Expand All @@ -1548,7 +1585,7 @@ async function handleVerb(verb, req, res) {


try {
const receipt = makeReceipt({ x402, status: "error", error: err });
const receipt = makeReceipt({ x402, status: "error", error: err, traceId, receiptId: makeFlowReceiptId() });
return res.status(500).json(wrapReceiptResponse(receipt, { trace, actor }));
} catch (signErr) {
return respondSigningError(res, signErr);
Expand Down
8 changes: 8 additions & 0 deletions tests/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@ async function main() {
const { receipt, runtimeMetadata } = extractReceiptEnvelope(describe.json);
assert.equal(receipt.status, "success", `describe receipt status must be success`);
assert.equal(runtimeMetadata?.trace?.provider, "runtime", `runtime_metadata.trace.provider must be runtime`);
assert.equal(describe.json?.trace_id, runtimeMetadata?.trace?.trace_id, `top-level trace_id must match runtime metadata`);
assert.ok(/^cltrace_[a-f0-9]{32}$/.test(String(describe.json?.trace_id || "")), `trace_id must use cltrace_ format`);
assert.ok(/^clrcpt_[a-f0-9]{32}$/.test(String(receipt?.metadata?.receipt_id || "")), `receipt_id must use clrcpt_ format`);
assert.equal(receipt?.metadata?.trace_id, describe.json?.trace_id, `receipt metadata.trace_id must match top-level trace_id`);
assert.equal(receipt?.metadata?.proof?.trace_id, describe.json?.trace_id, `receipt proof.trace_id must match top-level trace_id`);
assert.equal(receipt?.metadata?.proof?.receipt_id, receipt?.metadata?.receipt_id, `receipt proof.receipt_id must match metadata.receipt_id`);
assert.equal(describe.json?.steps?.[0]?.receipt?.metadata?.receipt_id, receipt?.metadata?.receipt_id, `steps[0] receipt must mirror signed receipt`);
assert.equal(describe.json?.final_receipt?.metadata?.receipt_id, receipt?.metadata?.receipt_id, `final_receipt must mirror signed receipt`);

const proof = extractProof(receipt);

Expand Down
Loading