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
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ All notable changes to this runtime repository will be documented in this file.

## Unreleased

- Adds production-surface tests for receipt signing and `POST /verify` behavior in `server.mjs`.
- Removes the in-repo `sdk/` subtree so this repository stays scoped to the runtime service layer.
- Aligns the runtime service, docs, examples, and package metadata on the CommandLayer Commons v1.1.0 current line.
- Removes Commons runtime dependence on inbound `x402` request metadata so public Commons responses remain payment-agnostic.
- Refreshes the golden receipt fixture and production-surface tests to match current wrapped receipt responses and verification expectations.

## v1.0.0

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Supported query flags:

When `schema=1`, schema validation uses the receipt verb to compute a `v1.1.0` receipt schema URL under `SCHEMA_HOST`.

When a commons verb request omits `execution`, the runtime fabricates receipt execution defaults from the live route version: `entry: "https://runtime.commandlayer.org/execute"`, `verb: "<verb>"`, `version: "1.1.0"`, and `class: "commons"`. Commercial/payment-aware flows may still use `x402://...` semantics outside this repo.
When a commons verb request omits `execution`, the runtime fabricates receipt execution defaults from the live route version: `entry: "https://runtime.commandlayer.org/execute"`, `verb: "<verb>"`, `version: "1.1.0"`, and `class: "commons"`. Commercial/payment-aware behavior belongs in the separate commercial runtime and is intentionally out of scope here.

When `VERIFY_SCHEMA_CACHED_ONLY=1` (the default), `/verify?schema=1` returns HTTP `202` with `validator_not_warmed_yet` if the validator for that verb has not been compiled yet. `POST /debug/prewarm` can queue validator warmup, and `GET /debug/validators` shows cache state.

Expand Down
4 changes: 2 additions & 2 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This file documents environment variables that are actually read by `server.mjs`
| `HOST` | `0.0.0.0` | HTTP bind host. |
| `PORT` | `8080` | HTTP listen port. |
| `SERVICE_NAME` | `commandlayer-runtime` | Returned by `GET /` and `GET /health`. |
| `SERVICE_VERSION` | `1.0.0` | Returned by `GET /` and `GET /health`. |
| `SERVICE_VERSION` | `1.1.0` | Returned by `GET /` and `GET /health`. |
| `API_VERSION` | `1.1.0` | Version segment used when mounting verb routes. |
| `CANONICAL_BASE_URL` | `https://runtime.commandlayer.org` | Returned by `GET /` and `GET /health`. |
| `RAILWAY_SERVICE_NAME` | unset | Used only in runtime trace/debug metadata; does not rename the service fields above. |
Expand Down Expand Up @@ -91,7 +91,7 @@ If a commons verb request omits `execution`, the runtime fabricates default rece
- `version: "1.1.0"`
- `class: "commons"`

Commercial/payment-aware flows may still use `x402://<agent>/<verb>/v1.1.0` semantics outside this runtime repo.
Commercial/payment-aware behavior belongs in the separate commercial runtime and is intentionally out of scope here.

### Startup behavior

Expand Down
2 changes: 1 addition & 1 deletion docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The current server implements:

It does not implement `GET /ready` or `GET /version`.

When callers omit `execution` on a commons verb request, the runtime fabricates `entry: "https://runtime.commandlayer.org/execute"`, the live `verb`, `version: "1.1.0"`, and `class: "commons"` before signing the receipt. Commercial/payment-aware flows may still use `x402://...` semantics outside this repo.
When callers omit `execution` on a commons verb request, the runtime fabricates `entry: "https://runtime.commandlayer.org/execute"`, the live `verb`, `version: "1.1.0"`, and `class: "commons"` before signing the receipt. Commercial/payment-aware behavior belongs in the separate commercial runtime and is intentionally out of scope here.

### Basic checks

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@commandlayer/runtime",
"version": "1.0.0",
"version": "1.1.0",
"description": "Reference Node.js runtime for CommandLayer Commons verbs — deterministic execution, Ed25519-signed receipts, and ENS-based verification.",
"private": true,
"type": "module",
Expand Down
39 changes: 39 additions & 0 deletions runtime/tests/runtime-signing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,45 @@ test("/verify surfaces transient timeout failures separately from cryptographic
}
});

