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
40 changes: 31 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ Reference Node.js runtime for CommandLayer Commons verbs. This service executes
## What this service does

- Exposes `POST /<verb>/v1.0.0` endpoints for Commons verbs (`fetch`, `describe`, `format`, `clean`, `parse`, `summarize`, `convert`, `explain`, `analyze`, `classify`).
- Returns signed receipts containing:
- deterministic result payloads,
- execution trace metadata,
- proof metadata (`alg`, canonical mode, SHA-256 hash, signature).
- Returns wrapper responses with:
- `receipt`: the signed Commons-compatible canonical receipt,
- `runtime_metadata`: optional runtime-only context such as trace and actor data,
- proof metadata (`alg`, canonical mode, SHA-256 hash, signature) kept inside `receipt.metadata.proof`.
- Exposes `POST /verify` to verify receipt hash/signature, and optionally validate schema + fetch public key from ENS.
- Includes schema validator caching, warmup queueing, SSRF protections for `fetch`, and runtime safety budgets.

Expand All @@ -18,7 +18,7 @@ Reference Node.js runtime for CommandLayer Commons verbs. This service executes

- `GET /` — service index with links and enabled verbs.
- `GET /health` — process/service health and signer readiness.
- `POST /<verb>/v1.0.0` — execute a single verb and return a signed receipt.
- `POST /<verb>/v1.0.0` — execute a single verb and return a wrapper containing a signed receipt plus optional runtime metadata.
- `POST /verify` — verify receipt integrity/signature; optional schema and ENS verification.

### Debug routes
Expand Down Expand Up @@ -68,7 +68,7 @@ You should see `"ok": true` and `"signer_ok": true`.

## Example flow

### Request a fetch receipt
### Request a fetch receipt wrapper

```bash
RECEIPT=$(curl -s -X POST "http://localhost:8080/fetch/v1.0.0" \
Expand All @@ -83,6 +83,9 @@ RECEIPT=$(curl -s -X POST "http://localhost:8080/fetch/v1.0.0" \
}')

printf '%s\n' "$RECEIPT" | jq .

# canonical receipt only
printf '%s\n' "$RECEIPT" | jq '.receipt'
```

### Verify the receipt locally
Expand All @@ -101,14 +104,33 @@ printf '%s' "$RECEIPT" | curl -s -X POST "http://localhost:8080/verify?ens=1" \
-d @- | jq .
```

## Response boundary

Verb endpoints now return:

```json
{
"receipt": { "...": "signed canonical receipt" },
"runtime_metadata": {
"trace": { "...": "optional" },
"actor": { "...": "optional" },
"delegation_result": { "...": "optional" }
}
}
```

Treat `receipt` as the verifiable payload. `runtime_metadata` is runtime-only context and is not part of the signed canonical receipt.

`POST /verify` accepts either a bare canonical receipt or the wrapped response above and will extract `.receipt` automatically when present.

## Verification semantics

`POST /verify` supports query flags:

- `ens=1` — fetch verifier pubkey from ENS TXT records (`cl.sig.pub`, `cl.sig.canonical`, optional `cl.sig.kid`).
- `strict_kid=1` — when `ens=1` and `cl.sig.kid` exists, require receipt `metadata.proof.kid` to match ENS `cl.sig.kid`.
- `strict_kid=1` — when `ens=1` and `cl.sig.kid` exists, require canonical receipt `metadata.proof.kid` to match ENS `cl.sig.kid`.
- `refresh=1` — bypass ENS cache and refresh lookup.
- `schema=1` — validate receipt against verb schema.
- `schema=1` — validate the canonical receipt against the verb receipt schema.

When `VERIFY_SCHEMA_CACHED_ONLY=1` (default), schema validation is edge-safe:

Expand Down Expand Up @@ -149,7 +171,7 @@ See [`docs/OPERATIONS.md`](docs/OPERATIONS.md) for deployment and runbook guidan

```bash
# mint
curl -s -X POST http://localhost:8080/describe/v1.0.0 -H "Content-Type: application/json" -d '{"x402":{"verb":"describe","version":"1.0.0","entry":"x402://describeagent.eth/describe/v1.0.0"},"input":{"subject":"CommandLayer","detail_level":"short"}}' | tee receipt.json | jq '.metadata.proof | {kid, canonical_id, hash_sha256, signature_b64}'
curl -s -X POST http://localhost:8080/describe/v1.0.0 -H "Content-Type: application/json" -d '{"x402":{"verb":"describe","version":"1.0.0","entry":"x402://describeagent.eth/describe/v1.0.0"},"input":{"subject":"CommandLayer","detail_level":"short"}}' | tee receipt.json | jq '.receipt.metadata.proof | {kid, canonical_id, hash_sha256, signature_b64}'

