Skip to content

Commit df15a56

Browse files
authored
Merge pull request #17 from commandlayer/codex/implement-phase-2-with-wrapper-model
[runtime] wrap canonical receipts in runtime metadata envelope
2 parents 1d85818 + b246124 commit df15a56

9 files changed

Lines changed: 143 additions & 100 deletions

File tree

README.md

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ Reference Node.js runtime for CommandLayer Commons verbs. This service executes
55
## What this service does
66

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

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

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

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

6969
## Example flow
7070

71-
### Request a fetch receipt
71+
### Request a fetch receipt wrapper
7272

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

8585
printf '%s\n' "$RECEIPT" | jq .
86+
87+
# canonical receipt only
88+
printf '%s\n' "$RECEIPT" | jq '.receipt'
8689
```
8790

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

107+
## Response boundary
108+
109+
Verb endpoints now return:
110+
111+
```json
112+
{
113+
"receipt": { "...": "signed canonical receipt" },
114+
"runtime_metadata": {
115+
"trace": { "...": "optional" },
116+
"actor": { "...": "optional" },
117+
"delegation_result": { "...": "optional" }
118+
}
119+
}
120+
```
121+
122+
Treat `receipt` as the verifiable payload. `runtime_metadata` is runtime-only context and is not part of the signed canonical receipt.
123+
124+
`POST /verify` accepts either a bare canonical receipt or the wrapped response above and will extract `.receipt` automatically when present.
125+
104126
## Verification semantics
105127

106128
`POST /verify` supports query flags:
107129

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

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

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

150172
```bash
151173
# mint
152-
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}'
174+
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}'
153175

154176
# verify env
155177
curl -s -X POST http://localhost:8080/verify -H "Content-Type: application/json" --data-binary @receipt.json | jq .

runtime/tests/runtime-signing.test.mjs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ async function startServer(extraEnv) {
4949
throw new Error(`server did not boot: ${stderr}`);
5050
}
5151