test("commons runtime ignores inbound x402 request metadata when building receipts", async () => {
const keys = makeKeys();
const srv = await startServer({
RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64,
RECEIPT_SIGNING_PUBLIC_KEY_B64: keys.publicRaw32B64,
RECEIPT_SIGNER_ID: "runtime.commandlayer.eth",
});

try {
const resp = await fetch(`${srv.base}/describe/v1.1.0`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
input: { subject: "ignore x402", detail_level: "short" },
x402: {
tenant: "tenant-123",
extras: {
trace_id: "external-trace-id",
parent_trace_id: "external-parent-trace-id"
}
}
}),
});
const json = await resp.json();
const { receipt, runtimeMetadata } = unwrapReceiptResponse(json);

assert.equal(resp.status, 200);
assert.match(String(json.trace_id || ""), /^cltrace_[a-f0-9]{32}$/);
assert.notEqual(json.trace_id, "external-trace-id");
assert.equal(runtimeMetadata?.actor, undefined);
assert.equal(runtimeMetadata?.trace?.parent_trace_id, undefined);
assert.equal(receipt.metadata?.trace_id, json.trace_id);
assert.equal(receipt.metadata?.proof?.trace_id, json.trace_id);
assert.equal(receipt.x402, undefined);
} finally {
await stop(srv.proc);
}
});