# verify env
curl -s -X POST http://localhost:8080/verify -H "Content-Type: application/json" --data-binary @receipt.json | jq .
Expand Down
26 changes: 22 additions & 4 deletions runtime/tests/runtime-signing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ async function startServer(extraEnv) {
throw new Error(`server did not boot: ${stderr}`);
}


function unwrapReceiptResponse(payload) {
return { receipt: payload?.receipt || payload, runtimeMetadata: payload?.runtime_metadata || null };
}

async function stop(proc) {
if (proc.exitCode !== null) return;
proc.kill("SIGTERM");
Expand Down Expand Up @@ -91,7 +96,9 @@ test("private PEM_B64 + public raw32 b64 path signs and /verify roundtrip works"
body: JSON.stringify(body),
});
assert.equal(receiptResp.status, 200);
const receipt = await receiptResp.json();
const response = await receiptResp.json();
const { receipt, runtimeMetadata } = unwrapReceiptResponse(response);
assert.equal(runtimeMetadata?.trace?.provider, "runtime");
assert.ok(receipt.metadata?.proof?.signature_b64);
assert.ok(receipt.metadata?.proof?.hash_sha256);
assert.equal(receipt.metadata?.proof?.signer_id, "runtime.commandlayer.eth");
Expand All @@ -100,12 +107,22 @@ test("private PEM_B64 + public raw32 b64 path signs and /verify roundtrip works"
const verifyResp = await fetch(`${srv.base}/verify`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(receipt),
body: JSON.stringify(response),
});
const verifyJson = await verifyResp.json();
assert.equal(verifyResp.status, 200);
assert.equal(verifyJson.ok, true);
assert.equal(verifyJson.verified_with, "env");

const verifyBareResp = await fetch(`${srv.base}/verify`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(receipt),
});
const verifyBareJson = await verifyBareResp.json();
assert.equal(verifyBareResp.status, 200);
assert.equal(verifyBareJson.ok, true);
assert.equal(verifyBareJson.verified_with, "env");
} finally {
await stop(srv.proc);
}
Expand All @@ -131,18 +148,19 @@ test("/verify?ens=1 passes with mocked ENS TXT response", async () => {
input: { subject: "t", detail_level: "short" },
trace: { provider: "test" },
};
const receipt = await (
const response = await (
await fetch(`${srv.base}/describe/v1.0.0`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
})
).json();
const { receipt } = unwrapReceiptResponse(response);

const verifyResp = await fetch(`${srv.base}/verify?ens=1`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(receipt),
body: JSON.stringify(response),
});
const verifyJson = await verifyResp.json();
assert.equal(verifyResp.status, 200);
Expand Down
9 changes: 5 additions & 4 deletions scripts/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ try {
headers: { "content-type": "application/json" },
body: JSON.stringify(input),
});
const receipt = await describeResp.json();
if (!describeResp.ok) fail("/describe/v1.0.0 http", receipt);
const response = await describeResp.json();
const receipt = response?.receipt || response;
if (!describeResp.ok) fail("/describe/v1.0.0 http", response);
if (!receipt?.metadata?.proof?.hash_sha256 || !receipt?.metadata?.proof?.signature_b64) {
fail("describe proof fields", receipt?.metadata?.proof || receipt);
fail("describe proof fields", receipt?.metadata?.proof || response);
}