52+
53+
function unwrapReceiptResponse(payload) {
54+
return { receipt: payload?.receipt || payload, runtimeMetadata: payload?.runtime_metadata || null };
55+
}
56+
5257
async function stop(proc) {
5358
if (proc.exitCode !== null) return;
5459
proc.kill("SIGTERM");
@@ -91,7 +96,9 @@ test("private PEM_B64 + public raw32 b64 path signs and /verify roundtrip works"
9196
body: JSON.stringify(body),
9297
});
9398
assert.equal(receiptResp.status, 200);
94-
const receipt = await receiptResp.json();
99+
const response = await receiptResp.json();
100+
const { receipt, runtimeMetadata } = unwrapReceiptResponse(response);
101+
assert.equal(runtimeMetadata?.trace?.provider, "runtime");
95102
assert.ok(receipt.metadata?.proof?.signature_b64);
96103
assert.ok(receipt.metadata?.proof?.hash_sha256);
97104
assert.equal(receipt.metadata?.proof?.signer_id, "runtime.commandlayer.eth");
@@ -100,12 +107,22 @@ test("private PEM_B64 + public raw32 b64 path signs and /verify roundtrip works"
100107
const verifyResp = await fetch(`${srv.base}/verify`, {
101108
method: "POST",
102109
headers: { "content-type": "application/json" },
103-
body: JSON.stringify(receipt),
110+
body: JSON.stringify(response),
104111
});
105112
const verifyJson = await verifyResp.json();
106113
assert.equal(verifyResp.status, 200);
107114
assert.equal(verifyJson.ok, true);
108115
assert.equal(verifyJson.verified_with, "env");
116+
117+
const verifyBareResp = await fetch(`${srv.base}/verify`, {
118+
method: "POST",
119+
headers: { "content-type": "application/json" },
120+
body: JSON.stringify(receipt),
121+
});
122+
const verifyBareJson = await verifyBareResp.json();
123+
assert.equal(verifyBareResp.status, 200);
124+
assert.equal(verifyBareJson.ok, true);
125+
assert.equal(verifyBareJson.verified_with, "env");
109126
} finally {
110127
await stop(srv.proc);
111128
}
@@ -131,18 +148,19 @@ test("/verify?ens=1 passes with mocked ENS TXT response", async () => {
131148
input: { subject: "t", detail_level: "short" },
132149
trace: { provider: "test" },
133150
};
134-
const receipt = await (
151+
const response = await (
135152
await fetch(`${srv.base}/describe/v1.0.0`, {
136153
method: "POST",
137154
headers: { "content-type": "application/json" },
138155
body: JSON.stringify(body),
139156
})
140157
).json();
158+
const { receipt } = unwrapReceiptResponse(response);
141159

142160
const verifyResp = await fetch(`${srv.base}/verify?ens=1`, {
143161
method: "POST",
144162
headers: { "content-type": "application/json" },
145-
body: JSON.stringify(receipt),
163+
body: JSON.stringify(response),
146164
});
147165
const verifyJson = await verifyResp.json();
148166
assert.equal(verifyResp.status, 200);

scripts/smoke.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,17 @@ try {
2323
headers: { "content-type": "application/json" },
2424
body: JSON.stringify(input),
2525
});
26-
const receipt = await describeResp.json();
27-
if (!describeResp.ok) fail("/describe/v1.0.0 http", receipt);
26+
const response = await describeResp.json();
27+
const receipt = response?.receipt || response;
28+
if (!describeResp.ok) fail("/describe/v1.0.0 http", response);
2829
if (!receipt?.metadata?.proof?.hash_sha256 || !receipt?.metadata?.proof?.signature_b64) {
29-
fail("describe proof fields", receipt?.metadata?.proof || receipt);
30+
fail("describe proof fields", receipt?.metadata?.proof || response);
3031
}
3132

3233
const verifyResp = await fetch(`${base}/verify?schema=0&ens=0`, {
3334
method: "POST",
3435
headers: { "content-type": "application/json" },
35-
body: JSON.stringify(receipt),
36+
body: JSON.stringify(response),
3637
});
3738
const verify = await verifyResp.json();
3839
if (!verifyResp.ok) fail("/verify http", verify);

server.mjs

Lines changed: 30 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -872,12 +872,10 @@ function startWarmWorker() {
872872
// -----------------------
873873
// receipts (runtime-core: single source of truth)
874874
// -----------------------
875-
function makeReceipt({ x402, trace, result, status = "success", error = null, delegation_result = null, actor = null }) {
875+
function makeReceipt({ x402, result, status = "success", error = null }) {
876876
let receipt = {
877877
status,
878878
x402,
879-
trace,
880-
...(delegation_result ? { delegation_result } : {}),
881879
...(error ? { error } : {}),
882880
...(status === "success" ? { result } : {}),
883881
metadata: {
@@ -893,8 +891,6 @@ function makeReceipt({ x402, trace, result, status = "success", error = null, de
893891
},
894892
};
895893

896-
if (actor) receipt.metadata.actor = actor;
897-
898894
const privPem = getActivePrivatePem();
899895
if (!privPem) throw new Error("Missing/invalid private key");
900896

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

917-
function normalizeReceiptForRuntimeCoreVerify(receipt) {
918-
const cloned = JSON.parse(JSON.stringify(receipt || {}));
913+
function wrapReceiptResponse(receipt, { trace = null, actor = null, delegation_result = null } = {}) {
914+
const runtime_metadata = {
915+
...(trace ? { trace } : {}),
916+
...(actor ? { actor } : {}),
917+
...(delegation_result ? { delegation_result } : {}),
918+
};
919+
920+
return {
921+
receipt,
922+
...(Object.keys(runtime_metadata).length ? { runtime_metadata } : {}),
923+
};
924+
}
925+
926+
function extractReceiptPayload(payload) {
927+
if (payload && typeof payload === "object" && payload.receipt && typeof payload.receipt === "object") {
928+
return payload.receipt;
929+
}
930+
return payload;
931+
}
932+
933+
function normalizeReceiptForRuntimeCoreVerify(payload) {
934+
const cloned = JSON.parse(JSON.stringify(extractReceiptPayload(payload) || {}));
919935
if (cloned?.metadata?.proof && typeof cloned.metadata.proof === "object") {
920936
const proof = cloned.metadata.proof;
921937
if (!proof.canonical && proof.canonical_id) proof.canonical = proof.canonical_id;
@@ -1351,18 +1367,12 @@ async function handleVerb(verb, req, res) {
13511367

13521368

13531369
try {
1354-
const receipt = makeReceipt({ x402, trace, result, status: "success", actor });
1355-
return res.json(receipt);
1370+
const receipt = makeReceipt({ x402, result, status: "success" });
1371+
return res.json(wrapReceiptResponse(receipt, { trace, actor }));
13561372
} catch (signErr) {
13571373
return respondSigningError(res, signErr);
13581374
}
13591375

1360-
1361-
const receipt = makeReceipt({ x402, trace, result, status: "success", actor });
1362-
1363-
1364-
return res.json(receipt);
1365-
13661376
} catch (e) {
13671377
const x402 = req.body?.x402 || { verb, version: "1.0.0", entry: `x402://${verb}agent.eth/${verb}/v1.0.0` };
13681378

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

13831393

13841394
try {
1385-
const receipt = makeReceipt({ x402, trace, status: "error", error: err, actor });
1386-
return res.status(500).json(receipt);
1395+
const receipt = makeReceipt({ x402, status: "error", error: err });
1396+
return res.status(500).json(wrapReceiptResponse(receipt, { trace, actor }));
13871397
} catch (signErr) {
13881398
return respondSigningError(res, signErr);
13891399
}
13901400

1391-
1392-
let receipt;
1393-
try {
1394-
receipt = makeReceipt({ x402, trace, status: "error", error: err, actor });
1395-
} catch (e2) {
1396-
receipt = {
1397-
status: "error",
1398-
x402,
1399-
trace,
1400-
error: err,
1401-
metadata: {
1402-
proof: {
1403-
alg: "ed25519-sha256",
1404-
canonical: CANONICAL_ID_SORTED_KEYS_V1,
1405-
signer_id: SIGNER_ID,
1406-
kid: SIGNER_KID,
1407-
hash_sha256: null,
1408-
signature_b64: null,
1409-
note: "unsigned_error_receipt",
1410-
},
1411-
receipt_id: "",
1412-
...(actor ? { actor } : {}),
1413-
},
1414-
};
1415-
}
1416-
1417-
return res.status(500).json(receipt);
1418-
14191401
}
14201402
}
14211403

@@ -1601,7 +1583,8 @@ app.post("/debug/prewarm", requireDebug, (req, res) => {
16011583
// verify endpoint (signature/hash + optional schema + optional ENS binding)
16021584
// -----------------------
16031585
app.post("/verify", async (req, res) => {
1604-
const receipt = req.body;
1586+
const verifyInput = req.body;
1587+
const receipt = extractReceiptPayload(verifyInput);
16051588

16061589
const wantEns = String(req.query.ens || "0") === "1";
16071590
const strictKid = String(req.query.strict_kid || "0") === "1";
@@ -1775,7 +1758,7 @@ app.post("/verify", async (req, res) => {
17751758
schemaOk = false;
17761759
schemaErrors = [{ message: "validator_missing" }];
17771760
} else {
1778-
const ok = validate(receipt);
1761+
const ok = validate(runtimeCoreReceipt);
17791762
schemaOk = !!ok;
17801763
if (!ok) schemaErrors = ajvErrorsToSimple(validate.errors) || [{ message: "schema validation failed" }];
17811764
}

tests/fixtures/golden.public.pem

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
-----BEGIN PUBLIC KEY-----
2-
MCowBQYDK2VwAyEAsYjUeG5oO/BTxwyd7U+BPcuwp3dBu51AAiv3DQNczv8=
2+
MCowBQYDK2VwAyEAIL4DH52qyRXv6DYEA253pjohH/l6Slr1cmtP/uJYGnQ=
33
-----END PUBLIC KEY-----

tests/fixtures/golden.receipt.json

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
11
{
2-
"status": "success",
3-
"x402": {
4-
"verb": "describe",
5-
"version": "1.0.0",
6-
"entry": "x402://describeagent.eth/describe/v1.0.0"
7-
},
8-
"trace": {
9-
"provider": "golden"
10-
},
11-
"result": {
12-
"description": "golden",
13-
"bullets": [
14-
"a",
15-
"b",
16-
"c"
17-
],
18-
"properties": {
2+
"receipt": {
3+
"status": "success",
4+
"x402": {
195
"verb": "describe",
20-
"version": "1.0.0"
6+
"version": "1.0.0",
7+
"entry": "x402://describeagent.eth/describe/v1.0.0"
8+
},
9+
"result": {
10+
"description": "golden",
11+
"bullets": [
12+
"a",
13+
"b",
14+
"c"
15+
],
16+
"properties": {
17+
"verb": "describe",
18+
"version": "1.0.0"
19+
}
20+
},
21+
"metadata": {
22+
"proof": {
23+
"alg": "ed25519-sha256",
24+
"canonical": "json.sorted_keys.v1",
25+
"signer_id": "runtime.commandlayer.eth",
26+
"kid": "v1",
27+
"hash_sha256": "8deae11dc363a4d43164a5e6778cc09ca92079b739185f261c656bb4852b7d96",
28+
"signature_b64": "l8rytZcttuB/McrYrN8/oHZH9ifVnP0teDa8fgWGGwtUq5h9i2wZdU1qW9J0+rseHwzgX1eFIA1AtPzjVkW5BQ=="
29+
},
30+
"receipt_id": "8deae11dc363a4d43164a5e6778cc09ca92079b739185f261c656bb4852b7d96"
2131
}
2232
},
23-
"metadata": {
24-
"proof": {
25-
"alg": "ed25519-sha256",
26-
"canonical": "json.sorted_keys.v1",
27-
"signer_id": "runtime.commandlayer.eth",
28-
"kid": "v1",
29-
"hash_sha256": "b3294386df7b67d507b2995fafa836845bcfae42bea43ccdfe9b73e310ee61ec",
30-
"signature_b64": "60tVtjnMg77ZSA8TWN2TV1qj8B8uV3fHaHNVCC/JscX+wvatwpPMr08F8BWC2MM/81rAGxlZAMTYphQMfQCpCA=="
31-
},
32-
"receipt_id": "b3294386df7b67d507b2995fafa836845bcfae42bea43ccdfe9b73e310ee61ec"
33+
"runtime_metadata": {
34+
"trace": {
35+
"provider": "golden"
36+
}
3337
}
3438
}

0 commit comments

Comments
 (0)