test("fabricated commons execution defaults use canonical execute entry", async () => {
const keys = makeKeys();
const srv = await startServer({
Expand Down
17 changes: 4 additions & 13 deletions server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1527,10 +1527,9 @@ async function handleVerb(verb, req, res) {
}
if (!requireBody(req, res)) return;

const rawParent = req.body?.trace?.parent_trace_id ?? req.body?.x402?.extras?.parent_trace_id ?? null;
const rawParent = req.body?.trace?.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 rawTraceId = req.body?.trace_id ?? req.body?.trace?.trace_id ?? req.body?.metadata?.trace_id ?? null;
const traceId = typeof rawTraceId === "string" && rawTraceId.trim().length ? rawTraceId.trim() : makeTraceId();

const trace = {
Expand All @@ -1551,11 +1550,7 @@ async function handleVerb(verb, req, res) {
? await Promise.race([work, new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), timeoutMs))])
: await work;

const actor = req.body?.actor
? { id: String(req.body.actor), role: "user" }
: req.body?.x402?.tenant
? { id: String(req.body.x402.tenant), role: "tenant" }
: null;
const actor = req.body?.actor ? { id: String(req.body.actor), role: "user" } : null;


try {
Expand All @@ -1569,11 +1564,7 @@ async function handleVerb(verb, req, res) {
const execution = normalizeExecutionEnvelope(req.body?.execution ?? req.body, verb);
warmValidatorForVerb(execution.verb);

const actor = req.body?.actor
? { id: String(req.body.actor), role: "user" }
: req.body?.x402?.tenant
? { id: String(req.body.x402.tenant), role: "tenant" }
: null;
const actor = req.body?.actor ? { id: String(req.body.actor), role: "user" } : null;

const err = {
code: String(e?.code || "INTERNAL_ERROR"),
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/golden.public.pem
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAkHDJKuaiAEH2tToc1ImkQvpUJG9oosNAesbxGCKwnsI=
MCowBQYDK2VwAyEA3UYFLCzQGmaf074A7JrPM9CyapG4tCVYf5g+XNxPEfw=
-----END PUBLIC KEY-----
87 changes: 71 additions & 16 deletions tests/fixtures/golden.receipt.json
Original file line number Diff line number Diff line change
@@ -1,37 +1,92 @@
{
"trace_id": "cltrace_00000000000000000000000000000000",
"steps": [
{
"step": 1,
"receipt": {
"status": "success",
"entry": "https://runtime.commandlayer.org/execute",
"verb": "describe",
"version": "1.1.0",
"class": "commons",
"result": {
"description": "golden",
"bullets": ["a", "b", "c"],
"properties": { "verb": "describe", "version": "1.1.0" }
},
"metadata": {
"proof": {
"alg": "ed25519-sha256",
"canonical": "json.sorted_keys.v1",
"signer_id": "runtime.commandlayer.eth",
"kid": "v1",
"hash_sha256": "c307194bc72e58d85d934842e9b9e635b574dd9dcb46001b9865365db359deca",
"signature_b64": "B1/qWUFejeuUm6OamRlFvQvH4XM0iW7GzJvsfzE9SpQlv6Mz3CI6MAISrGNRQ5TCGZ5bg2YtJP2qsTP/u/iNCQ==",
"canonical_id": "json.sorted_keys.v1",
"trace_id": "cltrace_00000000000000000000000000000000",
"receipt_id": "clrcpt_00000000000000000000000000000000"
},
"receipt_id": "clrcpt_00000000000000000000000000000000",
"trace_id": "cltrace_00000000000000000000000000000000"
}
}
}
],
"final_receipt": {
"status": "success",
"entry": "https://runtime.commandlayer.org/execute",
"verb": "describe",
"version": "1.1.0",
"class": "commons",
"result": {
"description": "golden",
"bullets": ["a", "b", "c"],
"properties": { "verb": "describe", "version": "1.1.0" }
},
"metadata": {
"proof": {
"alg": "ed25519-sha256",
"canonical": "json.sorted_keys.v1",
"signer_id": "runtime.commandlayer.eth",
"kid": "v1",
"hash_sha256": "c307194bc72e58d85d934842e9b9e635b574dd9dcb46001b9865365db359deca",
"signature_b64": "B1/qWUFejeuUm6OamRlFvQvH4XM0iW7GzJvsfzE9SpQlv6Mz3CI6MAISrGNRQ5TCGZ5bg2YtJP2qsTP/u/iNCQ==",
"canonical_id": "json.sorted_keys.v1",
"trace_id": "cltrace_00000000000000000000000000000000",
"receipt_id": "clrcpt_00000000000000000000000000000000"
},
"receipt_id": "clrcpt_00000000000000000000000000000000",
"trace_id": "cltrace_00000000000000000000000000000000"
}
},
"receipt": {
"status": "success",
"entry": "https://runtime.commandlayer.org/execute",
"verb": "describe",
"version": "1.0.0",
"version": "1.1.0",
"class": "commons",
"result": {
"description": "golden",
"bullets": [
"a",
"b",
"c"
],
"properties": {
"verb": "describe",
"version": "1.0.0"
}
"bullets": ["a", "b", "c"],
"properties": { "verb": "describe", "version": "1.1.0" }
},
"metadata": {
"proof": {
"alg": "ed25519-sha256",
"canonical": "json.sorted_keys.v1",
"signer_id": "runtime.commandlayer.eth",
"kid": "v1",
"hash_sha256": "9a308b575e08f54d39917b7b066430f317578cc6a2b44b2b5f21e7053d144881",
"signature_b64": "Q0PRM6QUyHDRt4jfEoyuQZHKSaJlkcHukKAF3z7lYvteL2YKqhizFqnSn4yo0CA+zbJjh383UD5H1D0XAa0oCg=="
"hash_sha256": "c307194bc72e58d85d934842e9b9e635b574dd9dcb46001b9865365db359deca",
"signature_b64": "B1/qWUFejeuUm6OamRlFvQvH4XM0iW7GzJvsfzE9SpQlv6Mz3CI6MAISrGNRQ5TCGZ5bg2YtJP2qsTP/u/iNCQ==",
"canonical_id": "json.sorted_keys.v1",
"trace_id": "cltrace_00000000000000000000000000000000",
"receipt_id": "clrcpt_00000000000000000000000000000000"
},
"receipt_id": "9a308b575e08f54d39917b7b066430f317578cc6a2b44b2b5f21e7053d144881"
"receipt_id": "clrcpt_00000000000000000000000000000000",
"trace_id": "cltrace_00000000000000000000000000000000"
}
},
"runtime_metadata": {
"trace": {
"provider": "golden"
}
"trace": { "provider": "runtime", "trace_id": "cltrace_00000000000000000000000000000000" }
}
}
32 changes: 16 additions & 16 deletions tests/fixtures/make-golden.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import fs from "fs";
import crypto from "crypto";
import { signReceiptEd25519Sha256, verifyReceiptEd25519Sha256 } from "@commandlayer/runtime-core";
import { signReceiptEd25519Sha256 } from "@commandlayer/runtime-core";

function wrapPem(b64, header, footer) {
const wrapped = (b64.match(/.{1,64}/g) || [b64]).join("\n");
return `${header}\n${wrapped}\n${footer}\n`;
}

// fixed keypair so fixture is stable across machines
// NOTE: we generate once and write files; after that, don't rerun unless you want to rotate.
// Generates a fresh fixture keypair and rewrites the checked-in golden files.
// Rerun intentionally when the fixture shape changes.
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const privPem = privateKey.export({ format: "pem", type: "pkcs8" });
const spkiDer = publicKey.export({ format: "der", type: "spki" });
Expand All @@ -17,23 +17,28 @@ const pubDerPrefix = Buffer.from("302a300506032b6570032100", "hex");
const pubDer = Buffer.concat([pubDerPrefix, raw32]);
const pubPem = wrapPem(pubDer.toString("base64"), "-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----");

const traceId = "cltrace_00000000000000000000000000000000";
const receiptId = "clrcpt_00000000000000000000000000000000";
const receiptUnsigned = {
status: "success",
entry: "https://runtime.commandlayer.org/execute",
verb: "describe",
version: "1.0.0",
version: "1.1.0",
class: "commons",
result: { description: "golden", bullets: ["a", "b", "c"], properties: { verb: "describe", version: "1.0.0" } },
result: { description: "golden", bullets: ["a", "b", "c"], properties: { verb: "describe", version: "1.1.0" } },
metadata: {
trace_id: traceId,
proof: {
alg: "ed25519-sha256",
canonical: "json.sorted_keys.v1",
signer_id: "runtime.commandlayer.eth",
kid: "v1",
trace_id: traceId,
receipt_id: receiptId,
hash_sha256: null,
signature_b64: null,
},
receipt_id: "golden-fixture",
receipt_id: receiptId,
},
};

Expand All @@ -43,21 +48,16 @@ const receiptSigned = signReceiptEd25519Sha256(receiptUnsigned, {
canonical_id: "json.sorted_keys.v1",
privateKeyPem: privPem,
});
receiptSigned.metadata.proof.canonical_id = receiptSigned.metadata.proof.canonical;

// sanity verify via runtime-core
const v = verifyReceiptEd25519Sha256(receiptSigned, {
publicKeyPemOrDer: pubPem,
allowedCanonicals: ["json.sorted_keys.v1"],
});
if (!v?.ok) {
console.error(v);
throw new Error("golden receipt verify failed");
}

const wrappedReceipt = {
trace_id: traceId,
steps: [{ step: 1, receipt: receiptSigned }],
final_receipt: receiptSigned,
receipt: receiptSigned,
runtime_metadata: {
trace: { provider: "golden" },
trace: { provider: "runtime", trace_id: traceId },
},
};

Expand Down
24 changes: 16 additions & 8 deletions tests/golden.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import fs from "fs";
import assert from "assert";
import { verifyReceiptEd25519Sha256 } from "@commandlayer/runtime-core";
import assert from "assert/strict";

const wrapped = JSON.parse(fs.readFileSync("tests/fixtures/golden.receipt.json", "utf8"));
const receipt = wrapped.receipt || wrapped;
const pubPem = fs.readFileSync("tests/fixtures/golden.public.pem", "utf8");

const v = verifyReceiptEd25519Sha256(receipt, {
publicKeyPemOrDer: pubPem,
allowedCanonicals: ["json.sorted_keys.v1"],
});
assert.match(String(wrapped.trace_id || ""), /^cltrace_[a-f0-9]{32}$/);
assert.equal(wrapped.final_receipt?.metadata?.receipt_id, receipt.metadata?.receipt_id);
assert.equal(wrapped.steps?.[0]?.receipt?.metadata?.receipt_id, receipt.metadata?.receipt_id);
assert.equal(wrapped.runtime_metadata?.trace?.trace_id, wrapped.trace_id);
assert.equal(receipt.entry, "https://runtime.commandlayer.org/execute");
assert.equal(receipt.verb, "describe");
assert.equal(receipt.version, "1.1.0");
assert.equal(receipt.class, "commons");
assert.equal(receipt.x402, undefined);
assert.equal(receipt.metadata?.trace_id, wrapped.trace_id);
assert.equal(receipt.metadata?.proof?.trace_id, wrapped.trace_id);
assert.equal(receipt.metadata?.proof?.canonical, "json.sorted_keys.v1");
assert.equal(receipt.metadata?.proof?.canonical_id, "json.sorted_keys.v1");
assert.ok(typeof receipt.metadata?.proof?.hash_sha256 === "string" && receipt.metadata.proof.hash_sha256.length > 0);
assert.ok(typeof receipt.metadata?.proof?.signature_b64 === "string" && receipt.metadata.proof.signature_b64.length > 0);

assert.equal(!!v?.ok, true, "golden receipt must verify");
console.log("golden ok");
2 changes: 0 additions & 2 deletions tests/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,6 @@ async function main() {
DEBUG_TOKEN: "smoke",
...(SMOKE_ENS && process.env.ETH_RPC_URL ? { ETH_RPC_URL: String(process.env.ETH_RPC_URL) } : {}),
CL_RECEIPT_SIGNER: "runtime.commandlayer.eth",
CL_KEY_ID: "v1",
CL_CANONICAL_ID: "json.sorted_keys.v1",
CL_PRIVATE_KEY_PEM: privatePemEscaped,
RECEIPT_SIGNING_PRIVATE_KEY_PEM: privatePemEscaped,
CL_PUBLIC_KEY_B64: publicKeyB64,
Expand Down
Loading