const verifyResp = await fetch(`${base}/verify?schema=0&ens=0`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(receipt),
body: JSON.stringify(response),
});
const verify = await verifyResp.json();
if (!verifyResp.ok) fail("/verify http", verify);
Expand Down
77 changes: 30 additions & 47 deletions server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -872,12 +872,10 @@ function startWarmWorker() {
// -----------------------
// receipts (runtime-core: single source of truth)
// -----------------------
function makeReceipt({ x402, trace, result, status = "success", error = null, delegation_result = null, actor = null }) {
function makeReceipt({ x402, result, status = "success", error = null }) {
let receipt = {
status,
x402,
trace,
...(delegation_result ? { delegation_result } : {}),
...(error ? { error } : {}),
...(status === "success" ? { result } : {}),
metadata: {
Expand All @@ -893,8 +891,6 @@ function makeReceipt({ x402, trace, result, status = "success", error = null, de
},
};

if (actor) receipt.metadata.actor = actor;

const privPem = getActivePrivatePem();
if (!privPem) throw new Error("Missing/invalid private key");

Expand All @@ -914,8 +910,28 @@ function makeReceipt({ x402, trace, result, status = "success", error = null, de
return receipt;
}

function normalizeReceiptForRuntimeCoreVerify(receipt) {
const cloned = JSON.parse(JSON.stringify(receipt || {}));
function wrapReceiptResponse(receipt, { trace = null, actor = null, delegation_result = null } = {}) {
const runtime_metadata = {
...(trace ? { trace } : {}),
...(actor ? { actor } : {}),
...(delegation_result ? { delegation_result } : {}),
};

return {
receipt,
...(Object.keys(runtime_metadata).length ? { runtime_metadata } : {}),
};
}

function extractReceiptPayload(payload) {
if (payload && typeof payload === "object" && payload.receipt && typeof payload.receipt === "object") {
return payload.receipt;
}
return payload;
}

function normalizeReceiptForRuntimeCoreVerify(payload) {
const cloned = JSON.parse(JSON.stringify(extractReceiptPayload(payload) || {}));
if (cloned?.metadata?.proof && typeof cloned.metadata.proof === "object") {
const proof = cloned.metadata.proof;
if (!proof.canonical && proof.canonical_id) proof.canonical = proof.canonical_id;
Expand Down Expand Up @@ -1351,18 +1367,12 @@ async function handleVerb(verb, req, res) {


try {
const receipt = makeReceipt({ x402, trace, result, status: "success", actor });
return res.json(receipt);
const receipt = makeReceipt({ x402, result, status: "success" });
return res.json(wrapReceiptResponse(receipt, { trace, actor }));
} catch (signErr) {
return respondSigningError(res, signErr);
}


const receipt = makeReceipt({ x402, trace, result, status: "success", actor });


return res.json(receipt);

} catch (e) {
const x402 = req.body?.x402 || { verb, version: "1.0.0", entry: `x402://${verb}agent.eth/${verb}/v1.0.0` };

Expand All @@ -1382,40 +1392,12 @@ return res.json(receipt);


try {
const receipt = makeReceipt({ x402, trace, status: "error", error: err, actor });
return res.status(500).json(receipt);
const receipt = makeReceipt({ x402, status: "error", error: err });
return res.status(500).json(wrapReceiptResponse(receipt, { trace, actor }));
} catch (signErr) {
return respondSigningError(res, signErr);
}


let receipt;
try {
receipt = makeReceipt({ x402, trace, status: "error", error: err, actor });
} catch (e2) {
receipt = {
status: "error",
x402,
trace,
error: err,
metadata: {
proof: {
alg: "ed25519-sha256",
canonical: CANONICAL_ID_SORTED_KEYS_V1,
signer_id: SIGNER_ID,
kid: SIGNER_KID,
hash_sha256: null,
signature_b64: null,
note: "unsigned_error_receipt",
},
receipt_id: "",
...(actor ? { actor } : {}),
},
};
}

return res.status(500).json(receipt);

}
}

Expand Down Expand Up @@ -1601,7 +1583,8 @@ app.post("/debug/prewarm", requireDebug, (req, res) => {
// verify endpoint (signature/hash + optional schema + optional ENS binding)
// -----------------------
app.post("/verify", async (req, res) => {
const receipt = req.body;
const verifyInput = req.body;
const receipt = extractReceiptPayload(verifyInput);

const wantEns = String(req.query.ens || "0") === "1";
const strictKid = String(req.query.strict_kid || "0") === "1";
Expand Down Expand Up @@ -1775,7 +1758,7 @@ app.post("/verify", async (req, res) => {
schemaOk = false;
schemaErrors = [{ message: "validator_missing" }];
} else {
const ok = validate(receipt);
const ok = validate(runtimeCoreReceipt);
schemaOk = !!ok;
if (!ok) schemaErrors = ajvErrorsToSimple(validate.errors) || [{ message: "schema validation failed" }];
}
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-----
MCowBQYDK2VwAyEAsYjUeG5oO/BTxwyd7U+BPcuwp3dBu51AAiv3DQNczv8=
MCowBQYDK2VwAyEAIL4DH52qyRXv6DYEA253pjohH/l6Slr1cmtP/uJYGnQ=
-----END PUBLIC KEY-----
60 changes: 32 additions & 28 deletions tests/fixtures/golden.receipt.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
{
"status": "success",
"x402": {
"verb": "describe",
"version": "1.0.0",
"entry": "x402://describeagent.eth/describe/v1.0.0"
},
"trace": {
"provider": "golden"
},
"result": {
"description": "golden",
"bullets": [
"a",
"b",
"c"
],
"properties": {
"receipt": {
"status": "success",
"x402": {
"verb": "describe",
"version": "1.0.0"
"version": "1.0.0",
"entry": "x402://describeagent.eth/describe/v1.0.0"
},
"result": {
"description": "golden",
"bullets": [
"a",
"b",
"c"
],
"properties": {
"verb": "describe",
"version": "1.0.0"
}
},
"metadata": {
"proof": {
"alg": "ed25519-sha256",
"canonical": "json.sorted_keys.v1",
"signer_id": "runtime.commandlayer.eth",
"kid": "v1",
"hash_sha256": "8deae11dc363a4d43164a5e6778cc09ca92079b739185f261c656bb4852b7d96",
"signature_b64": "l8rytZcttuB/McrYrN8/oHZH9ifVnP0teDa8fgWGGwtUq5h9i2wZdU1qW9J0+rseHwzgX1eFIA1AtPzjVkW5BQ=="
},
"receipt_id": "8deae11dc363a4d43164a5e6778cc09ca92079b739185f261c656bb4852b7d96"
}
},
"metadata": {
"proof": {
"alg": "ed25519-sha256",
"canonical": "json.sorted_keys.v1",
"signer_id": "runtime.commandlayer.eth",
"kid": "v1",
"hash_sha256": "b3294386df7b67d507b2995fafa836845bcfae42bea43ccdfe9b73e310ee61ec",
"signature_b64": "60tVtjnMg77ZSA8TWN2TV1qj8B8uV3fHaHNVCC/JscX+wvatwpPMr08F8BWC2MM/81rAGxlZAMTYphQMfQCpCA=="
},
"receipt_id": "b3294386df7b67d507b2995fafa836845bcfae42bea43ccdfe9b73e310ee61ec"
"runtime_metadata": {
"trace": {
"provider": "golden"
}
}
}
Loading
Loading