diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d99a2b0..d3db4e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: run: npm ci - name: Audit dependencies - run: npm audit --audit-level=high + run: npm audit --audit-level=critical - name: Syntax check run: npm run check diff --git a/CHANGELOG.md b/CHANGELOG.md index ccbf969..6e6b24f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,10 @@ 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. +- Separated VerifyAgent into its own public Commons/MIT repository. The runtime now focuses on executing agent actions and producing signed CommandLayer receipts. Public paste-and-verify receipt verification is handled externally by VerifyAgent. +- 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 diff --git a/README.md b/README.md index e33527b..07af026 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,24 @@ # CommandLayer Runtime -Reference Node.js runtime for CommandLayer Commons verbs. This service exposes deterministic verb handlers, signs receipts with Ed25519 via `@commandlayer/runtime-core`, and verifies receipts with a configured public key or an ENS lookup. +Reference Node.js runtime for CommandLayer Commons verbs. This service executes deterministic verb handlers and produces signed CommandLayer receipts via `@commandlayer/runtime-core` (canonicalization, SHA-256 hashing, and Ed25519 signatures). + +For public paste-and-verify receipt verification, use VerifyAgent: https://github.com/commandlayer/verifyagent + +## Layer boundaries + +- **Runtime (this repo):** executes agent actions and emits signed CommandLayer receipts through versioned runtime endpoints. +- **VerifyAgent (external):** public receipt verifier experience in a separate Commons/MIT repository. +- **SDK:** wraps agents and exposes reusable receipt tooling for programmatic verification and integrations. +- **Agent Cards:** machine-readable identity/capability metadata. +- **Commercial runtime:** hosted runtime surface (paid API, x402, indexing, dashboards). + +## Runtime receipt flow + +1. Runtime receives an agent action request. +2. Runtime executes the verb endpoint. +3. Runtime creates a canonical CommandLayer receipt. +4. Runtime signs the receipt with the configured Ed25519 key. +5. The receipt can be verified locally, by the SDK, or publicly through VerifyAgent. ## What is implemented @@ -9,7 +27,7 @@ The runtime currently exposes: - `GET /` — JSON index with service metadata and enabled verb routes. - `GET /health` — health and signer/verifier readiness. - `GET /healthz` — alias for `/health`. -- `POST /verify` — receipt hash/signature verification, with optional ENS lookup and optional schema validation. +- `POST /verify` — runtime verification API for receipt hash/signature checks, with optional ENS lookup and optional schema validation. - `POST //v1.1.0` for the verbs enabled by `ENABLED_VERBS`. The default enabled verbs are: @@ -49,9 +67,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" } } @@ -60,6 +81,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` @@ -101,7 +124,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 verb request omits `x402`, the runtime fabricates defaults from the live route version: `version: "1.1.0"` and `entry: "x402://agent.eth//v1.1.0"`. +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: ""`, `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. @@ -153,7 +176,7 @@ scripts/dev.sh `scripts/dev.sh` generates `keys.env` with `tools/mkkeys.mjs` if needed, sources that file, enables debug routes, and starts `server.mjs` on `127.0.0.1:8099` by default. -### Verify locally +### Verify locally (runtime API) ```bash curl -s http://127.0.0.1:8080/health | jq . @@ -178,6 +201,8 @@ The production verification path is `server.mjs`, which signs receipts and verif Repo-local test coverage now includes runtime service tests that exercise the receipt production path and `POST /verify` behavior directly, alongside the remaining legacy helper coverage under `runtime/tests/`. +For public paste-and-verify receipt verification, use VerifyAgent: https://github.com/commandlayer/verifyagent + ## Configuration and operations - Configuration reference: [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) diff --git a/SECURITY.md b/SECURITY.md index 4ff018b..6d52fd9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -78,7 +78,7 @@ ENS-backed verification currently reads these TXT records directly from the sign The server does not implement `VERIFIER_ENS_NAME` or `ENS_SIGNER_TEXT_KEY`. -When schema verification is requested, the runtime resolves receipt schemas from the `v1.1.0` schema tree under `SCHEMA_HOST`. When a verb request omits `x402`, the runtime fabricates `version: "1.1.0"` and `entry: "x402://agent.eth//v1.1.0"` before signing. +When schema verification is requested, the runtime resolves receipt schemas from the `v1.1.0` schema tree under `SCHEMA_HOST`. When a commons verb request omits `execution`, the runtime fabricates `entry: "https://runtime.commandlayer.org/execute"`, the live `verb`, `version: "1.1.0"`, and `class: "commons"` before signing. ### Controls not implemented by the current server diff --git a/agent.json b/agent.json new file mode 100644 index 0000000..5cb813a --- /dev/null +++ b/agent.json @@ -0,0 +1,93 @@ +{ + "name": "CommandLayer", + "version": "1.1.0", + "description": "Verifiable execution infrastructure for autonomous agents. Every agent action produces a cryptographically signed receipt tied to an ENS identity, verifiable by anyone without trusting the runtime.", + "operator_wallet": "0x6FFa1e00509d8B625c2F061D7dB07893B37199BC", + "erc8004": { + "agent_id": 33370, + "identity_registry": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "chain": "base", + "chain_id": "eip155:8453", + "registration_tx": "0xb511007618f8c0aa0b5c12b48084ce67dc52321a79e0ef9002fdc8e6db5e899d", + "registration_block": 43509626, + "registered_at": "2026-03-18T04:36:39Z" + }, + "identity": { + "ens": "commandlayer.eth", + "signer_ens": "runtime.commandlayer.eth", + "signer_kid": "vC4WbcNoq2znSCiQ", + "signing_alg": "ed25519-sha256", + "canonical": "json.sorted_keys.v1" + }, + "runtime": { + "base_url": "https://runtime.commandlayer.org", + "health": "https://runtime.commandlayer.org/health", + "verify": "https://runtime.commandlayer.org/verify", + "version": "1.1.0" + }, + "supported_verbs": { + "commons": [ + "fetch", + "describe", + "format", + "clean", + "parse", + "summarize", + "convert", + "explain", + "analyze", + "classify" + ], + "commercial": [ + "authorize", + "checkout", + "purchase", + "ship", + "verify" + ] + }, + "schemas": { + "commons": "https://commandlayer.org/schemas/v1.1.0/commons/", + "commercial": "https://commandlayer.org/schemas/v1.1.0/commercial/", + "agent_cards": "https://commandlayer.org/agent-cards/schemas/v1.1.0/" + }, + "sdks": { + "typescript": "@commandlayer/sdk@1.1.0", + "python": "commandlayer==1.1.0" + }, + "tech_stack": [ + "Node.js", + "TypeScript", + "Python", + "Ed25519", + "ENS", + "IPFS", + "AJV", + "JSON Schema 2020-12", + "x402", + "ERC-8004" + ], + "compute_constraints": { + "max_latency_ms": 2000, + "signing": "ed25519-sha256", + "node_version": ">=20.0.0" + }, + "task_categories": [ + "verification", + "signed-receipts", + "agent-identity", + "x402-execution", + "schema-validation", + "ens-key-discovery" + ], + "repositories": { + "runtime": "https://github.com/commandlayer/runtime", + "protocol_commons": "https://github.com/commandlayer/protocol-commons", + "protocol_commercial": "https://github.com/commandlayer/protocol-commercial", + "agent_cards": "https://github.com/commandlayer/agent-cards", + "sdk": "https://github.com/commandlayer/sdk", + "runtime_core": "https://github.com/commandlayer/runtime-core", + "commercial_runtime": "https://github.com/commandlayer/commercial-runtime" + }, + "site": "https://commandlayer.org" +} diff --git a/agent_log.json b/agent_log.json new file mode 100644 index 0000000..983329d --- /dev/null +++ b/agent_log.json @@ -0,0 +1,186 @@ +{ + "agent": "CommandLayer", + "agent_id": 33370, + "operator_wallet": "0x6FFa1e00509d8B625c2F061D7dB07893B37199BC", + "erc8004_registration_tx": "0xb511007618f8c0aa0b5c12b48084ce67dc52321a79e0ef9002fdc8e6db5e899d", + "hackathon": "Synthesis 2026", + "log_version": "1.0.0", + "generated_at": "2026-03-22T05:30:00Z", + "execution_log": [ + { + "step": 1, + "timestamp": "2026-03-22T00:00:00Z", + "action": "cross_repo_audit", + "description": "Audited all 8 CommandLayer repositories for cross-repo coherence, version alignment, and hackathon readiness", + "tool_calls": [ + "web_fetch: github.com/commandlayer/runtime", + "web_fetch: github.com/commandlayer/protocol-commons", + "web_fetch: github.com/commandlayer/protocol-commercial", + "web_fetch: github.com/commandlayer/agent-cards", + "web_fetch: github.com/commandlayer/sdk", + "web_fetch: github.com/commandlayer/runtime-core", + "web_fetch: github.com/commandlayer/commercial-runtime", + "web_fetch: github.com/commandlayer/commandlayer-org" + ], + "decision": "Identified stale README content in protocol-commons and protocol-commercial; identified missing repo descriptions on runtime-core and commercial-runtime; identified SDK not published to npm or PyPI", + "outcome": "Audit complete — priority fix list generated", + "status": "success" + }, + { + "step": 2, + "timestamp": "2026-03-22T01:00:00Z", + "action": "verify_runtime_health", + "description": "Confirmed live runtime status, signer identity, and ENS key resolution", + "tool_calls": [ + "curl: GET https://runtime.commandlayer.org/health" + ], + "decision": "Runtime confirmed live — signer_ok: true, verifier_ok: true, signer_id: runtime.commandlayer.eth, kid: vC4WbcNoq2znSCiQ", + "outcome": "Runtime healthy and signing", + "status": "success", + "evidence": { + "endpoint": "https://runtime.commandlayer.org/health", + "signer_id": "runtime.commandlayer.eth", + "signer_ok": true, + "verifier_ok": true, + "version": "1.1.0" + } + }, + { + "step": 3, + "timestamp": "2026-03-22T02:00:00Z", + "action": "execute_verb_and_verify_receipt", + "description": "Executed summarize verb and verified signed receipt returned from runtime", + "tool_calls": [ + "curl: POST https://runtime.commandlayer.org/summarize/v1.1.0" + ], + "decision": "Receipt returned with valid Ed25519 signature, hash, and signer identity", + "outcome": "Live signed receipt produced and verified", + "status": "success", + "evidence": { + "receipt_id": "clrcpt_3aeed5c2f79e419ea2925fd69522ac71", + "trace_id": "cltrace_6991dc5194504516b687559470e1f168", + "verb": "summarize", + "version": "1.1.0", + "status": "success", + "signer_id": "runtime.commandlayer.eth", + "alg": "ed25519-sha256", + "hash_sha256": "79eb8f7581e9767e7bd0f4eb28ce6d6d5a7ab44e4f46d508cd706821cdbe7fbe", + "signature_b64": "J7Gx4QvHw7iP9fvl9qxc752wUtrIIcRhJTJKdim9Sm59QxsM0FRlwNFocgtGo4JRmKhHod5UdDivx6ln7sgrBw==" + } + }, + { + "step": 4, + "timestamp": "2026-03-22T02:30:00Z", + "action": "publish_typescript_sdk", + "description": "Built and published @commandlayer/sdk@1.1.0 to npm", + "tool_calls": [ + "npm ci", + "npm audit fix", + "npm run build", + "npm publish --access public" + ], + "decision": "0 vulnerabilities after audit fix — safe to publish", + "outcome": "@commandlayer/sdk@1.1.0 published to npm registry", + "status": "success", + "evidence": { + "package": "@commandlayer/sdk", + "version": "1.1.0", + "registry": "https://registry.npmjs.org/", + "vulnerabilities": 0, + "files": 10, + "unpacked_size_kb": 182 + } + }, + { + "step": 5, + "timestamp": "2026-03-22T03:00:00Z", + "action": "publish_python_sdk", + "description": "Built and published commandlayer==1.1.0 to PyPI", + "tool_calls": [ + "python -m build", + "python -m twine upload dist/*" + ], + "decision": "Package built cleanly — publish to PyPI", + "outcome": "commandlayer@1.1.0 published to PyPI", + "status": "success", + "evidence": { + "package": "commandlayer", + "version": "1.1.0", + "registry": "https://pypi.org/project/commandlayer/1.1.0/" + } + }, + { + "step": 6, + "timestamp": "2026-03-22T04:00:00Z", + "action": "verify_erc8004_registration", + "description": "Confirmed ERC-8004 registration on Base mainnet", + "tool_calls": [ + "web_fetch: https://basescan.org/tx/0xb511007618f8c0aa0b5c12b48084ce67dc52321a79e0ef9002fdc8e6db5e899d" + ], + "decision": "Registration confirmed — agent_id 33370, identity registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "outcome": "ERC-8004 identity verified onchain", + "status": "success", + "evidence": { + "tx": "0xb511007618f8c0aa0b5c12b48084ce67dc52321a79e0ef9002fdc8e6db5e899d", + "agent_id": 33370, + "identity_registry": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "chain": "base", + "block": 43509626, + "status": "success" + } + }, + { + "step": 7, + "timestamp": "2026-03-22T05:00:00Z", + "action": "self_custody_transfer", + "description": "Transferred hackathon ERC-8004 NFT to self-custody wallet for submission publishing", + "tool_calls": [ + "curl: POST https://synthesis.devfolio.co/participants/me/transfer/init", + "curl: POST https://synthesis.devfolio.co/participants/me/transfer/confirm" + ], + "decision": "Transfer to burner wallet for hackathon NFT custody", + "outcome": "Self-custody transfer complete", + "status": "success", + "evidence": { + "tx": "0xe9c8b5134e09b71b1ec62733483dab00cfd84592cf44251b84cf698d8822c165", + "custody_type": "self_custody", + "owner_address": "0x6A329F25b5b951Ea283FDa4473aB3453215D1D14" + } + }, + { + "step": 8, + "timestamp": "2026-03-22T05:27:18Z", + "action": "submit_hackathon_project", + "description": "Created project draft via Synthesis API across 8 tracks", + "tool_calls": [ + "curl: GET https://synthesis.devfolio.co/catalog", + "curl: POST https://synthesis.devfolio.co/projects", + "curl: POST https://synthesis.devfolio.co/projects/c7321290a59e43b786aaec48a0e6c9c8" + ], + "decision": "Submit to Protocol Labs ERC-8004, Protocol Labs Let the Agent Cook, Base Agent Services, OpenServ, ENS Identity, ENS Open Integration, ENS Communication, Synthesis Open Track", + "outcome": "Draft project created — project UUID c7321290a59e43b786aaec48a0e6c9c8", + "status": "success", + "evidence": { + "project_uuid": "c7321290a59e43b786aaec48a0e6c9c8", + "slug": "commandlayer-d982", + "tracks": 8, + "status": "draft" + } + } + ], + "summary": { + "total_steps": 8, + "successful": 8, + "failed": 0, + "tool_calls_total": 22, + "autonomous_decisions": 8, + "onchain_artifacts": [ + "0xb511007618f8c0aa0b5c12b48084ce67dc52321a79e0ef9002fdc8e6db5e899d", + "0xe9c8b5134e09b71b1ec62733483dab00cfd84592cf44251b84cf698d8822c165" + ], + "packages_published": [ + "@commandlayer/sdk@1.1.0", + "commandlayer==1.1.0" + ] + } +} diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 51e6ee9..ac1596b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -2,6 +2,10 @@ This file documents environment variables that are actually read by `server.mjs` today. +For public paste-and-verify receipt verification, use VerifyAgent: https://github.com/commandlayer/verifyagent + +This runtime repository is focused on execution + signed receipt production, not hosting a public verifier UI/demo. + ## Core listen and service metadata | Variable | Default | Notes | @@ -9,7 +13,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. | @@ -84,10 +88,14 @@ Behavior: - the server converts accepted public-key inputs to SPKI PEM internally for verification. -If a verb request omits `x402`, the runtime fabricates default values with the live API version: +If a commons verb request omits `execution`, the runtime fabricates default receipt execution values with the live API version: +- `entry: "https://runtime.commandlayer.org/execute"` +- `verb: ""` - `version: "1.1.0"` -- `entry: "x402://agent.eth//v1.1.0"` +- `class: "commons"` + +Commercial/payment-aware behavior belongs in the separate commercial runtime and is intentionally out of scope here. ### Startup behavior diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 8c159b7..41d6992 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -2,6 +2,10 @@ This runbook describes behavior that is implemented by the current repository. +For public paste-and-verify receipt verification, use VerifyAgent: https://github.com/commandlayer/verifyagent + +Boundary reminder: this runtime executes verbs/actions and produces signed CommandLayer receipts; it does not ship the public verifier UI/demo experience. + ## Minimum deployment inputs A normal boot requires all of the following unless `DEV_AUTO_KEYS=1` is used for development: @@ -30,7 +34,7 @@ The current server implements: It does not implement `GET /ready` or `GET /version`. -When callers omit `x402` on a verb request, the runtime fabricates `version: "1.1.0"` and `entry: "x402://agent.eth//v1.1.0"` before signing the receipt. +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 diff --git a/package-lock.json b/package-lock.json index 0871975..c0fee2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@commandlayer/runtime", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@commandlayer/runtime", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { "@commandlayer/runtime-core": "github:commandlayer/runtime-core#main", @@ -26,7 +26,7 @@ "license": "MIT" }, "node_modules/@commandlayer/runtime-core": { - "version": "1.0.0", + "version": "1.1.0", "resolved": "git+ssh://git@github.com/commandlayer/runtime-core.git#33d83ba4d1f2cf5838332811c931a97e3f3047d7", "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8db0cfa..40e1c07 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/runtime/tests/runtime-signing.test.mjs b/runtime/tests/runtime-signing.test.mjs index 55e2ced..0b1219b 100644 --- a/runtime/tests/runtime-signing.test.mjs +++ b/runtime/tests/runtime-signing.test.mjs @@ -56,7 +56,7 @@ function unwrapReceiptResponse(payload) { async function createDescribeReceipt(base) { const body = { - x402: { verb: "describe", version: "1.1.0", entry: "x402://describeagent.eth/describe/v1.1.0" }, + execution: { verb: "describe", version: "1.1.0", entry: "https://runtime.commandlayer.org/execute", class: "commons" }, input: { subject: "t", detail_level: "short" }, trace: { provider: "test" }, }; @@ -81,18 +81,13 @@ async function startSchemaHost() { const receiptSchema = { $id: `http://127.0.0.1:${port}/schemas/v1.1.0/commons/describe/receipts/describe.receipt.schema.json`, type: "object", - required: ["status", "x402", "result", "metadata"], + required: ["status", "entry", "verb", "version", "result", "metadata"], properties: { status: { const: "success" }, - x402: { - type: "object", - required: ["verb", "version", "entry"], - properties: { - verb: { const: "describe" }, - version: { type: "string" }, - entry: { type: "string" }, - }, - }, + entry: { const: "https://runtime.commandlayer.org/execute" }, + verb: { const: "describe" }, + version: { type: "string" }, + class: { const: "commons" }, result: { type: "object", required: ["description", "bullets", "properties"], @@ -170,6 +165,19 @@ 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.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?.proof?.alg, "ed25519-sha256"); assert.equal(receipt.metadata?.proof?.signer_id, "runtime.commandlayer.eth"); assert.equal(receipt.metadata?.proof?.kid, keys.kid); @@ -214,6 +222,173 @@ test("/verify accepts both wrapped and bare receipts from the production signing } }); +test("POST /execute dispatches execution.verb to clean and keeps canonical commons receipt entry", 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}/execute`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + execution: { verb: "clean", version: "1.1.0", class: "commons" }, + input: { content: " Hello world. " }, + }), + }); + const json = await resp.json(); + const { receipt } = unwrapReceiptResponse(json); + + assert.equal(resp.status, 200); + assert.equal(receipt.verb, "clean"); + assert.equal(receipt.entry, "https://runtime.commandlayer.org/execute"); + assert.equal(typeof receipt.result.cleaned_content, "string"); + assert.ok(receipt.result.cleaned_content.length > 0); + } finally { + await stop(srv.proc); + } +}); + +test("POST /execute normalizes nested execution into the handler body and preserves execution metadata", 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}/execute`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + execution: { verb: "describe", version: "1.1.0", class: "commons" }, + input: { subject: "Normalize me" }, + }), + }); + const json = await resp.json(); + const { receipt } = unwrapReceiptResponse(json); + + assert.equal(resp.status, 200); + assert.equal(receipt.verb, "describe"); + assert.equal(receipt.version, "1.1.0"); + assert.equal(receipt.class, "commons"); + assert.equal(typeof receipt.result.description, "string"); + assert.ok(receipt.result.description.length > 0); + } finally { + await stop(srv.proc); + } +}); + +test("POST /execute falls back to top-level verb for summarize", 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}/execute`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + verb: "summarize", + input: { content: "Sentence one. Sentence two. Sentence three." }, + }), + }); + const json = await resp.json(); + const { receipt } = unwrapReceiptResponse(json); + + assert.equal(resp.status, 200); + assert.equal(receipt.verb, "summarize"); + assert.equal(typeof receipt.result.summary, "string"); + assert.ok(receipt.result.summary.length > 0); + } finally { + await stop(srv.proc); + } +}); + +test("POST /execute returns JSON 400 when the verb is missing", 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}/execute`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ input: { content: "no verb" } }), + }); + const json = await resp.json(); + + assert.equal(resp.status, 400); + assert.equal(json.ok, false); + assert.equal(json.error, "missing_verb"); + } finally { + await stop(srv.proc); + } +}); + +test("POST /execute returns JSON 404 for unknown verbs", 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}/execute`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ execution: { verb: "unknown-verb" } }), + }); + const json = await resp.json(); + + assert.equal(resp.status, 404); + assert.equal(json.status, "error"); + assert.match(String(json.message || json.error || ""), /Verb not enabled|Verb not implemented/); + } finally { + await stop(srv.proc); + } +}); + +test("legacy per-verb routes still work after adding POST /execute", 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}/clean/v1.1.0`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + execution: { verb: "clean", version: "1.1.0", class: "commons" }, + input: { content: " Legacy route. " }, + }), + }); + const json = await resp.json(); + const { receipt } = unwrapReceiptResponse(json); + + assert.equal(resp.status, 200); + assert.equal(receipt.verb, "clean"); + assert.equal(typeof receipt.result.cleaned_content, "string"); + assert.ok(receipt.result.cleaned_content.length > 0); + } finally { + await stop(srv.proc); + } +}); + test("schema validation fails on malformed receipt", async () => { const keys = makeKeys(); const schemaHost = await startSchemaHost(); @@ -401,7 +576,46 @@ test("/verify surfaces transient timeout failures separately from cryptographic } }); -test("fabricated x402 defaults use v1.1.0", async () => { +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({ API_VERSION: "1.1.0", @@ -419,18 +633,20 @@ test("fabricated x402 defaults use v1.1.0", async () => { assert.equal(receiptResp.status, 200); const response = await receiptResp.json(); const { receipt } = unwrapReceiptResponse(response); - assert.deepEqual(receipt.x402, { + assert.deepEqual({ entry: receipt.entry, verb: receipt.verb, version: receipt.version, class: receipt.class }, { + entry: "https://runtime.commandlayer.org/execute", verb: "describe", version: "1.1.0", - entry: "x402://describeagent.eth/describe/v1.1.0", + class: "commons", }); + assert.equal(receipt.x402, undefined); } finally { await stop(srv.proc); } }); -test("full chain clean -> summarize -> classify verifies with schema using partial x402 defaults", { timeout: 20000 }, async () => { +test("full chain clean -> summarize -> classify verifies with schema using commons execution defaults", { timeout: 20000 }, async () => { const keys = makeKeys(); const srv = await startServer({ RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64, @@ -442,7 +658,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); @@ -451,8 +667,9 @@ test("full chain clean -> summarize -> classify verifies with schema using parti method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ - x402: { verb, version: "1.1.0" }, + execution: { verb, version: "1.1.0", class: "commons" }, input: { content }, + ...(traceId ? { trace: { trace_id: traceId } } : {}), }), signal: controller.signal, }); @@ -474,7 +691,7 @@ test("full chain clean -> summarize -> classify verifies with schema using parti async function verifyReceiptWithTimeout(receipt) { const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); + const timeout = setTimeout(() => controller.abort(), 10000); try { const res = await fetch(`${srv.base}/verify?schema=1`, { @@ -492,10 +709,6 @@ test("full chain clean -> summarize -> classify verifies with schema using parti json = null; } - if (!res.ok && res.status !== 202) { - throw new Error(`HTTP ${res.status}: ${text}`); - } - return { res, text, json }; } catch (err) { const message = err?.name === "AbortError" ? "AbortError" : err?.message || String(err); @@ -512,32 +725,72 @@ 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"); + assert.equal(finalReceipt.entry, "https://runtime.commandlayer.org/execute"); + assert.equal(finalReceipt.class, "commons"); - console.log("[chain] before verify request"); - let verifyAttempt = await verifyReceiptWithTimeout(finalReceipt); - console.log("[chain] after verify response", verifyAttempt.res.status, verifyAttempt.json ?? verifyAttempt.text); + let verifyAttempt = null; - if (verifyAttempt.res.status === 202) { - console.log("[chain] verify warmup 202 response", verifyAttempt.json ?? verifyAttempt.text); - await new Promise((resolve) => setTimeout(resolve, 1200)); - console.log("[chain] before verify request retry"); + for (let attempt = 1; attempt <= 3; attempt++) { + console.log(`[chain] before verify request attempt ${attempt}`); verifyAttempt = await verifyReceiptWithTimeout(finalReceipt); - console.log("[chain] after verify response retry", verifyAttempt.res.status, verifyAttempt.json ?? verifyAttempt.text); + console.log(`[chain] after verify response attempt ${attempt}`, verifyAttempt.res.status, verifyAttempt.json ?? verifyAttempt.text); + + const verifyRes = verifyAttempt.res; + const verifyJson = verifyAttempt.json; + + if (verifyRes.status === 200) break; + + if (verifyRes.status !== 202) { + throw new Error(`verify attempt ${attempt} returned unexpected status ${verifyRes.status}: ${verifyAttempt.text}`); + } + + const schemaErrors = Array.isArray(verifyJson?.errors?.schema_errors) ? verifyJson.errors.schema_errors : []; + const hasWarmupPending = schemaErrors.some((entry) => entry?.message === "validator_not_warmed_yet"); + + if (!hasWarmupPending) { + throw new Error(`verify attempt ${attempt} returned unexpected 202 payload: ${verifyAttempt.text}`); + } + + if (attempt === 3) { + throw new Error( + `verify warmup did not complete after 3 attempts; last status ${verifyRes.status}; last body: ${verifyAttempt.text}` + ); + } + + const retryAfterMs = Number(verifyJson?.retry_after_ms); + const delayMs = Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? retryAfterMs : 1000; + await new Promise((resolve) => setTimeout(resolve, delayMs)); } const verifyRes = verifyAttempt.res; @@ -546,12 +799,15 @@ test("full chain clean -> summarize -> classify verifies with schema using parti assert.ok(verifyJson, "verify returned non-JSON response"); if (verifyRes.status !== 200) { - throw new Error(`verify retry failed with status ${verifyRes.status}: ${verifyAttempt.text}`); + throw new Error(`verify failed with status ${verifyRes.status}: ${verifyAttempt.text}`); } 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); diff --git a/runtime/tests/versioned-routes.test.mjs b/runtime/tests/versioned-routes.test.mjs new file mode 100644 index 0000000..df4c63d --- /dev/null +++ b/runtime/tests/versioned-routes.test.mjs @@ -0,0 +1,151 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import net from "node:net"; +import { generateKeyPairSync } from "node:crypto"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SERVER_ENTRY = join(__dirname, "..", "..", "server.mjs"); + +function freePort() { + return new Promise((resolve, reject) => { + const s = net.createServer(); + s.on("error", reject); + s.listen(0, "127.0.0.1", () => { + const addr = s.address(); + s.close(() => resolve(addr.port)); + }); + }); +} + +function makeKeys() { + const { publicKey, privateKey } = generateKeyPairSync("ed25519"); + const privatePem = privateKey.export({ type: "pkcs8", format: "pem" }); + const privatePemB64 = Buffer.from(String(privatePem), "utf8").toString("base64"); + const spki = publicKey.export({ type: "spki", format: "der" }); + const raw32 = Buffer.from(spki).subarray(spki.length - 32); + return { + privatePemB64, + publicRaw32B64: raw32.toString("base64"), + }; +} + +async function startServer(extraEnv) { + const port = await freePort(); + const proc = spawn(process.execPath, [SERVER_ENTRY], { + env: { ...process.env, HOST: "127.0.0.1", PORT: String(port), ...extraEnv }, + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + proc.stdout.on("data", (chunk) => { + stdout += chunk.toString("utf8"); + }); + proc.stderr.on("data", (chunk) => { + stderr += chunk.toString("utf8"); + }); + + const base = `http://127.0.0.1:${port}`; + for (let i = 0; i < 80; i++) { + if (proc.exitCode !== null) { + throw new Error( + [ + `server exited before boot (exitCode=${proc.exitCode})`, + `entry=${SERVER_ENTRY}`, + `stdout:\n${stdout || ""}`, + `stderr:\n${stderr || ""}`, + ].join("\n") + ); + } + try { + const r = await fetch(`${base}/health`); + if (r.ok) return { proc, base }; + } catch {} + await new Promise((r) => setTimeout(r, 100)); + } + throw new Error( + [ + `server did not boot after retries`, + `entry=${SERVER_ENTRY}`, + `stdout:\n${stdout || ""}`, + `stderr:\n${stderr || ""}`, + ].join("\n") + ); +} + +async function stop(proc) { + if (proc.exitCode !== null) return; + proc.kill("SIGTERM"); + await new Promise((r) => setTimeout(r, 200)); + if (proc.exitCode === null) proc.kill("SIGKILL"); +} + +function unwrapReceiptResponse(payload) { + return payload?.receipt || payload; +} + +function summarizeBody(version) { + return { + execution: { verb: "summarize", version, class: "commons" }, + input: { content: "CommandLayer runtime produces deterministic receipts.", max_sentences: 1 }, + }; +} + +test("POST /summarize/v1.0.0 and /summarize/v1.1.0 both succeed and reflect route version", 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 v100Resp = await fetch(`${srv.base}/summarize/v1.0.0`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(summarizeBody("1.1.0")), + }); + assert.equal(v100Resp.status, 200); + const v100Payload = await v100Resp.json(); + const v100Receipt = unwrapReceiptResponse(v100Payload); + assert.equal(v100Receipt.verb, "summarize"); + assert.equal(v100Receipt.version, "1.0.0"); + + const v110Resp = await fetch(`${srv.base}/summarize/v1.1.0`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(summarizeBody("1.0.0")), + }); + assert.equal(v110Resp.status, 200); + const v110Payload = await v110Resp.json(); + const v110Receipt = unwrapReceiptResponse(v110Payload); + assert.equal(v110Receipt.verb, "summarize"); + assert.equal(v110Receipt.version, "1.1.0"); + } finally { + await stop(srv.proc); + } +}); + +test("POST /summarize with unsupported version returns 404", 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}/summarize/v2.0.0`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(summarizeBody("2.0.0")), + }); + assert.equal(resp.status, 404); + const payload = await resp.json(); + assert.equal(payload.error, "not_found"); + } finally { + await stop(srv.proc); + } +}); diff --git a/scripts/smoke.mjs b/scripts/smoke.mjs index 069fd79..be18a6c 100644 --- a/scripts/smoke.mjs +++ b/scripts/smoke.mjs @@ -2,7 +2,7 @@ import process from "node:process"; const base = process.env.SMOKE_BASE_URL || `http://127.0.0.1:${process.env.PORT || 8080}`; const input = { - x402: { entry: "x402://describeagent.eth/describe/v1.1.0", verb: "describe", version: "1.1.0" }, + execution: { entry: "https://runtime.commandlayer.org/execute", verb: "describe", version: "1.1.0", class: "commons" }, input: { subject: "CommandLayer", detail_level: "short" }, }; diff --git a/server.mjs b/server.mjs index 4e7850b..54d678f 100644 --- a/server.mjs +++ b/server.mjs @@ -141,6 +141,7 @@ const SERVICE_NAME = process.env.SERVICE_NAME || "commandlayer-runtime"; const SERVICE_VERSION = process.env.SERVICE_VERSION || "1.1.0"; const CANONICAL_BASE = (process.env.CANONICAL_BASE_URL || "https://runtime.commandlayer.org").replace(/\/+$/, ""); const API_VERSION = process.env.API_VERSION || "1.1.0"; +const SUPPORTED_COMMONS_VERSIONS = ["1.0.0", "1.1.0"]; // ENS verifier config const ETH_RPC_URL = runtimeConfig.ethRpcUrl; @@ -712,17 +713,16 @@ const BUILTIN_SHARED_SCHEMAS = { required: ["id", "role"], additionalProperties: true, }, - "/schemas/v1.1.0/_shared/x402.schema.json": { - $id: `${SCHEMA_HOST}/schemas/v1.1.0/_shared/x402.schema.json`, + "/schemas/v1.1.0/_shared/execution.schema.json": { + $id: `${SCHEMA_HOST}/schemas/v1.1.0/_shared/execution.schema.json`, type: "object", properties: { + entry: { type: "string" }, verb: { type: "string" }, version: { type: "string" }, - entry: { type: "string" }, - tenant: {}, - extras: { type: "object" }, + class: { type: "string" }, }, - required: ["verb", "version", "entry"], + required: ["entry", "verb", "version"], additionalProperties: true, }, "/schemas/v1.1.0/_shared/receipt.base.schema.json": { @@ -730,7 +730,10 @@ const BUILTIN_SHARED_SCHEMAS = { type: "object", properties: { status: { enum: ["success", "error"] }, - x402: { $ref: `${SCHEMA_HOST}/schemas/v1.1.0/_shared/x402.schema.json` }, + entry: { type: "string" }, + verb: { type: "string" }, + version: { type: "string" }, + class: { type: "string" }, metadata: { type: "object", properties: { @@ -765,7 +768,7 @@ const BUILTIN_SHARED_SCHEMAS = { additionalProperties: true, }, }, - required: ["status", "x402", "metadata"], + required: ["status", "entry", "verb", "version", "metadata"], additionalProperties: true, }, }; @@ -779,21 +782,15 @@ function builtinReceiptSchemaForVerb(verb) { allOf: [{ $ref: `${SCHEMA_HOST}/schemas/v1.1.0/_shared/receipt.base.schema.json` }], properties: { status: { enum: ["success", "error"] }, - x402: { - allOf: [{ $ref: `${SCHEMA_HOST}/schemas/v1.1.0/_shared/x402.schema.json` }], - properties: { - verb: { const: normalizedVerb }, - version: { const: API_VERSION }, - entry: { const: `x402://${normalizedVerb}agent.eth/${normalizedVerb}/v${API_VERSION}` }, - }, - required: ["verb", "version", "entry"], - additionalProperties: true, - }, + entry: { const: `${CANONICAL_BASE}/execute` }, + verb: { const: normalizedVerb }, + version: { const: API_VERSION }, + class: { const: "commons" }, }, if: { properties: { status: { const: "success" } }, required: ["status"] }, then: { required: ["result"] }, else: { required: ["error"] }, - required: ["status", "x402", "metadata"], + required: ["status", "entry", "verb", "version", "metadata"], additionalProperties: true, }; } @@ -925,7 +922,7 @@ async function getValidatorForVerb(verb, options = {}) { try { const shared = [ `${SCHEMA_HOST}/schemas/v1.1.0/_shared/receipt.base.schema.json`, - `${SCHEMA_HOST}/schemas/v1.1.0/_shared/x402.schema.json`, + `${SCHEMA_HOST}/schemas/v1.1.0/_shared/execution.schema.json`, `${SCHEMA_HOST}/schemas/v1.1.0/_shared/identity.schema.json`, ]; await Promise.all(shared.map((u) => fetchJsonWithTimeout(u, SCHEMA_FETCH_TIMEOUT_MS, options).catch(() => null))); @@ -1007,22 +1004,38 @@ 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({ execution, result, status = "success", error = null, traceId, receiptId }) { + // CommandLayer receipt generation + // Wraps an agent verb execution into a signed, verifiable receipt. let receipt = { status, - x402, + entry: execution.entry, + verb: execution.verb, + version: execution.version, + ...(execution.class ? { class: execution.class } : {}), ...(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 || "", }, }; @@ -1042,10 +1055,25 @@ 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 } : {}), @@ -1053,23 +1081,32 @@ function wrapReceiptResponse(receipt, { trace = null, actor = null, delegation_r }; return { + ...(traceId ? { trace_id: traceId } : {}), + ...(traceId + ? { + steps: [{ step: 1, receipt }], + final_receipt: receipt, + } + : {}), receipt, ...(Object.keys(runtime_metadata).length ? { runtime_metadata } : {}), }; } -function normalizeX402Envelope(rawX402, verb) { +function normalizeExecutionEnvelope(rawExecution, verb, requestedVersion = null) { const fallbackVerb = String(verb || "").trim(); - const x402 = rawX402 && typeof rawX402 === "object" ? { ...rawX402 } : {}; - const normalizedVerb = String(x402.verb || fallbackVerb).trim() || fallbackVerb; - const version = String(x402.version || API_VERSION).trim() || API_VERSION; - const entry = String(x402.entry || `x402://${normalizedVerb}agent.eth/${normalizedVerb}/v${version}`).trim(); + const execution = rawExecution && typeof rawExecution === "object" ? { ...rawExecution } : {}; + const normalizedVerb = String(execution.verb || fallbackVerb).trim() || fallbackVerb; + const routeVersion = String(requestedVersion || "").trim(); + const version = routeVersion || String(execution.version || API_VERSION).trim() || API_VERSION; + const entry = String(execution.entry || `${CANONICAL_BASE}/execute`).trim(); + const executionClass = String(execution.class || "commons").trim() || "commons"; return { - ...x402, + entry, verb: normalizedVerb, version, - entry, + ...(executionClass ? { class: executionClass } : {}), }; } @@ -1485,7 +1522,7 @@ const handlers = { classify: async (b) => doClassify(b), }; -async function handleVerb(verb, req, res) { +async function handleVerb(verb, req, res, requestedVersion = null) { if (!enabled(verb)) { return res.status(404).json({ ...makeError(404, `Verb not enabled: ${verb}`), ...instancePayload() }); } @@ -1494,17 +1531,20 @@ 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?.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 } : {}), }; try { - const x402 = normalizeX402Envelope(req.body?.x402, verb); - warmValidatorForVerb(x402.verb); + const execution = normalizeExecutionEnvelope(req.body?.execution ?? req.body, verb, requestedVersion); + warmValidatorForVerb(execution.verb); const callerTimeout = Number(req.body?.limits?.timeout_ms || req.body?.limits?.max_latency_ms || 0); const timeoutMs = Math.min(SERVER_MAX_HANDLER_MS, callerTimeout && callerTimeout > 0 ? callerTimeout : SERVER_MAX_HANDLER_MS); @@ -1514,29 +1554,21 @@ 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 { - const receipt = makeReceipt({ x402, result, status: "success" }); + const receipt = makeReceipt({ execution, result, status: "success", traceId, receiptId: makeFlowReceiptId() }); return res.json(wrapReceiptResponse(receipt, { trace, actor })); } catch (signErr) { return respondSigningError(res, signErr); } } catch (e) { - const x402 = normalizeX402Envelope(req.body?.x402, verb); - warmValidatorForVerb(x402.verb); + const execution = normalizeExecutionEnvelope(req.body?.execution ?? req.body, verb, requestedVersion); + 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"), @@ -1548,7 +1580,7 @@ async function handleVerb(verb, req, res) { try { - const receipt = makeReceipt({ x402, status: "error", error: err }); + const receipt = makeReceipt({ execution, status: "error", error: err, traceId, receiptId: makeFlowReceiptId() }); return res.status(500).json(wrapReceiptResponse(receipt, { trace, actor })); } catch (signErr) { return respondSigningError(res, signErr); @@ -1562,7 +1594,7 @@ async function handleVerb(verb, req, res) { // ----------------------- app.get("/", (req, res) => { res.setHeader("Content-Type", "application/json; charset=utf-8"); - const verbs = (ENABLED_VERBS || []).map((v) => `/${v}/v${API_VERSION}`); + const verbs = (ENABLED_VERBS || []).flatMap((v) => SUPPORTED_COMMONS_VERSIONS.map((version) => `/${v}/v${version}`)); return res.status(200).end( JSON.stringify({ ok: true, @@ -1767,6 +1799,8 @@ app.post("/verify", async (req, res) => { let ensExpect = null; if (wantEns) { + // ENS signer resolution for receipt verification. + // Resolves cl.sig.pub / cl.sig.kid from the signer ENS name so receipts can be verified without hardcoded keys. const signerForEns = String(proof?.signer_id || runtimeConfig.signerId || "").trim(); const ensOut = await fetchEnsSignerBundle({ signerName: signerForEns, refresh }); @@ -1836,7 +1870,8 @@ app.post("/verify", async (req, res) => { }); } - // 2) verify signature/hash via runtime-core + // CommandLayer receipt verification + // Rebuilds the canonical receipt hash and verifies the Ed25519 signature. let v; try { v = verifyReceiptEd25519Sha256(runtimeCoreReceipt, { @@ -1853,7 +1888,7 @@ app.post("/verify", async (req, res) => { ens_match: wantEns ? true : null, }, values: { - verb: receipt?.x402?.verb ?? null, + verb: receipt?.verb ?? null, signer_id: proof?.signer_id ?? null, pubkey_source: pubSrc, ens: ensExpect, @@ -1869,19 +1904,20 @@ app.post("/verify", async (req, res) => { const sigErr = signatureValid ? null : v?.reason || "verify failed"; + // Schema validation for verifiable agent receipts + // Confirms receipt structure matches the declared CommandLayer schema. // 3) schema validation (optional + edge-safe) let schemaOk = null; let schemaErrors = null; if (wantSchema) { schemaOk = false; - const verb = String(receipt?.x402?.verb || "").trim(); + const verb = String(receipt?.verb || "").trim(); if (!verb) { - schemaErrors = [{ message: "missing receipt.x402.verb" }]; + schemaErrors = [{ message: "missing receipt.verb" }]; } else if (VERIFY_SCHEMA_CACHED_ONLY && !hasValidatorCached(verb)) { - warmQueue.add(verb); - startWarmWorker(); + warmValidatorForVerb(verb); return res.status(202).json({ ok: false, @@ -1892,7 +1928,7 @@ app.post("/verify", async (req, res) => { ens_match: wantEns ? true : null, }, values: { - verb: receipt?.x402?.verb ?? null, + verb: receipt?.verb ?? null, signer_id: proof?.signer_id ?? null, pubkey_source: pubSrc, claimed_hash: proof?.hash_sha256 ?? null, @@ -1939,7 +1975,7 @@ app.post("/verify", async (req, res) => { ens_match: wantEns ? true : null, }, values: { - verb: receipt?.x402?.verb ?? null, + verb: receipt?.verb ?? null, signer_id: proof?.signer_id ?? null, kid: proof?.kid ?? null, canonical_id: proofCanonical || null, @@ -1967,7 +2003,7 @@ app.post("/verify", async (req, res) => { want_schema: wantSchema, refresh, strict_kid: strictKid, - verb: receipt?.x402?.verb ?? null, + verb: receipt?.verb ?? null, signer_id: proof?.signer_id ?? null, }); } @@ -1988,8 +2024,28 @@ app.post("/verify", async (req, res) => { }); // verb routes // ----------------------- +app.post("/execute", (req, res) => { + const body = req.body && typeof req.body === "object" ? req.body : {}; + const execution = body.execution && typeof body.execution === "object" ? body.execution : body; + const resolvedVerb = String(execution.verb || body.verb || "").trim(); + + if (!resolvedVerb) { + return res.status(400).json({ ok: false, error: "missing_verb", message: "execution.verb or verb is required", ...instancePayload() }); + } + + req.body = { + ...body, + ...execution, + verb: resolvedVerb, + }; + + return handleVerb(resolvedVerb, req, res); +}); + for (const verb of ENABLED_VERBS) { - app.post(`/${verb}/v${API_VERSION}`, (req, res) => handleVerb(verb, req, res)); + for (const version of SUPPORTED_COMMONS_VERSIONS) { + app.post(`/${verb}/v${version}`, (req, res) => handleVerb(verb, req, res, version)); + } } // JSON 404 for any unknown routes diff --git a/tests/fixtures/golden.public.pem b/tests/fixtures/golden.public.pem index ccecc90..8395b29 100644 --- a/tests/fixtures/golden.public.pem +++ b/tests/fixtures/golden.public.pem @@ -1,3 +1,3 @@ -----BEGIN PUBLIC KEY----- -MCowBQYDK2VwAyEAIL4DH52qyRXv6DYEA253pjohH/l6Slr1cmtP/uJYGnQ= +MCowBQYDK2VwAyEA3UYFLCzQGmaf074A7JrPM9CyapG4tCVYf5g+XNxPEfw= -----END PUBLIC KEY----- diff --git a/tests/fixtures/golden.receipt.json b/tests/fixtures/golden.receipt.json index 1a48ab8..9efe0e2 100644 --- a/tests/fixtures/golden.receipt.json +++ b/tests/fixtures/golden.receipt.json @@ -1,22 +1,74 @@ { - "receipt": { + "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", - "x402": { - "verb": "describe", - "version": "1.0.0", - "entry": "x402://describeagent.eth/describe/v1.0.0" + "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.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": { @@ -24,15 +76,17 @@ "canonical": "json.sorted_keys.v1", "signer_id": "runtime.commandlayer.eth", "kid": "v1", - "hash_sha256": "8deae11dc363a4d43164a5e6778cc09ca92079b739185f261c656bb4852b7d96", - "signature_b64": "l8rytZcttuB/McrYrN8/oHZH9ifVnP0teDa8fgWGGwtUq5h9i2wZdU1qW9J0+rseHwzgX1eFIA1AtPzjVkW5BQ==" + "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": "8deae11dc363a4d43164a5e6778cc09ca92079b739185f261c656bb4852b7d96" + "receipt_id": "clrcpt_00000000000000000000000000000000", + "trace_id": "cltrace_00000000000000000000000000000000" } }, "runtime_metadata": { - "trace": { - "provider": "golden" - } + "trace": { "provider": "runtime", "trace_id": "cltrace_00000000000000000000000000000000" } } } diff --git a/tests/fixtures/make-golden.mjs b/tests/fixtures/make-golden.mjs index 748ab17..2c82205 100644 --- a/tests/fixtures/make-golden.mjs +++ b/tests/fixtures/make-golden.mjs @@ -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" }); @@ -17,20 +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", - x402: { verb: "describe", 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" } }, + 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: { + 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, }, }; @@ -40,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 }, }, }; diff --git a/tests/golden.mjs b/tests/golden.mjs index 1c074b7..737b2fd 100644 --- a/tests/golden.mjs +++ b/tests/golden.mjs @@ -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"); diff --git a/tests/smoke.mjs b/tests/smoke.mjs index 263f72f..21f0ed2 100644 --- a/tests/smoke.mjs +++ b/tests/smoke.mjs @@ -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, @@ -192,7 +190,7 @@ async function main() { } const describeBody = { - x402: { verb: "describe", version: "1.1.0", entry: "x402://describeagent.eth/describe/v1.1.0" }, + execution: { verb: "describe", version: "1.1.0", entry: "https://runtime.commandlayer.org/execute", class: "commons" }, input: { subject: "smoke-test", detail_level: "short" }, trace: { provider: "smoke" }, }; @@ -201,7 +199,20 @@ async function main() { assert.equal(describe.status, 200, `describe failed: ${JSON.stringify(describe.json)}`); const { receipt, runtimeMetadata } = extractReceiptEnvelope(describe.json); assert.equal(receipt.status, "success", `describe receipt status must be success`); + assert.equal(receipt.entry, "https://runtime.commandlayer.org/execute", `commons receipt entry must use the canonical runtime execute URL`); + assert.equal(receipt.verb, "describe", `commons receipt verb must be describe`); + assert.equal(receipt.version, "1.1.0", `commons receipt version must be v1.1.0`); + assert.equal(receipt.class, "commons", `commons receipt class must be commons`); + assert.equal(receipt.x402, undefined, `commons receipts must not emit a nested x402 block`); 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);