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
233 changes: 48 additions & 185 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,209 +1,72 @@
# @commandlayer/runtime-core

Core signing, verification, and canonicalization primitives for the CommandLayer v1.1.0 protocol.
Canonical crypto and receipt verification primitives for CommandLayer CLAS.

This package is the single canonical implementation of:
## Canonical proof envelope (CLAS)

- **Canonicalization** — `json.sorted_keys.v1` deterministic JSON (keys sorted at every level)
- **Ed25519 signing and verification** — real `node:crypto` Ed25519, no mocks
- **Signed layered receipts** — v1.1.0 `SignedLayeredReceipt` with structured proof envelope
- **ENS signer resolution** — live TXT record lookup for `cl.sig.pub`
- **Legacy compat shims** — `metadata.proof` envelope bridge for runtime/server.mjs
`signCommandLayerReceipt()` writes the canonical proof envelope:

All other CommandLayer repos import from here. Nothing is reimplemented downstream.

## Install

```bash
npm install @commandlayer/runtime-core
```

Requires Node.js >= 20.

## Usage

### Canonicalization

```ts
import { canonicalize } from '@commandlayer/runtime-core';

const canonical = canonicalize({
verb: 'chat.completions',
version: '1.1.0',
agent: 'runtime.commandlayer.eth',
timestamp: '2026-05-12T00:00:00.000Z',
});
// Keys are sorted at every level, no trailing whitespace, no undefined values
```

### Sign and verify a receipt

```ts
import {
generateEd25519KeyPair,
signReceipt,
verifyReceipt,
} from '@commandlayer/runtime-core';

const { privateKeyPem, rawPublicKey } = generateEd25519KeyPair();

const signed = signReceipt(
{
verb: 'chat.completions',
version: '1.1.0',
agent: 'runtime.commandlayer.eth',
timestamp: new Date().toISOString(),
payload: { prompt: 'hello' },
result: { output: 'world' },
},
{
privateKeyPem,
kid: process.env.KEY_ID!,
signerEns: 'runtime.commandlayer.eth',
}
);

const result = verifyReceipt(signed, {
rawPublicKey,
expectedSigner: 'runtime.commandlayer.eth',
});

console.assert(result.valid === true);
```

### Resolve signer from ENS

```ts
import { JsonRpcProvider } from 'ethers';
import { resolveSignerFromENS } from '@commandlayer/runtime-core';

// Positional form
const provider = new JsonRpcProvider(process.env.RPC_URL);
const signer = await resolveSignerFromENS('signer.commandlayer.eth', provider);

// Options-object form (equivalent)
const signer2 = await resolveSignerFromENS({
ensName: 'signer.commandlayer.eth',
provider,
});
```

Supported TXT records:

| Key | Required | Description |
|-----|----------|-------------|
| `cl.sig.pub` | Yes | `ed25519:<standard_base64_raw32>` |
| `cl.sig.kid` | No | Short key identifier |
| `cl.sig.canonical` | No | Defaults to `json.sorted_keys.v1` |

### Key encoding
- `metadata.proof.canonicalization = "json.sorted_keys.v1"`
- `metadata.proof.hash.alg = "SHA-256"`
- `metadata.proof.hash.value = <lowercase hex digest>`
- `metadata.proof.signature.alg = "ed25519"`
- `metadata.proof.signature.value = <base64 signature>`
- `metadata.proof.signature.kid = <required key id>`

```ts
import { encodePublicKey, parsePublicKey } from '@commandlayer/runtime-core';

// Encode raw 32-byte key for ENS TXT record
const ensValue = encodePublicKey(rawPublicKey);
// => "ed25519:hhyCuPNoMk4JtEvGEV8F6nMZ4uDO1EcyizPufmnJTOY="

// Parse ENS TXT record back to raw bytes
const raw = parsePublicKey(ensValue);
// => Uint8Array(32)
import { signCommandLayerReceipt, verifyCommandLayerReceipt } from "@commandlayer/runtime-core";

const signed = signCommandLayerReceipt(receipt, { privateKeyPem, kid: "vC4WbcNoq2znSCiQ" });
const result = verifyCommandLayerReceipt(signed, { publicKeyPemOrDer: publicKeyPem });

// result shape
// {
// ok: boolean,
// status: "VERIFIED" | "INVALID",
// checks: { schema, canonical_hash, signature, signer },
// errors: string[]
// }
```

### Low-level crypto
## ENS signer records

```ts
import {
canonicalize,
signCanonical,
verifyCanonical,
verifyCanonicalWithRawKey,
} from '@commandlayer/runtime-core';

const canonical = canonicalize(payload);
const signature = signCanonical(canonical, privateKeyPem);
const valid = verifyCanonical(canonical, signature, publicKeyPem);
const validFromRaw = verifyCanonicalWithRawKey(canonical, signature, rawPublicKey);
```
Supported signer TXT records:

### Canonical CLAS proof envelope
- `cl.sig.pub = ed25519:<base64-raw-public-key>`
- `cl.sig.kid = <kid>`
- `cl.sig.canonical = json.sorted_keys.v1`
- `cl.receipt.signer = <signer ENS identity>`

Use the canonical metadata proof API:
Example fixture:

```ts
import {
signCommandLayerReceipt,
verifyCommandLayerReceipt,
buildCanonicalProof,
isSignedCommandLayerReceipt,
} from '@commandlayer/runtime-core';
```
- `cl.sig.kid = vC4WbcNoq2znSCiQ`
- `cl.sig.pub = ed25519:hhyCuPNoMk4JtEvGEV8F6nMZ4uDO1EcyizPufmnJTOY=`
- `cl.sig.canonical = json.sorted_keys.v1`
- `cl.receipt.signer = runtime.commandlayer.eth`

Canonical envelope fields:
- `metadata.proof.canonicalization`
- `metadata.proof.hash.alg`
- `metadata.proof.hash.value`
- `metadata.proof.signature.alg`
- `metadata.proof.signature.value`
- `metadata.proof.signature.kid`
When `ensRecord` is provided to `verifyCommandLayerReceipt`, verifier compares:

### Cross-repo canonicalization alignment
- `signature.kid` ↔ `cl.sig.kid`
- `metadata.proof.canonicalization` ↔ `cl.sig.canonical`
- `receipt.agent` ↔ `cl.receipt.signer`

Every repo that imports `@commandlayer/runtime-core` should run the shared test vectors:
## Endpoint discovery metadata (optional)

```ts
import { canonicalize, CANONICAL_TEST_VECTORS } from '@commandlayer/runtime-core';

for (const { description, input, expected } of CANONICAL_TEST_VECTORS) {
const actual = canonicalize(input);
if (actual !== expected) throw new Error(`Vector failed: ${description}`);
}
```
ENS resolver also parses optional discovery TXT records:

## Protocol constants
- `cl.endpoint.runtime`
- `cl.endpoint.verify`
- `cl.endpoint.mcp`
- `cl.endpoint.docs`
- `cl.endpoint.registry`

```ts
import {
PROTOCOL_VERSION, // "1.1.0"
CANONICAL_METHOD, // "json.sorted_keys.v1"
SIGNATURE_ALG, // "ed25519"
ENS_KEY_PUB, // "cl.sig.pub"
ENS_KEY_KID, // "cl.sig.kid"
ENS_KEY_CANONICAL, // "cl.sig.canonical"
ENS_KEY_SIGNER, // "cl.receipt.signer"
} from '@commandlayer/runtime-core';
```

## Environment variables

See `.env.example` for the full list. Key variables:

| Variable | Used by | Description |
|----------|---------|-------------|
| `RPC_URL` | `resolveSignerFromENS` | Ethereum JSON-RPC endpoint |
| `SIGNING_PRIVATE_KEY_PEM` | `signReceipt`, `signCanonical` | Ed25519 private key (PEM) |
| `SIGNING_PUBLIC_KEY_PEM` | `verifyReceipt`, `verifyCanonical` | Ed25519 public key (PEM) |
| `SIGNER_ENS_NAME` | ENS | ENS name with `cl.sig.pub` set |
These endpoint records are **optional discovery metadata only** and are **not verification-critical proof**.

## Development

```bash
npm run build # compile TypeScript
npm test # run all tests
npm run typecheck # type-check without emitting
```

## Signing protocol

The signing message is **raw UTF-8 bytes** of `canonicalize(receipt)`. This is NOT `sha256(canonical)` — signatures are over the data directly. Any change to this contract requires a protocol version bump.

```
signature = Ed25519.sign(
privateKey,
UTF8(canonicalize(receiptPayload))
)
npm install
npm run build
npm test
npm run typecheck
```

## License

Apache-2.0
2 changes: 1 addition & 1 deletion src/canonicalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export const CANONICAL_TEST_VECTORS = [
description: "audit protocol vector: verb/family/version",
input: { verb: "verify", family: "trust", version: "1.0.0" },
expected: '{"family":"trust","verb":"verify","version":"1.0.0"}',
// SHA-256 of the canonical string (UTF-8 encoded):
// SHA-256 of the canonical string (UTF-8 encoded), independently verified:
sha256: "7f84cc113290c283fe97e3beb9bd3f65e5de0022e278cad25ef7619c398b1bab",
},
] as const;
Loading
Loading