diff --git a/.github/workflows/python-sdk-tests.yml b/.github/workflows/python-sdk-tests.yml index d91f253..d186a47 100644 --- a/.github/workflows/python-sdk-tests.yml +++ b/.github/workflows/python-sdk-tests.yml @@ -1,13 +1,15 @@ -name: Python SDK Tests +name: Python SDK on: push: paths: - "python-sdk/**" + - "test_vectors/**" - ".github/workflows/python-sdk-tests.yml" pull_request: paths: - "python-sdk/**" + - "test_vectors/**" - ".github/workflows/python-sdk-tests.yml" jobs: @@ -31,11 +33,17 @@ jobs: - name: Install dependencies run: pip install -e '.[dev]' - - name: Lint (ruff) - run: python -m ruff check commandlayer/ + - name: Lint + run: python -m ruff check . - - name: Type check (mypy) - run: python -m mypy commandlayer/ + - name: Type check + run: python -m mypy commandlayer - - name: Tests (pytest) + - name: Tests run: python -m pytest tests/ -v + + - name: Build package + run: python -m build + + - name: Twine check + run: python -m twine check dist/* diff --git a/.github/workflows/typescript-sdk-cli-smoke.yml b/.github/workflows/typescript-sdk-cli-smoke.yml index d3e7fd9..fadc537 100644 --- a/.github/workflows/typescript-sdk-cli-smoke.yml +++ b/.github/workflows/typescript-sdk-cli-smoke.yml @@ -1,17 +1,21 @@ -name: TypeScript SDK CLI Smoke +name: TypeScript SDK on: push: paths: - "typescript-sdk/**" + - "runtime/tests/**" + - "test_vectors/**" - ".github/workflows/typescript-sdk-cli-smoke.yml" pull_request: paths: - "typescript-sdk/**" + - "runtime/tests/**" + - "test_vectors/**" - ".github/workflows/typescript-sdk-cli-smoke.yml" jobs: - cli-smoke: + test: runs-on: ubuntu-latest defaults: run: @@ -23,7 +27,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 20 cache: npm cache-dependency-path: typescript-sdk/package-lock.json @@ -39,5 +43,11 @@ jobs: - name: Unit tests run: npm run test:unit + - name: Runtime protocol tests + run: node --test ../runtime/tests/*.mjs + - name: CLI smoke tests run: npm run test:cli-smoke + + - name: Package dry run + run: npm pack --dry-run diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md index 6987885..2bddc27 100644 --- a/DEPLOYMENT_GUIDE.md +++ b/DEPLOYMENT_GUIDE.md @@ -1,392 +1,115 @@ -# CommandLayer SDK — Deployment Guide - -This repo ships **client SDKs + a CLI**. “Deploy” here means: build artifacts, run smoke tests, and (optionally) publish packages. - -Repo layout (current intent): -- `typescript-sdk/` → npm package + CLI (`commandlayer`) -- `python-sdk/` → PyPI package + CLI (optional) -- `GsCommand/` → optional wrapper / legacy tooling (only if you’re using it) - ---- - -## 0) Prereqs (do this first) - -### Node / npm (TypeScript SDK) -- Node: **LTS recommended** (Node 20.x is the safe default). - - Your logs show Node **22.20.0**. It can work, but if Windows native deps act up (esbuild), use Node 20 LTS. -- npm: comes with Node. -- Git: installed. - -### Python (Python SDK) -- Python 3.10+ recommended. -- `pip`, `venv`. - -### Windows-specific notes (your exact errors) -You hit: -- `EBUSY` on `esbuild.exe` during install -- `'tsup' is not recognized` - -That combination usually means: -1) `npm install` didn’t complete, so dev deps (tsup) never installed. -2) Windows locked a file in `node_modules` (Defender, indexing, or a stale node process). - -**Fix path (Windows):** -- Close any running node processes using the repo (VSCode terminals too). -- Add your repo folder to Windows Defender exclusions (at least `typescript-sdk/node_modules`). -- Then clean and reinstall (see section 2). - ---- - -## 1) Environment variables (only if needed) - -Most SDK work doesn’t require env vars. You only need them if you’re calling a live runtime or verifying receipts. - -Common: -- `COMMANDLAYER_RUNTIME_BASE_URL=https://runtime.commandlayer.org` -- `COMMANDLAYER_VERIFY_URL=https://runtime.commandlayer.org/verify` (or your Vercel proxy `/api/verify-receipt`) - -If you’re doing ENS-based pubkey verification server-side: -- `ETH_RPC_URL=...` -- `VERIFIER_ENS_NAME=runtime.commandlayer.eth` -- `ENS_PUBKEY_TEXT_KEY=cl.receipt.pubkey_pem` *(match your ENS TXT key exactly)* - -> SDK build itself does **not** depend on ENS TXT records. ENS only matters for runtime receipt verification logic. - ---- - -## 2) TypeScript SDK — Build + smoke test - -### A) Clean install (recommended when Windows breaks) -From repo root: -```bash -cd typescript-sdk - -# hard clean -rmdir /s /q node_modules 2>nul || true -del package-lock.json 2>nul || true - -# clear npm cache (optional but helps) -npm cache verify - -# install -npm install - -## If esbuild still throws EBUSY - -- Run the terminal as **Administrator** -- Temporarily disable real-time protection or add Windows Defender exclusions -- Retry: - -```bash -npm install -``` - ---- - -## B) Build - -```bash -npm run build -``` - -### Expected output - -- `dist/index.js` (CJS + ESM depending on config) -- `dist/index.d.ts` (types) - -If you see: - -``` -'tsup' is not recognized -``` - -That means `npm install` did not finish. -Re-run the clean install process until dependencies install successfully. - ---- - -## C) CLI smoke test (local) - -Your CLI is intended to import from `dist/`. - -```bash -node bin/cli.js summarize --content "test" --style bullet_points --json -``` - -### If CLI fails with syntax errors - -- Ensure `bin/cli.js` is valid JS (no stray template string quoting bugs) -- Ensure it requires: - -```js -require("../dist/index.js") -``` - -And that `dist/` exists after build. - ---- - -## D) Optional: link CLI globally for local testing - -Inside `typescript-sdk/`: - -```bash -npm link -commandlayer --help -commandlayer summarize --content "test" --style bullet_points -``` - -### To unlink - -```bash -npm unlink -g @commandlayer/sdk || true -``` - ---- - -# 3) TypeScript SDK — Publish to npm (optional) - -You have two sane approaches. - ---- - -## Option 1: Publish from `typescript-sdk/` as its own package - -Best if `typescript-sdk` is a standalone npm package directory. - -### Checklist - -`typescript-sdk/package.json` must include: - -- `"name": "@commandlayer/sdk"` (or your chosen scope) -- `"version": "x.y.z"` -- `"main"` and/or `"exports"` pointing at `dist/*` -- `"types"` pointing at `dist/index.d.ts` -- `"bin"` pointing at `bin/cli.js` (if publishing CLI) -- `"files"` field including: - - `dist/` - - `bin/` - -### Publish - -```bash -npm login -npm publish --access public -``` - ---- - -## Option 2: Root publishes multiple packages (monorepo) - -Only do this if you’ve set up workspaces + a release tool. - -Common tools: - -- npm workspaces + changesets -- pnpm + changesets -- lerna (less preferred) - -If you’re not already using these, don’t introduce them yet. - ---- - -# 4) Python SDK — Build + publish (optional) - -## A) Setup venv and install - -```bash -cd python-sdk -python -m venv .venv -``` - -Windows: - -```bash -.venv\Scripts\activate -``` - -macOS / Linux: - -```bash -source .venv/bin/activate -``` - -Then: - -```bash -pip install -U pip build twine -pip install -e . -``` - ---- - -## B) Build - -```bash -python -m build -``` - ---- - -## C) Publish - -```bash -twine upload dist/* -``` - ---- - -# 5) GitHub Actions (recommended) - -Minimal CI for `typescript-sdk/` should: - -- install -- build -- run CLI smoke test - -### Example workflow -`.github/workflows/typescript-sdk.yml` - -```yaml -name: typescript-sdk - -on: - push: - branches: [ main ] - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - defaults: - run: - working-directory: typescript-sdk - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - cache-dependency-path: typescript-sdk/package-lock.json - - run: npm ci - - run: npm run build - - run: node bin/cli.js summarize --content "test" --style bullet_points --json -``` - -If you want publish-on-tag later, add a separate job gated on Git tags. - ---- - -# 6) Versioning + release discipline - -Use **SemVer**: - -- Patch → bug fixes (no API break) -- Minor → new verbs / options (backward compatible) -- Major → breaking API changes - -### Recommended release steps - -1. Update `CHANGELOG.md` -2. Bump version: - -```bash -npm version patch -# or -npm version minor -# or -npm version major -``` - -3. Build -4. Smoke test CLI -5. Tag + push -6. Publish - ---- - -# 7) ENS TXT records — do they affect SDK deployment? - -No. Not for building or publishing. - -They affect: - -- Runtime receipt verification (“resolve pubkey from ENS”) -- Cross-verification tooling - -If your SDK includes a `verifyReceipt()` helper that can: - -- Verify with an explicit pubkey (offline) -- OR resolve pubkey from ENS (requires RPC) - -Then ENS records affect verification correctness — not build or release. - -### Your current ENS records - -``` -cl.receipt.pubkey_pem = PEM (escaped newlines) -cl.receipt.signer_id = runtime.commandlayer.eth -cl.receipt.alg = ed25519 -``` - -That’s the correct structure for “ENS as pubkey directory.” - ---- - -# 8) Known pitfalls (save yourself time) - -## Windows esbuild EBUSY - -Symptoms: -- Install fails - -Fix: -- Add Defender exclusion -- Close processes holding `node_modules` -- Delete `node_modules` and reinstall - ---- - -## CLI shebang on Windows - -```bash -#!/usr/bin/env node -``` - -This is fine. Windows ignores it; npm shim handles execution. - -Ensure `package.json` includes: - -```json -"bin": { - "commandlayer": "bin/cli.js" -} -``` - ---- - -## Don’t run CLI before build - -CLI requires: - -``` -../dist/index.js -``` - -Always build first. - ---- - -# 9) “Definition of Done” for SDK deployment - -You’re deployed when: - -- `npm install` succeeds cleanly -- `npm run build` produces `dist/` -- `node bin/cli.js summarize ...` returns receipt JSON without crashing -- CI runs the same steps on push/PR - ---- - -If you want, paste your `typescript-sdk/package.json` and I’ll rewrite it so `dist/` and `bin/` publish cleanly and the CLI installs as `commandlayer` without hacks. - +# Deployment and Release Guide + +This repo publishes two SDK packages from one protocol-aligned codebase: +- npm: `@commandlayer/sdk` +- PyPI: `commandlayer` + +Current release line: +- SDK package version: `1.1.0` +- Supported protocol line: Protocol-Commons v1.1.0 +- ENS / Agent-Card alignment: v1.1.0 signer-discovery flow + +## 1. Preconditions + +Before cutting a release: +- confirm both SDK packages are on the same version, +- confirm docs reference the same protocol version and receipt model, +- confirm shared test vectors still represent the current signed receipt truth, +- decide whether the release is docs-only or publishable. + +## 2. Local quality gates + +### TypeScript SDK + +```bash +cd typescript-sdk +npm ci +npm run typecheck +npm test +``` + +### Python SDK + +```bash +cd python-sdk +python -m venv .venv +source .venv/bin/activate +pip install -e '.[dev]' +ruff check . +mypy commandlayer +pytest +``` + +## 3. Packaging checks + +### npm package + +```bash +cd typescript-sdk +npm pack --dry-run +``` + +Verify that the tarball includes: +- `dist/index.cjs` +- `dist/index.mjs` +- `dist/index.d.ts` +- `dist/cli.cjs` +- `README.md` + +### PyPI package + +```bash +cd python-sdk +python -m build +python -m twine check dist/* +``` + +## 4. Publish flow + +### npm + +```bash +cd typescript-sdk +npm publish --access public +``` + +### PyPI + +```bash +cd python-sdk +python -m build +python -m twine upload dist/* +``` + +## 5. Git and release metadata + +Release steps: +1. merge the release branch, +2. create a git tag matching the SDK version, for example `sdk-v1.1.0`, +3. publish npm, +4. publish PyPI, +5. create GitHub release notes summarizing protocol line, SDK changes, and any migration notes. + +Release notes should call out: +- supported protocol version, +- receipt model changes, +- verification API changes, +- runtime compatibility notes, +- any explicit legacy compatibility retained. + +## 6. commandlayer.org coordination + +If the public docs site references installation or verification examples, update it in the same release window so that: +- package versions match, +- receipt examples match the repo, +- verification examples use the same API shapes, +- CLI examples are reproducible. + +## 7. CI expectations + +CI should stay green for: +- TypeScript typecheck/build/tests, +- Python lint/typecheck/tests, +- cross-SDK runtime fixture checks. + +Do not publish if any of those lanes are red. diff --git a/DEVELOPER_EXPERIENCE.md b/DEVELOPER_EXPERIENCE.md index 0796588..5fd6e29 100644 --- a/DEVELOPER_EXPERIENCE.md +++ b/DEVELOPER_EXPERIENCE.md @@ -1,387 +1,88 @@ -# Developer Experience (DX) — CommandLayer SDK - -This document defines how developers should experience CommandLayer — from install to first receipt — and the principles guiding SDK design decisions. - -The goal is simple: - -> Zero friction to first successful, verifiable receipt. - ---- - -# 1. Design Principles - -## 1.1 Deterministic by Default - -CommandLayer Commons verbs are: - -- Strictly schema-defined -- Deterministic where possible -- Receipt-producing -- Cryptographically verifiable - -Developers should never wonder: - -- What shape is this request? -- What does this response contain? -- Can I verify this output later? - -The SDK exists to eliminate ambiguity. - ---- - -## 1.2 Receipts > Raw Responses - -Every SDK method returns a **receipt object**, not just output. - -Example: - -```ts -const receipt = await client.summarize({ - content: "Long text...", - style: "bullet_points" -}); - -console.log(receipt.result); -console.log(receipt.metadata.proof.hash_sha256); -``` - -Why? - -Because CommandLayer is not just execution — it is **evidence**. - ---- - -## 1.3 Verification Is First-Class - -Verification should be: - -- Available -- Simple -- Optional -- Explicit - -Developers can: - -- Verify using a provided public key (offline) -- Resolve signer pubkey from ENS (online) -- Skip verification entirely (if desired) - -The SDK must not silently perform network verification without consent. - ---- - -# 2. Installation Experience - -## TypeScript - -```bash -npm install @commandlayer/sdk -``` - -Basic usage: - -```ts -import { createClient } from "@commandlayer/sdk"; - -const client = createClient({ - runtime: "https://runtime.commandlayer.org" -}); -``` - ---- - -## Python - -```bash -pip install commandlayer-sdk -``` - -Basic usage: - -```python -from commandlayer import create_client - -client = create_client(runtime="https://runtime.commandlayer.org") -``` - ---- - -# 3. First Successful Call - -The "Hello World" of CommandLayer: - -```ts -const receipt = await client.summarize({ - content: "CommandLayer standardizes agent verbs.", - style: "bullet_points" -}); - -console.log(receipt.result.summary); -``` - -Expected: - -- A valid structured result -- A signed receipt -- A verifiable hash - ---- - -# 4. SDK API Philosophy - -## 4.1 Flat, Verb-Based Methods - -Each Commons verb is a top-level method: - -```ts -client.summarize(...) -client.analyze(...) -client.classify(...) -client.fetch(...) -client.convert(...) -``` - -No nested namespaces. No magic proxies. - -Clarity over cleverness. - ---- - -## 4.2 No Hidden Global State - -The client instance holds configuration: - -```ts -const client = createClient({ - runtime: "...", - actor: "...", - verifyReceipts: true -}); -``` - -No singleton. -No global mutation. - ---- - -## 4.3 Explicit Runtime Selection - -Default runtime: - -``` -https://runtime.commandlayer.org -``` - -But developers may override: - -```ts -createClient({ - runtime: "https://my-runtime.internal" -}); -``` - -The SDK should not hard-code execution endpoints. - ---- - -# 5. Receipt Verification Model - -CommandLayer receipts contain: - -- Structured result -- Canonicalized hash -- Ed25519 signature -- Signer identity - -Verification modes: - -## 5.1 Local Public Key - -```ts -verifyReceipt(receipt, { - publicKey: "...pem..." -}); -``` - -Works offline. - ---- - -## 5.2 ENS Resolution - -```ts -verifyReceipt(receipt, { - ens: true, - rpcUrl: "https://mainnet.infura.io/v3/..." -}); -``` - -Resolves: - -``` -cl.receipt.pubkey_* TXT record -``` - -This anchors signer identity to ENS. - ---- - -## 5.3 Hybrid (Recommended) - -The SDK may: - -1. Attempt ENS resolution -2. Fall back to provided public key -3. Fail clearly if neither is available - -This provides: - -- Decentralized trust anchor -- Operational resilience - ---- - -# 6. CLI Developer Experience - -The CLI mirrors SDK behavior. - -Example: - -```bash -commandlayer summarize \ - --content "Test text" \ - --style bullet_points \ - --json -``` - -CLI expectations: - -- Always outputs valid JSON when `--json` is passed -- Human-readable output by default -- Never hides errors - -The CLI is: - -- A smoke test tool -- A reproducibility tool -- A receipt inspector - ---- - -# 7. Error Handling Philosophy - -Errors must be: - -- Structured -- Predictable -- Transparent - -SDK errors include: - -```ts -{ - statusCode: 400, - message: "summarize.input.content required", - details: {...} -} -``` - -No silent failures. -No swallowed verification errors. - ---- - -# 8. Local Development Flow - -Recommended flow: - -```bash -npm install -npm run build -node bin/cli.js summarize --content "test" --json -``` - -Definition of working local SDK: - -- Build succeeds -- CLI returns receipt JSON -- No runtime crashes -- No syntax errors - ---- - -# 9. Performance Considerations - -The SDK must: - -- Avoid schema compilation during hot paths -- Cache validators -- Avoid blocking verification by default -- Allow disabling receipt verification - -Verification should not cause production latency spikes. - ---- - -# 10. Backwards Compatibility - -We follow SemVer: - -- Patch → bug fixes -- Minor → new verbs/options -- Major → breaking changes - -The SDK must not: - -- Change receipt shapes silently -- Alter canonicalization rules -- Break verification logic without version bump - ---- - -# 11. What Is Not SDK Responsibility - -The SDK does NOT: - -- Define new verbs -- Modify schemas -- Change receipt canonicalization rules -- Override runtime security policy -- Perform business logic - -It is a transport + validation layer. - ---- - -# 12. Definition of Excellent Developer Experience - -A developer can: - -1. Install SDK in under 60 seconds -2. Make a successful call in under 2 minutes -3. Verify a receipt in under 5 minutes -4. Understand the receipt structure without reading the runtime source - -If any of those fail, DX needs improvement. - ---- - -# 13. Long-Term Vision - -CommandLayer SDKs should become: - -- The standard client layer for agentic infrastructure -- The simplest way to produce verifiable execution artifacts -- A reference implementation of semantic API contracts - -The SDK is not just a client. - -It is the interface between: - -- Intent -- Execution -- Evidence -- Trust - ---- - +# Developer Experience Guide + +This document is for maintainers and advanced integrators. Start with `README.md` or `QUICKSTART.md` if you are adopting the SDK. + +## Product rules this repo now enforces + +1. **One receipt truth**: the signed canonical payload is `receipt`. +2. **Runtime metadata stays separate**: execution context lives in optional `runtime_metadata` and is not part of the receipt hash. +3. **One version story**: both published SDK packages track CommandLayer Commons v1.1.0 and are versioned at `1.1.0` in this repo. +4. **Compatibility is explicit**: SDK clients normalize older blended runtime responses, but docs only teach the current envelope. + +## Repo structure + +- `typescript-sdk/`: npm package, CLI, JS verification helpers. +- `python-sdk/`: PyPI package and Python verification helpers. +- `test_vectors/`: shared receipt fixtures used across SDKs. +- `runtime/tests/`: cross-SDK protocol checks run against the built TypeScript package. +- root docs: public landing page, quickstart, examples, and release guide. + +## Shared protocol model + +### Canonical receipt + +The signed payload includes: +- `status`, +- `x402` verb metadata, +- `result` or `error`, +- `metadata.receipt_id`, and +- `metadata.proof` with `alg`, `canonical`, `signer_id`, `hash_sha256`, and `signature_b64`. + +### Runtime metadata + +Unsigned context can include: +- `trace_id`, +- timing fields, +- runtime provider metadata, +- request IDs. + +Clients normalize runtime responses into: + +```json +{ + "receipt": { ...canonical signed receipt... }, + "runtime_metadata": { ...optional unsigned context... } +} +``` + +## SDK parity expectations + +The TypeScript and Python SDKs should stay aligned on: +- method names, +- request body shaping, +- default runtime URL, +- receipt verification semantics, +- ENS resolution flow, +- major error messages where practical, +- shared test vectors. + +If one SDK intentionally diverges, document it in that package README and in release notes. + +## Verification rules + +Both SDKs use the same verification contract: +1. strip `receipt_id` and the signed hash/signature fields from the receipt, +2. canonicalize with `cl-stable-json-v1`, +3. recompute `sha256`, +4. compare against `metadata.proof.hash_sha256`, +5. verify the Ed25519 signature over the UTF-8 hash string, +6. optionally discover the signing key via ENS. + +## CLI rules + +The npm package owns the primary `commandlayer` CLI. + +The CLI should remain: +- installable with `npm install -g @commandlayer/sdk`, +- aligned with SDK examples, +- useful for CI smoke tests, +- capable of verifying saved receipts. + +## Maintenance checklist + +When protocol versions change: +1. update package versions and protocol constants, +2. update root docs and per-package READMEs, +3. regenerate or update shared fixtures, +4. run both SDK test suites plus `runtime/tests`, +5. confirm release instructions in `DEPLOYMENT_GUIDE.md` still match reality. diff --git a/EXAMPLES.md b/EXAMPLES.md index 5a43983..8351f8a 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,432 +1,296 @@ -# CommandLayer SDK — Examples (Spec-Ready) - -This document provides canonical, implementation-aligned examples for: - -- All Commons verbs -- Receipt structure -- Verification flows -- CLI usage -- cURL reproduction -- Multi-step orchestration -- Error handling - -All examples target: - -``` -API Version: v1.0.0 -Canonical Runtime: https://runtime.commandlayer.org -Schema Host: https://www.commandlayer.org -``` - ---- - -# 1. Receipt Structure (Canonical Reference) - -Every successful SDK call returns a receipt shaped as: - -```json -{ - "status": "success", - "x402": { - "verb": "summarize", - "version": "1.0.0", - "entry": "x402://summarizeagent.eth/summarize/v1.0.0" - }, - "trace": { - "trace_id": "trace_abc123", - "parent_trace_id": null, - "started_at": "2026-02-15T02:30:00.000Z", - "completed_at": "2026-02-15T02:30:00.120Z", - "duration_ms": 120, - "provider": "runtime" - }, - "result": { ... }, - "metadata": { - "proof": { - "alg": "ed25519-sha256", - "canonical": "cl-stable-json-v1", - "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "abc...", - "signature_b64": "xyz..." - }, - "receipt_id": "abc..." - } -} -``` - -Verification requires: - -- Canonical JSON reconstruction -- SHA-256 hash match -- Ed25519 signature verification - ---- - -# 2. TypeScript SDK Examples - -## 2.1 Create Client - -```ts -import { createClient } from "@commandlayer/sdk"; - -const client = createClient({ - runtime: "https://runtime.commandlayer.org", - verifyReceipts: false -}); -``` - ---- - -## 2.2 Summarize - -```ts -const receipt = await client.summarize({ - content: "CommandLayer defines semantic agent verbs.", - style: "bullet_points" -}); - -console.log(receipt.result.summary); -``` - -Expected `result`: - -```json -{ - "summary": "CommandLayer defines semantic agent verbs.", - "format": "text", - "compression_ratio": 1.0, - "source_hash": "..." -} -``` - ---- - -## 2.3 Analyze - -```ts -const receipt = await client.analyze({ - input: "Invoice total: $1200", - goal: "detect finance intent" -}); -``` - -Expected result: - -```json -{ - "summary": "...", - "insights": [...], - "labels": ["finance"], - "score": 0.25 -} -``` - ---- - -## 2.4 Classify - -```ts -const receipt = await client.classify({ - actor: "tenant_1", - input: { - content: "Contact support@example.com" - } -}); -``` - -Expected result: - -```json -{ - "labels": ["contains_emails"], - "scores": [0.5], - "taxonomy": ["root", "contains_emails"] -} -``` - ---- - -## 2.5 Fetch - -```ts -const receipt = await client.fetch({ - source: "https://example.com" -}); -``` - -Expected result: - -```json -{ - "items": [ - { - "source": "https://example.com", - "ok": true, - "http_status": 200, - "body_preview": "...", - "truncated": false - } - ] -} -``` - ---- - -## 2.6 Convert - -```ts -const receipt = await client.convert({ - input: { - content: "{\"a\":1}", - source_format: "json", - target_format: "csv" - } -}); -``` - ---- - -## 2.7 Explain - -```ts -await client.explain({ - input: { - subject: "Receipt verification", - audience: "novice", - style: "step-by-step" - } -}); -``` - ---- - -## 2.8 Parse - -```ts -await client.parse({ - input: { - content: "{ \"a\": 1 }", - content_type: "json" - } -}); -``` - ---- - -## 2.9 Clean - -```ts -await client.clean({ - input: { - content: " test@example.com ", - operations: ["trim", "redact_emails"] - } -}); -``` - ---- - -## 2.10 Format - -```ts -await client.format({ - input: { - content: "a: 1\nb: 2", - target_style: "table" - } -}); -``` - ---- - -# 3. CLI Examples - -## 3.1 Basic - -```bash -commandlayer summarize \ - --content "CommandLayer defines semantic verbs." \ - --style bullet_points \ - --json -``` - ---- - -## 3.2 Pipe Input - -```bash -cat file.txt | commandlayer summarize --stdin --json -``` - ---- - -## 3.3 Analyze - -```bash -commandlayer analyze \ - --content "Invoice total: $500" \ - --dimensions sentiment -``` - ---- - -# 4. cURL Reproduction - -Each receipt should include a `curl` block (if exposed by orchestration). - -Example: - -```bash -curl -X POST https://runtime.commandlayer.org/summarize/v1.0.0 \ - -H "Content-Type: application/json" \ - -d '{ - "x402": { - "verb": "summarize", - "version": "1.0.0" - }, - "input": { - "content": "Test text" - } - }' -``` - ---- - -# 5. Multi-Step Orchestration Example - -## Flow: Fetch → Summarize → Explain - -```ts -const step1 = await client.fetch({ source: "https://example.com" }); - -const step2 = await client.summarize({ - content: step1.result.items[0].body_preview -}); - -const step3 = await client.explain({ - input: { - subject: "Example site summary", - context: step2.result.summary - } -}); -``` - -Each step yields independent receipts. - ---- - -# 6. Receipt Verification (Offline) - -```ts -import { verifyReceipt } from "@commandlayer/sdk"; - -const ok = await verifyReceipt(receipt, { - publicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" -}); - -console.log(ok); -``` - -Verification steps: - -1. Reconstruct unsigned receipt -2. Stable JSON stringify -3. SHA-256 hash -4. Compare with `metadata.proof.hash_sha256` -5. Verify Ed25519 signature - ---- - -# 7. Receipt Verification (ENS) - -```ts -await verifyReceipt(receipt, { - ens: true, - rpcUrl: "https://mainnet.infura.io/v3/..." -}); -``` - -Resolution flow: - -- Fetch ENS resolver -- Read `cl.receipt.pubkey_*` -- Convert to PEM if required -- Verify signature - ---- - -# 8. Error Example - -```json -{ - "status": "error", - "x402": { "verb": "summarize", "version": "1.0.0" }, - "trace": {...}, - "error": { - "code": "INTERNAL_ERROR", - "message": "summarize.input.content required", - "retryable": false - }, - "metadata": { ... } -} -``` - -SDK should throw structured errors mirroring receipt shape. - ---- - -# 9. Schema Validation Example - -To validate receipt schema: - -```ts -await verifyReceipt(receipt, { - schema: true -}); -``` - -If schema validator not warmed: - -- SDK should either: - - Compile schema - - Or return controlled error - -Never silently ignore invalid schema. - ---- - -# 10. Deterministic Hash Example - -Unsigned receipt canonicalization: - -```ts -const unsigned = structuredClone(receipt); -unsigned.metadata.proof.hash_sha256 = ""; -unsigned.metadata.proof.signature_b64 = ""; -unsigned.metadata.receipt_id = ""; - -const canonical = stableStringify(unsigned); -const hash = sha256(canonical); -``` - -Hash must equal: - -``` -receipt.metadata.proof.hash_sha256 -``` - ---- - -# 11. Definition of Spec Compliance - -An SDK implementation is compliant if: - -- All verbs map to `/verb/v1.0.0` -- Receipt canonicalization matches runtime -- Verification passes against runtime receipts -- Errors match receipt schema -- CLI and SDK produce identical receipt structures - ---- - - +# CommandLayer SDK Examples + +Canonical examples for the CommandLayer SDK repo. + +All examples in this file target: +- Protocol-Commons v1.1.0, +- canonical signed receipts returned as `response.receipt`, and +- optional execution context returned as `response.runtime_metadata`. + +## 1. Canonical response envelope + +```json +{ + "receipt": { + "status": "success", + "x402": { + "verb": "summarize", + "version": "1.1.0", + "entry": "x402://summarizeagent.eth/summarize/v1.1.0" + }, + "result": { + "summary": "..." + }, + "metadata": { + "receipt_id": "...", + "proof": { + "alg": "ed25519-sha256", + "canonical": "cl-stable-json-v1", + "signer_id": "runtime.commandlayer.eth", + "hash_sha256": "...", + "signature_b64": "..." + } + } + }, + "runtime_metadata": { + "trace_id": "trace_abc123", + "duration_ms": 120, + "provider": "runtime.commandlayer.org" + } +} +``` + +## 2. TypeScript examples + +### Create client + +```ts +import { createClient } from "@commandlayer/sdk"; + +const client = createClient({ + actor: "examples-ts", + runtime: "https://runtime.commandlayer.org" +}); +``` + +### Summarize + +```ts +const response = await client.summarize({ + content: "CommandLayer defines semantic agent verbs.", + style: "bullet_points" +}); + +console.log(response.receipt.result?.summary); +``` + +### Analyze + +```ts +const response = await client.analyze({ + content: "Invoice total: $1200", + goal: "detect finance intent" +}); +``` + +### Classify + +```ts +const response = await client.classify({ + content: "Contact support@example.com" +}); +``` + +### Clean + +```ts +const response = await client.clean({ + content: " test@example.com ", + operations: ["trim", "redact_emails"] +}); +``` + +### Convert + +```ts +const response = await client.convert({ + content: '{"a":1}', + from: "json", + to: "csv" +}); +``` + +### Describe + +```ts +const response = await client.describe({ + subject: "receipt verification", + audience: "general", + detail: "medium" +}); +``` + +### Explain + +```ts +const response = await client.explain({ + subject: "receipt verification", + audience: "novice", + style: "step-by-step" +}); +``` + +### Format + +```ts +const response = await client.format({ + content: "a: 1\nb: 2", + to: "table" +}); +``` + +### Parse + +```ts +const response = await client.parse({ + content: '{ "a": 1 }', + contentType: "json", + mode: "strict" +}); +``` + +### Fetch + +```ts +const response = await client.fetch({ + source: "https://example.com", + include_metadata: true +}); +``` + +## 3. Python examples + +```python +from commandlayer import create_client + +client = create_client(actor="examples-py") + +summary = client.summarize(content="CommandLayer defines semantic agent verbs.", style="bullet_points") +analysis = client.analyze(content="Invoice total: $1200", goal="detect finance intent") +classification = client.classify(content="Contact support@example.com") +cleaned = client.clean(content=" test@example.com ", operations=["trim", "redact_emails"]) +converted = client.convert(content='{"a":1}', from_format="json", to_format="csv") +description = client.describe(subject="receipt verification") +explanation = client.explain(subject="receipt verification", style="step-by-step") +formatted = client.format(content="a: 1\nb: 2", to="table") +parsed = client.parse(content='{ "a": 1 }', content_type="json", mode="strict") +fetched = client.fetch(source="https://example.com", include_metadata=True) +``` + +## 4. Verification examples + +### TypeScript, explicit key + +```ts +import { verifyReceipt } from "@commandlayer/sdk"; + +const result = await verifyReceipt(response.receipt, { + publicKey: "ed25519:BASE64_PUBLIC_KEY" +}); +``` + +### Python, explicit key + +```python +from commandlayer import verify_receipt + +result = verify_receipt(response["receipt"], public_key="ed25519:BASE64_PUBLIC_KEY") +``` + +### ENS-backed verification + +```ts +const result = await verifyReceipt(response.receipt, { + ens: { + name: "summarizeagent.eth", + rpcUrl: process.env.MAINNET_RPC_URL! + } +}); +``` + +```python +result = verify_receipt( + response["receipt"], + ens={"name": "summarizeagent.eth", "rpcUrl": "https://mainnet.infura.io/v3/YOUR_KEY"}, +) +``` + +## 5. CLI examples + +### Summarize + +```bash +commandlayer summarize \ + --content "CommandLayer defines semantic verbs." \ + --style bullet_points \ + --json +``` + +### Analyze + +```bash +commandlayer analyze \ + --content "Invoice total: $500" \ + --goal "detect finance intent" \ + --json +``` + +### Verify a saved receipt + +```bash +commandlayer verify \ + --file receipt.json \ + --public-key "ed25519:BASE64_PUBLIC_KEY" +``` + +## 6. Runtime override + +### TypeScript + +```ts +const client = createClient({ + actor: "override-example", + runtime: "https://staging-runtime.commandlayer.org" +}); +``` + +### Python + +```python +client = create_client( + actor="override-example", + runtime="https://staging-runtime.commandlayer.org", +) +``` + +## 7. Persist the canonical receipt + +```ts +import { writeFile } from "node:fs/promises"; + +await writeFile("receipt.json", JSON.stringify(response.receipt, null, 2)); +``` + +```python +import json +from pathlib import Path + +Path("receipt.json").write_text(json.dumps(response["receipt"], indent=2), encoding="utf-8") +``` + +## 8. Error handling + +### TypeScript + +```ts +import { CommandLayerError } from "@commandlayer/sdk"; + +try { + await client.summarize({ content: "" }); +} catch (error) { + if (error instanceof CommandLayerError) { + console.error(error.statusCode, error.message, error.details); + } +} +``` + +### Python + +```python +from commandlayer import CommandLayerError + +try: + client.summarize(content="") +except CommandLayerError as error: + print(error.status_code, error, error.details) +``` diff --git a/QUICKSTART.md b/QUICKSTART.md index f441843..e80b027 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -1,195 +1,156 @@ # CommandLayer SDK Quickstart -Install. Call a verb. Get a signed receipt. +Goal: install the SDK, run one verb, inspect the receipt, verify it, and reproduce the call in under three minutes. -You can integrate CommandLayer in under 2 minutes. +## 1. Install ---- - -## What is CommandLayer? - -CommandLayer is the **semantic verb layer** for autonomous agents. - -It provides: - -- Standardized verbs (`summarize`, `analyze`, `classify`, etc.) -- Strict JSON request & receipt schemas -- Cryptographically signed receipts (Ed25519 + SHA-256) -- x402-compatible execution envelopes -- ERC-8004–aligned agent discovery - -CommandLayer turns agent actions into **verifiable infrastructure**. - ---- - -# 1️⃣ Install - -## TypeScript / JavaScript +### TypeScript / JavaScript ```bash npm install @commandlayer/sdk ``` -## Python +### Python ```bash pip install commandlayer ``` ---- +### CLI + +The CLI ships with the npm package: + +```bash +npm install -g @commandlayer/sdk +``` -# 2️⃣ Make Your First Call +## 2. Make your first call -## TypeScript +### TypeScript ```ts import { createClient } from "@commandlayer/sdk"; -const client = createClient({ - actor: "my-app" -}); +const client = createClient({ actor: "quickstart-ts" }); -const receipt = await client.summarize({ - content: "CommandLayer makes agent actions structured and verifiable.", +const response = await client.summarize({ + content: "CommandLayer makes agent execution verifiable.", style: "bullet_points" }); -console.log(receipt.result.summary); +console.log(response.receipt.result?.summary); ``` ---- - -## Python +### Python ```python from commandlayer import create_client -client = create_client(actor="my-app") - -receipt = client.summarize( - content="CommandLayer makes agent actions structured and verifiable.", - style="bullet_points" +client = create_client(actor="quickstart-py") +response = client.summarize( + content="CommandLayer makes agent execution verifiable.", + style="bullet_points", ) -print(receipt["result"]["summary"]) +print(response["receipt"]["result"]["summary"]) ``` ---- - -## CLI - -```bash -commandlayer summarize \ - --content "CommandLayer makes agent actions structured and verifiable." \ - --style bullet_points +## 3. Inspect the response + +Both SDKs return the same shape: + +```json +{ + "receipt": { + "status": "success", + "x402": { "verb": "summarize", "version": "1.1.0" }, + "result": { "summary": "..." }, + "metadata": { + "receipt_id": "...", + "proof": { + "alg": "ed25519-sha256", + "canonical": "cl-stable-json-v1", + "signer_id": "runtime.commandlayer.eth", + "hash_sha256": "...", + "signature_b64": "..." + } + } + }, + "runtime_metadata": { + "trace_id": "trace_123", + "duration_ms": 118 + } +} ``` ---- +Use `response.receipt` as the durable protocol artifact. `runtime_metadata` is optional execution context. -# 3️⃣ What You Get Back +## 4. Verify the receipt -Every call returns a **signed receipt**, not just raw output. +### TypeScript ```ts -receipt.status // "success" -receipt.metadata.receipt_id // Deterministic receipt hash -receipt.trace.duration_ms // Execution latency +import { verifyReceipt } from "@commandlayer/sdk"; -receipt.result // Structured verb output +const result = await verifyReceipt(response.receipt, { + publicKey: "ed25519:BASE64_PUBLIC_KEY" +}); -receipt.metadata.proof.hash_sha256 -receipt.metadata.proof.signature_b64 -receipt.metadata.proof.signer_id -receipt.metadata.proof.alg // "ed25519-sha256" +console.log(result.ok); ``` -Receipts are: - -- Canonicalized -- Hashed (SHA-256) -- Signed (Ed25519) -- Verifiable independently - -By default, the SDK verifies receipts automatically. - ---- +### Python -# 4️⃣ Available Verbs - -The Commons SDK includes 10 verbs: - -- `summarize` -- `analyze` -- `classify` -- `clean` -- `convert` -- `describe` -- `explain` -- `format` -- `parse` -- `fetch` - -All verbs return structured, signed receipts. - ---- - -# 5️⃣ Configuration +```python +from commandlayer import verify_receipt -```ts -const client = createClient({ - actor: "my-production-app", - runtime: "https://runtime.commandlayer.org", // default - verifyReceipts: true // default -}); +result = verify_receipt( + response["receipt"], + public_key="ed25519:BASE64_PUBLIC_KEY", +) +print(result["ok"]) ``` -### Options - -- `actor` — Identifier for your application or tenant -- `runtime` — Custom runtime base URL -- `verifyReceipts` — Enable/disable signature verification - ---- - -# 6️⃣ Production Notes +### ENS-backed verification -- Always set a meaningful `actor` -- Keep `verifyReceipts` enabled in production -- Store `receipt_id` for audit trails -- Treat receipts as durable evidence, not logs +Use the same signer-discovery model in both SDKs: +- agent ENS TXT: `cl.receipt.signer` +- signer ENS TXT: `cl.sig.pub` +- signer ENS TXT: `cl.sig.kid` ---- +## 5. Try the CLI -# 7️⃣ Verify a Receipt (Optional) - -```ts -import { verifyReceipt } from "@commandlayer/sdk"; - -const ok = await verifyReceipt(receipt, { - ens: true, - rpcUrl: "https://mainnet.infura.io/v3/..." -}); - -console.log("Verified:", ok); +```bash +commandlayer summarize \ + --content "CommandLayer makes agent execution verifiable." \ + --style bullet_points \ + --json ``` -You can verify: - -- With a provided public key (offline) -- By resolving signer pubkey from ENS -- Or disable verification entirely +Save the returned JSON and verify it: ---- +```bash +commandlayer verify \ + --file receipt.json \ + --public-key "ed25519:BASE64_PUBLIC_KEY" +``` -# Next Steps +## 6. What is stable today? -📖 Real-world usage → `EXAMPLES.md` -🚀 Deployment & publishing → `DEPLOYMENT_GUIDE.md` -🔍 SDK architecture → `DEVELOPER_EXPERIENCE.md` -🌐 Full docs → https://commandlayer.org/docs.html +Stable in this repo: +- Protocol-Commons v1.1.0 verb surface, +- canonical signed receipt verification, +- ENS signer discovery helpers, +- TypeScript SDK `@commandlayer/sdk` v1.1.0, +- Python SDK `commandlayer` v1.1.0. ---- +Not claimed as first-class SDK support here: +- Protocol-Commercial payment flows, +- runtime-specific orchestration metadata beyond the generic `runtime_metadata` envelope. -CommandLayer turns agent execution into verifiable infrastructure. +## Next steps -You're ready to build. +- More recipes: `EXAMPLES.md` +- Package docs: `typescript-sdk/README.md`, `python-sdk/README.md` +- Maintainer notes: `DEVELOPER_EXPERIENCE.md` +- Release flow: `DEPLOYMENT_GUIDE.md` diff --git a/README.md b/README.md index 46f0839..34dfd12 100644 --- a/README.md +++ b/README.md @@ -1,305 +1,170 @@ # CommandLayer SDK -**Semantic verbs. Structured schemas. Signed receipts.** +Official SDK repo for CommandLayer Protocol-Commons v1.1.0. -CommandLayer is the execution layer for autonomous agents that turns actions into verifiable infrastructure. +This repository ships the public developer surfaces for CommandLayer: +- the TypeScript SDK: `@commandlayer/sdk`, +- the Python SDK: `commandlayer`, +- the `commandlayer` CLI shipped with the npm package, +- verification helpers and test vectors, and +- repo-level docs for install, release, and reproducibility. -This SDK lets you: +## Supported protocol line -- Call standardized agent verbs (`summarize`, `analyze`, `classify`, etc.) -- Receive structured, typed responses -- Get cryptographically signed receipts -- Verify execution integrity (hash + signature) -- Integrate x402-ready workflows +This repo is aligned to the current CommandLayer v1.1.0 surface: +- Protocol-Commons v1.1.0, +- Agent-Cards v1.1.0 for ENS-backed signer discovery, +- canonical signed receipts as the verification contract payload, and +- optional `runtime_metadata` as unsigned execution context. ---- +Protocol-Commercial / x402 payment flows are not a first-class SDK surface in this repo today. The SDK is Commons-first; if commercial support expands, it should be added explicitly rather than implied. -# What Makes CommandLayer Different? +## Install -Traditional APIs return data. - -CommandLayer returns **evidence**. - -Every call produces a signed receipt containing: - -- Structured result -- Canonical hash -- Ed25519 signature -- Signer identity -- Trace metadata - -This enables: - -- Auditability -- Independent verification -- Cross-runtime interoperability -- Agent-to-agent trust - ---- - -# Installation - -## TypeScript / JavaScript +### TypeScript / JavaScript ```bash npm install @commandlayer/sdk ``` -## Python +Node.js 20+ is supported. + +### Python ```bash pip install commandlayer ``` ---- +Python 3.10+ is supported. -# Quick Example (TypeScript) +## First call: TypeScript ```ts -import { createClient } from "@commandlayer/sdk"; +import { createClient, verifyReceipt } from "@commandlayer/sdk"; -const client = createClient({ - actor: "my-app" -}); +const client = createClient({ actor: "my-app" }); -const receipt = await client.summarize({ - content: "CommandLayer defines semantic verbs.", +const response = await client.summarize({ + content: "CommandLayer turns agent calls into signed receipts.", style: "bullet_points" }); -console.log(receipt.result.summary); -``` +console.log(response.receipt.result?.summary); +console.log(response.receipt.metadata?.receipt_id); +console.log(response.runtime_metadata?.duration_ms); -You receive a signed receipt — not just raw output. +const verification = await verifyReceipt(response.receipt, { + publicKey: process.env.COMMANDLAYER_PUBLIC_KEY! +}); ---- +console.log(verification.ok); +``` -# Available Verbs (Commons v1.0.0) +## First call: Python -- `summarize` -- `analyze` -- `classify` -- `clean` -- `convert` -- `describe` -- `explain` -- `format` -- `parse` -- `fetch` +```python +from commandlayer import create_client, verify_receipt -All verbs: +client = create_client(actor="my-app") +response = client.summarize( + content="CommandLayer turns agent calls into signed receipts.", + style="bullet_points", +) -- Use strict JSON schemas -- Produce deterministic receipt envelopes -- Support receipt verification +print(response["receipt"]["result"]["summary"]) +print(response["receipt"]["metadata"]["receipt_id"]) +print(response.get("runtime_metadata", {}).get("duration_ms")) ---- +verification = verify_receipt( + response["receipt"], + public_key="ed25519:BASE64_PUBLIC_KEY", +) +print(verification["ok"]) +``` -# Receipt Structure (High-Level) +## Return shape + +Client methods now return a command response envelope: ```json { - "status": "success", - "x402": { "verb": "summarize", "version": "1.0.0" }, - "trace": { "trace_id": "...", "duration_ms": 112 }, - "result": { ... }, - "metadata": { - "proof": { - "alg": "ed25519-sha256", - "hash_sha256": "...", - "signature_b64": "...", - "signer_id": "runtime.commandlayer.eth" + "receipt": { + "status": "success", + "x402": { + "verb": "summarize", + "version": "1.1.0", + "entry": "x402://summarizeagent.eth/summarize/v1.1.0" + }, + "result": { + "summary": "..." }, - "receipt_id": "..." + "metadata": { + "receipt_id": "...", + "proof": { + "alg": "ed25519-sha256", + "canonical": "cl-stable-json-v1", + "signer_id": "runtime.commandlayer.eth", + "hash_sha256": "...", + "signature_b64": "..." + } + } + }, + "runtime_metadata": { + "trace_id": "trace_123", + "duration_ms": 118, + "provider": "runtime.commandlayer.org" } } ``` -Receipts are: - -- Canonicalized -- SHA-256 hashed -- Ed25519 signed - ---- +The canonical signed object is `receipt`. `runtime_metadata` is optional and unsigned. Verification, persistence, and downstream audit should use the canonical `receipt` object. -# Verification +The SDK still normalizes older blended runtime responses for compatibility, but the repo now documents the v1.1.0 envelope as the single current truth. -The SDK supports: +## Verification -### Offline Verification +### Offline verification ```ts -verifyReceipt(receipt, { - publicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" -}); -``` - -### ENS-Based Verification +import { verifyReceipt } from "@commandlayer/sdk"; -```ts -verifyReceipt(receipt, { - ens: { - name: "runtime.commandlayer.eth", - rpcUrl: "https://mainnet.infura.io/v3/..." - } +const result = await verifyReceipt(response.receipt, { + publicKey: "ed25519:BASE64_PUBLIC_KEY" }); ``` -ENS verification resolves signer material using this TXT record chain: - -- Agent/issuer ENS name TXT `cl.receipt.signer` => signer ENS name -- Signer ENS name TXT `cl.sig.pub` and `cl.sig.kid` => Ed25519 public key metadata - -To verify receipts issued by the runtime signer, set `ens.name` to the issuer/agent name that contains `cl.receipt.signer`. If using `runtime.commandlayer.eth` as the agent name, set `cl.receipt.signer` on `runtime.commandlayer.eth` to point to itself. - -### Verification (Working Example) +### ENS-backed verification ```ts -import { createClient } from "@commandlayer/sdk"; -import { verifyReceipt } from "@commandlayer/sdk"; - -const client = createClient({ - actor: "my-app", - verifyReceipts: true, - verify: { - ens: { - name: "runtime.commandlayer.eth", - rpcUrl: process.env.MAINNET_RPC! - } +const result = await verifyReceipt(response.receipt, { + ens: { + name: "summarizeagent.eth", + rpcUrl: process.env.MAINNET_RPC_URL! } }); - -const receipt = await client.summarize({ content: "hello", style: "bullet_points" }); -// if verifyReceipts true, client methods should throw or return verify result depending on your design -// also show direct call: -const vr = await verifyReceipt(receipt, { - ens: { name: "runtime.commandlayer.eth", rpcUrl: process.env.MAINNET_RPC! } -}); -console.log(vr.ok, vr.checks); ``` -### ENS Setup - -For `runtime.commandlayer.eth` as the signer identity: - -- Existing TXT records on `runtime.commandlayer.eth`: - - `cl.sig.kid = v1` - - `cl.sig.pub = ed25519:CEHI9g4...` -- Add one additional TXT record on `runtime.commandlayer.eth`: - - `cl.receipt.signer = runtime.commandlayer.eth` - -This makes `runtime.commandlayer.eth` self-describing for ENS verification, so the SDK can resolve `cl.receipt.signer` and then fetch `cl.sig.pub`/`cl.sig.kid` from the same name. - -Optional future pattern: - -- If each issuer/agent ENS name (for example, `summarizeagent.eth`) should verify through its own lookup, add: - - `cl.receipt.signer = runtime.commandlayer.eth` - on each issuer/agent ENS name, while keeping signing keys only on `runtime.commandlayer.eth`. - ---- +ENS signer discovery resolves: +1. `cl.receipt.signer` on the agent ENS name, +2. `cl.sig.pub` on the signer ENS name, +3. `cl.sig.kid` on the signer ENS name. -# CLI +## CLI -The SDK includes a CLI for testing and reproducibility. +Install the npm package and use the bundled CLI: ```bash -commandlayer summarize \ - --content "Test text" \ - --style bullet_points +commandlayer summarize --content "Test text" --style bullet_points --json +commandlayer verify --file receipt.json --public-key "ed25519:BASE64_PUBLIC_KEY" ``` -Pipe support: - -```bash -cat file.txt | commandlayer summarize --stdin -``` - ---- - -# Runtime Compatibility - -Default runtime: - -``` -https://runtime.commandlayer.org -``` - -Override if needed: - -```ts -createClient({ - runtime: "https://your-runtime.example" -}); -``` - -The SDK does not hard-code execution infrastructure. - ---- - -# Architecture - -CommandLayer separates: - -1. **Semantic layer** (verbs + schemas) -2. **Execution layer** (runtime) -3. **Verification layer** (hash + signature) -4. **Discovery layer** (ENS / ERC-8004) - -The SDK acts as the client transport and validation interface across these layers. - ---- - -# Versioning - -This SDK follows **Semantic Versioning**: - -- Patch → bug fixes -- Minor → new verbs or backward-compatible changes -- Major → breaking changes - -Commons schemas are versioned (`v1.0.0`) and stable. - ---- - -# Developer Resources - -- Quickstart → `QUICKSTART.md` -- Full Examples → `EXAMPLES.md` -- Deployment Guide → `DEPLOYMENT_GUIDE.md` -- Developer Architecture → `DEVELOPER_EXPERIENCE.md` -- Schemas → https://commandlayer.org/schemas -- Runtime Docs → https://commandlayer.org/runtime.html - ---- - -# Philosophy - -CommandLayer is not an AI model. - -It is a **semantic contract layer**. - -It standardizes: - -- What an action means -- How it is executed -- How it is proven -- How it is verified - -Receipts are not logs. - -They are **cryptographic execution artifacts**. - ---- - -# License - -Commons SDK components are MIT licensed. - -See `LICENSE` for details. - ---- +The CLI is intended for demos, CI smoke tests, debugging, and reproducing SDK flows without writing app code. -CommandLayer turns agent execution into verifiable infrastructure. +## Repo guide -Build systems that can prove what they did. +- Fast onboarding: `QUICKSTART.md` +- Cookbook examples: `EXAMPLES.md` +- Maintainer / architecture notes: `DEVELOPER_EXPERIENCE.md` +- Build, release, and publish flow: `DEPLOYMENT_GUIDE.md` +- TypeScript package docs: `typescript-sdk/README.md` +- Python package docs: `python-sdk/README.md` diff --git a/python-sdk/README.md b/python-sdk/README.md index 4e3f5ad..f5be7f2 100644 --- a/python-sdk/README.md +++ b/python-sdk/README.md @@ -1,99 +1,48 @@ # CommandLayer Python SDK -Semantic verbs. Signed receipts. Deterministic verification. +Official Python SDK for CommandLayer Commons v1.1.0. -Official Python SDK for **CommandLayer Commons v1.0.0**. +The Python package mirrors the TypeScript SDK's protocol model: +- client methods return `{ "receipt": ..., "runtime_metadata": ... }`, +- the signed `receipt` is the canonical verification payload, +- `runtime_metadata` is optional execution context, and +- verification can use an explicit Ed25519 key or ENS discovery. ---- - -## Installation +## Install ```bash pip install commandlayer ``` -Python **3.10+** is supported. - -For local development: - -```bash -pip install -e '.[dev]' -``` - ---- +Supported Python versions: 3.10+. -## Quickstart +## Quick start ```python -from commandlayer import create_client +from commandlayer import create_client, verify_receipt -client = create_client( - actor="my-app", - runtime="https://runtime.commandlayer.org", # optional -) - -receipt = client.summarize( - content="CommandLayer turns agent actions into verifiable receipts.", +client = create_client(actor="docs-example") +response = client.summarize( + content="CommandLayer makes agent execution verifiable.", style="bullet_points", ) -print(receipt["status"]) -print(receipt["metadata"]["receipt_id"]) -``` - -> `verify_receipts` is **off by default** (matching the TypeScript SDK behavior). - ---- - -## Client Configuration - -```python -from commandlayer import CommandLayerClient - -client = CommandLayerClient( - runtime="https://runtime.commandlayer.org", - actor="my-app", - timeout_ms=30_000, - headers={"X-Trace-ID": "abc123"}, - retries=1, - verify_receipts=True, - verify={ - "public_key": "ed25519:7Vkkmt6R02Iltp/+i3D5mraZyvLjfuTSVB33KwfzQC8=", - # or ENS: - # "ens": {"name": "summarizeagent.eth", "rpcUrl": "https://..."}, - }, -) -``` - -### Verification options - -- `verify["public_key"]` (alias: `publicKey`): explicit Ed25519 pubkey - - accepted formats: `ed25519:`, ``, `0x`, `` -- `verify["ens"]`: `{ "name": str, "rpcUrl"|"rpc_url": str }` - - resolves `cl.receipt.signer` on the agent ENS name - - resolves `cl.sig.pub` and `cl.sig.kid` on the signer ENS name - ---- - -## Receipt Verification API - -```python -from commandlayer import verify_receipt +print(response["receipt"]["result"]["summary"]) +print(response["receipt"]["metadata"]["receipt_id"]) +print(response.get("runtime_metadata", {}).get("duration_ms")) -result = verify_receipt( - receipt, - public_key="ed25519:7Vkkmt6R02Iltp/+i3D5mraZyvLjfuTSVB33KwfzQC8=", +verification = verify_receipt( + response["receipt"], + public_key="ed25519:BASE64_PUBLIC_KEY", ) - -print(result["ok"]) -print(result["checks"]) +print(verification["ok"]) ``` -ENS-based verification: +## Verification ```python result = verify_receipt( - receipt, + response["receipt"], ens={ "name": "summarizeagent.eth", "rpcUrl": "https://mainnet.infura.io/v3/YOUR_KEY", @@ -101,27 +50,6 @@ result = verify_receipt( ) ``` ---- - -## Supported Verbs - -All verbs return a signed receipt. - -```python -client.summarize(content="...", style="bullet_points") -client.analyze(content="...", goal="extract key risks") -client.classify(content="...", max_labels=5) -client.clean(content="...", operations=["trim", "normalize_newlines"]) -client.convert(content='{"a":1}', from_format="json", to_format="csv") -client.describe(subject="x402 receipt", detail="medium") -client.explain(subject="receipt verification", style="step-by-step") -client.format(content="a: 1\nb: 2", to="table") -client.parse(content='{"a":1}', content_type="json", mode="strict") -client.fetch(source="https://example.com", include_metadata=True) -``` - ---- - ## Development ```bash @@ -133,13 +61,3 @@ ruff check . mypy commandlayer pytest ``` - ---- - -## Documentation - -See `docs/` for usage and API details: - -- `docs/getting-started.md` -- `docs/client.md` -- `docs/verification.md` diff --git a/python-sdk/commandlayer/__init__.py b/python-sdk/commandlayer/__init__.py index 6a150cc..405ed4c 100644 --- a/python-sdk/commandlayer/__init__.py +++ b/python-sdk/commandlayer/__init__.py @@ -1,10 +1,12 @@ """CommandLayer Python SDK.""" -from .client import CommandLayerClient, create_client +from .client import CommandLayerClient, create_client, normalize_command_response from .errors import CommandLayerError from .types import ( + CanonicalReceipt, + CommandResponse, EnsVerifyOptions, - Receipt, + RuntimeMetadata, SignerKeyResolution, VerifyOptions, VerifyResult, @@ -19,15 +21,18 @@ ) __all__ = [ + "CanonicalReceipt", "CommandLayerClient", - "create_client", "CommandLayerError", + "CommandResponse", "EnsVerifyOptions", + "RuntimeMetadata", "VerifyOptions", "SignerKeyResolution", - "Receipt", "VerifyResult", "canonicalize_stable_json_v1", + "create_client", + "normalize_command_response", "sha256_hex_utf8", "parse_ed25519_pubkey", "recompute_receipt_hash_sha256", @@ -35,4 +40,4 @@ "verify_receipt", ] -__version__ = "1.0.0" +__version__ = "1.1.0" diff --git a/python-sdk/commandlayer/client.py b/python-sdk/commandlayer/client.py index 06e5949..60f600c 100644 --- a/python-sdk/commandlayer/client.py +++ b/python-sdk/commandlayer/client.py @@ -3,16 +3,17 @@ import json import time from collections.abc import Mapping -from typing import Any +from typing import Any, cast import httpx from .errors import CommandLayerError -from .types import Receipt, VerifyOptions +from .types import CommandResponse, RuntimeMetadata, VerifyOptions from .verify import verify_receipt -VERSION = "1.0.0" - +COMMONS_VERSION = "1.1.0" +PACKAGE_VERSION = "1.1.0" +DEFAULT_RUNTIME = "https://runtime.commandlayer.org" VERBS = { "summarize", "analyze", @@ -31,12 +32,30 @@ def _normalize_base(url: str) -> str: return str(url or "").rstrip("/") +def normalize_command_response(payload: Any) -> CommandResponse: + if not isinstance(payload, dict): + raise CommandLayerError("Runtime response must be a JSON object", 502, payload) + + if isinstance(payload.get("receipt"), dict): + response: CommandResponse = {"receipt": payload["receipt"]} + if isinstance(payload.get("runtime_metadata"), dict): + response["runtime_metadata"] = cast(RuntimeMetadata, dict(payload["runtime_metadata"])) + return response + + receipt = dict(payload) + runtime_metadata = receipt.pop("trace", None) + response = {"receipt": receipt} + if isinstance(runtime_metadata, dict): + response["runtime_metadata"] = cast(RuntimeMetadata, runtime_metadata) + return response + + class CommandLayerClient: - """Synchronous CommandLayer client for Commons verbs.""" + """Synchronous CommandLayer client for Protocol-Commons v1.1.0 verbs.""" def __init__( self, - runtime: str = "https://runtime.commandlayer.org", + runtime: str = DEFAULT_RUNTIME, actor: str = "sdk-user", timeout_ms: int = 30_000, headers: Mapping[str, str] | None = None, @@ -51,28 +70,23 @@ def __init__( self.retries = max(0, retries) self.verify_receipts = verify_receipts is True self.verify_defaults: VerifyOptions = verify or {} - self.default_headers = { "Content-Type": "application/json", - "User-Agent": f"commandlayer-py/{VERSION}", + "User-Agent": f"commandlayer-py/{PACKAGE_VERSION}", } if headers: self.default_headers.update(dict(headers)) - self._http = http_client or httpx.Client(timeout=self.timeout_ms / 1000) def _ensure_verify_config_if_enabled(self) -> None: if not self.verify_receipts: return - explicit_public_key = self.verify_defaults.get("public_key") or self.verify_defaults.get( "publicKey" ) has_explicit = bool(str(explicit_public_key or "").strip()) - ens = self.verify_defaults.get("ens") or {} has_ens = bool(ens.get("name") and (ens.get("rpcUrl") or ens.get("rpc_url"))) - if not has_explicit and not has_ens: raise CommandLayerError( "verify_receipts is enabled but no verification key config provided. " @@ -87,7 +101,7 @@ def summarize( style: str | None = None, format: str | None = None, max_tokens: int = 1000, - ) -> Receipt: + ) -> CommandResponse: return self.call( "summarize", { @@ -107,7 +121,7 @@ def analyze( goal: str | None = None, hints: list[str] | None = None, max_tokens: int = 1000, - ) -> Receipt: + ) -> CommandResponse: payload: dict[str, Any] = { "input": content, "limits": {"max_output_tokens": max_tokens}, @@ -118,7 +132,9 @@ def analyze( payload["hints"] = hints return self.call("analyze", payload) - def classify(self, *, content: str, max_labels: int = 5, max_tokens: int = 1000) -> Receipt: + def classify( + self, *, content: str, max_labels: int = 5, max_tokens: int = 1000 + ) -> CommandResponse: return self.call( "classify", { @@ -134,7 +150,7 @@ def clean( content: str, operations: list[str] | None = None, max_tokens: int = 1000, - ) -> Receipt: + ) -> CommandResponse: return self.call( "clean", { @@ -154,7 +170,7 @@ def convert( from_format: str, to_format: str, max_tokens: int = 1000, - ) -> Receipt: + ) -> CommandResponse: return self.call( "convert", { @@ -174,7 +190,7 @@ def describe( audience: str = "general", detail: str = "medium", max_tokens: int = 1000, - ) -> Receipt: + ) -> CommandResponse: return self.call( "describe", { @@ -195,7 +211,7 @@ def explain( style: str = "step-by-step", detail: str = "medium", max_tokens: int = 1000, - ) -> Receipt: + ) -> CommandResponse: return self.call( "explain", { @@ -209,7 +225,7 @@ def explain( }, ) - def format(self, *, content: str, to: str, max_tokens: int = 1000) -> Receipt: + def format(self, *, content: str, to: str, max_tokens: int = 1000) -> CommandResponse: return self.call( "format", { @@ -226,7 +242,7 @@ def parse( mode: str = "best_effort", target_schema: str | None = None, max_tokens: int = 1000, - ) -> Receipt: + ) -> CommandResponse: payload: dict[str, Any] = { "input": { "content": content, @@ -246,13 +262,12 @@ def fetch( query: str | None = None, include_metadata: bool | None = None, max_tokens: int = 1000, - ) -> Receipt: + ) -> CommandResponse: input_obj: dict[str, Any] = {"source": source} if query is not None: input_obj["query"] = query if include_metadata is not None: input_obj["include_metadata"] = include_metadata - return self.call( "fetch", {"input": input_obj, "limits": {"max_output_tokens": max_tokens}}, @@ -262,16 +277,15 @@ def _build_payload(self, verb: str, body: dict[str, Any]) -> dict[str, Any]: return { "x402": { "verb": verb, - "version": VERSION, - "entry": f"x402://{verb}agent.eth/{verb}/v{VERSION}", + "version": COMMONS_VERSION, + "entry": f"x402://{verb}agent.eth/{verb}/v{COMMONS_VERSION}", }, "actor": body.get("actor", self.actor), **body, } def _request(self, verb: str, payload: dict[str, Any]) -> httpx.Response: - url = f"{self.runtime}/{verb}/v{VERSION}" - + url = f"{self.runtime}/{verb}/v{COMMONS_VERSION}" attempt = 0 while True: try: @@ -282,14 +296,12 @@ def _request(self, verb: str, payload: dict[str, Any]) -> httpx.Response: except httpx.HTTPError as err: if attempt >= self.retries: raise CommandLayerError(f"HTTP transport error: {err}") from err - attempt += 1 time.sleep(min(0.2 * attempt, 1.0)) - def call(self, verb: str, body: dict[str, Any]) -> Receipt: + def call(self, verb: str, body: dict[str, Any]) -> CommandResponse: if verb not in VERBS: raise CommandLayerError(f"Unsupported verb: {verb}", 400) - self._ensure_verify_config_if_enabled() payload = self._build_payload(verb, body) response = self._request(verb, payload) @@ -311,22 +323,17 @@ def call(self, verb: str, body: dict[str, Any]) -> Receipt: ) raise CommandLayerError(str(message), response.status_code, data) - if not isinstance(data, dict): - raise CommandLayerError( - "Runtime response must be a JSON object", response.status_code, data - ) - + normalized = normalize_command_response(data) if self.verify_receipts: verify_result = verify_receipt( - data, + normalized["receipt"], public_key=self.verify_defaults.get("public_key") or self.verify_defaults.get("publicKey"), ens=self.verify_defaults.get("ens"), ) if not verify_result["ok"]: raise CommandLayerError("Receipt verification failed", 422, verify_result) - - return data + return normalized def close(self) -> None: self._http.close() diff --git a/python-sdk/commandlayer/types.py b/python-sdk/commandlayer/types.py index 5e4b548..7bad916 100644 --- a/python-sdk/commandlayer/types.py +++ b/python-sdk/commandlayer/types.py @@ -3,20 +3,32 @@ from dataclasses import dataclass from typing import Any, Literal, TypedDict -Receipt = dict[str, Any] +CanonicalReceipt = dict[str, Any] -class EnsVerifyOptions(TypedDict, total=False): - """ENS options for receipt verification.""" +class RuntimeMetadata(TypedDict, total=False): + trace_id: str + parent_trace_id: str | None + started_at: str + completed_at: str + duration_ms: int + provider: str + runtime: str + request_id: str + + +class CommandResponse(TypedDict, total=False): + receipt: CanonicalReceipt + runtime_metadata: RuntimeMetadata + +class EnsVerifyOptions(TypedDict, total=False): name: str rpc_url: str rpcUrl: str class VerifyOptions(TypedDict, total=False): - """Verification options for client-side receipt checks.""" - public_key: str publicKey: str ens: EnsVerifyOptions diff --git a/python-sdk/commandlayer/verify.py b/python-sdk/commandlayer/verify.py index a709b3f..80d69d7 100644 --- a/python-sdk/commandlayer/verify.py +++ b/python-sdk/commandlayer/verify.py @@ -11,7 +11,13 @@ from nacl.signing import VerifyKey from web3 import Web3 -from .types import EnsVerifyOptions, Receipt, SignerKeyResolution, VerifyResult +from .types import ( + CanonicalReceipt, + CommandResponse, + EnsVerifyOptions, + SignerKeyResolution, + VerifyResult, +) _ED25519_PREFIX_RE = re.compile(r"^ed25519\s*[:=]\s*(.+)$", re.IGNORECASE) _ED25519_HEX_RE = re.compile(r"^(0x)?[0-9a-fA-F]{64}$") @@ -28,15 +34,12 @@ def __init__(self, rpc_url: str): def get_text(self, name: str, key: str) -> str | None: if not self._w3.is_connected(): raise ValueError(f"Unable to connect to RPC: {self._w3.provider}") - ens_module = self._w3.ens # type: ignore[attr-defined] if ens_module is None: raise ValueError("ENS module is unavailable on this web3 instance") - value = ens_module.get_text(name, key) # type: ignore[union-attr] if value is None: return None - text = str(value).strip() return text or None @@ -45,14 +48,11 @@ def canonicalize_stable_json_v1(value: Any) -> str: def encode(v: Any) -> str: if v is None: return "null" - value_type = type(v) - if value_type is str: return json.dumps(v, ensure_ascii=False) if value_type is bool: return "true" if v else "false" - if value_type in (int, float): if isinstance(v, float): if v != v or v in (float("inf"), float("-inf")): @@ -60,13 +60,10 @@ def encode(v: Any) -> str: if v == 0.0 and str(v).startswith("-"): return "0" return str(v) - if value_type in (complex, bytes, bytearray): raise ValueError(f"canonicalize: unsupported type {value_type.__name__}") - if isinstance(v, list): return "[" + ",".join(encode(item) for item in v) + "]" - if isinstance(v, dict): out: list[str] = [] for key in sorted(v.keys()): @@ -75,7 +72,6 @@ def encode(v: Any) -> str: raise ValueError(f'canonicalize: unsupported value for key "{key}"') out.append(f"{json.dumps(str(key), ensure_ascii=False)}:{encode(val)}") return "{" + ",".join(out) + "}" - raise ValueError(f"canonicalize: unsupported type {value_type.__name__}") return encode(value) @@ -90,22 +86,18 @@ def parse_ed25519_pubkey(text: str) -> bytes: match = _ED25519_PREFIX_RE.match(candidate) if match: candidate = match.group(1).strip() - if _ED25519_HEX_RE.match(candidate): hex_part = candidate[2:] if candidate.startswith("0x") else candidate decoded = bytes.fromhex(hex_part) if len(decoded) != 32: raise ValueError("invalid ed25519 pubkey length") return decoded - try: decoded = base64.b64decode(candidate, validate=True) except Exception as err: # noqa: BLE001 raise ValueError("invalid base64 in ed25519 pubkey") from err - if len(decoded) != 32: raise ValueError("invalid base64 ed25519 pubkey length (need 32 bytes)") - return decoded @@ -116,15 +108,12 @@ def verify_ed25519_signature_over_utf8_hash_string( ) -> bool: if len(pubkey32) != 32: raise ValueError("ed25519: pubkey must be 32 bytes") - try: signature = base64.b64decode(signature_b64, validate=True) except Exception as err: # noqa: BLE001 raise ValueError("ed25519: signature must be valid base64") from err - if len(signature) != 64: raise ValueError("ed25519: signature must be 64 bytes") - verify_key = VerifyKey(pubkey32) try: verify_key.verify(hash_hex.encode("utf-8"), signature) @@ -141,28 +130,22 @@ def resolve_signer_key( ) -> SignerKeyResolution: if not rpc_url: raise ValueError("rpcUrl is required for ENS verification") - txt_resolver = resolver or Web3EnsTextResolver(rpc_url) - signer_name = txt_resolver.get_text(name, "cl.receipt.signer") if not signer_name: raise ValueError(f"ENS TXT cl.receipt.signer missing for agent ENS name: {name}") - pub_key_text = txt_resolver.get_text(signer_name, "cl.sig.pub") if not pub_key_text: raise ValueError(f"ENS TXT cl.sig.pub missing for signer ENS name: {signer_name}") - kid = txt_resolver.get_text(signer_name, "cl.sig.kid") if not kid: raise ValueError(f"ENS TXT cl.sig.kid missing for signer ENS name: {signer_name}") - try: raw_public_key_bytes = parse_ed25519_pubkey(pub_key_text) except ValueError as err: raise ValueError( f"ENS TXT cl.sig.pub malformed for signer ENS name: {signer_name}. {err}" ) from err - return SignerKeyResolution( algorithm="ed25519", kid=kid, @@ -170,16 +153,20 @@ def resolve_signer_key( ) -def to_unsigned_receipt(receipt: Receipt) -> Receipt: - if not isinstance(receipt, dict): - raise ValueError("receipt must be an object") +def _extract_receipt(receipt: CanonicalReceipt | CommandResponse) -> CanonicalReceipt: + if isinstance(receipt, dict) and isinstance(receipt.get("receipt"), dict): + return dict(receipt["receipt"]) + return dict(receipt) - unsigned = copy.deepcopy(receipt) +def to_unsigned_receipt(receipt: CanonicalReceipt | CommandResponse) -> CanonicalReceipt: + target = _extract_receipt(receipt) + if not isinstance(target, dict): + raise ValueError("receipt must be an object") + unsigned = copy.deepcopy(target) metadata = unsigned.get("metadata") if isinstance(metadata, dict): metadata.pop("receipt_id", None) - proof = metadata.get("proof") if isinstance(proof, dict): unsigned_proof: dict[str, str] = {} @@ -188,12 +175,11 @@ def to_unsigned_receipt(receipt: Receipt) -> Receipt: if isinstance(value, str): unsigned_proof[key] = value metadata["proof"] = unsigned_proof - unsigned.pop("receipt_id", None) return unsigned -def recompute_receipt_hash_sha256(receipt: Receipt) -> dict[str, str]: +def recompute_receipt_hash_sha256(receipt: CanonicalReceipt | CommandResponse) -> dict[str, str]: unsigned = to_unsigned_receipt(receipt) canonical = canonicalize_stable_json_v1(unsigned) return {"canonical": canonical, "hash_sha256": sha256_hex_utf8(canonical)} @@ -204,17 +190,15 @@ def _extract_rpc_url(ens: EnsVerifyOptions) -> str: def verify_receipt( - receipt: Receipt, + receipt: CanonicalReceipt | CommandResponse, public_key: str | None = None, ens: EnsVerifyOptions | None = None, ) -> VerifyResult: + target = _extract_receipt(receipt) try: proof = ( - ((receipt.get("metadata") or {}).get("proof") or {}) - if isinstance(receipt, dict) - else {} + ((target.get("metadata") or {}).get("proof") or {}) if isinstance(target, dict) else {} ) - claimed_hash = ( proof.get("hash_sha256") if isinstance(proof.get("hash_sha256"), str) else None ) @@ -224,20 +208,12 @@ def verify_receipt( alg = proof.get("alg") if isinstance(proof.get("alg"), str) else None canonical = proof.get("canonical") if isinstance(proof.get("canonical"), str) else None signer_id = proof.get("signer_id") if isinstance(proof.get("signer_id"), str) else None - alg_matches = alg == "ed25519-sha256" canonical_matches = canonical == "cl-stable-json-v1" - - recomputed_hash = recompute_receipt_hash_sha256(receipt)["hash_sha256"] + recomputed_hash = recompute_receipt_hash_sha256(target)["hash_sha256"] hash_matches = bool(claimed_hash and claimed_hash == recomputed_hash) - - receipt_id_value: Any = None - if isinstance(receipt, dict): - metadata = receipt.get("metadata") - if isinstance(metadata, dict): - receipt_id_value = metadata.get("receipt_id") - receipt_id_value = receipt_id_value or receipt.get("receipt_id") - + metadata = target.get("metadata") if isinstance(target, dict) else None + receipt_id_value = metadata.get("receipt_id") if isinstance(metadata, dict) else None receipt_id = receipt_id_value if isinstance(receipt_id_value, str) else None receipt_id_matches = bool(claimed_hash and receipt_id == claimed_hash) @@ -264,7 +240,6 @@ def verify_receipt( signature_valid = False signature_error: str | None = None - if not alg_matches: signature_error = f'proof.alg must be "ed25519-sha256" (got {alg})' elif not canonical_matches: @@ -285,16 +260,12 @@ def verify_receipt( except Exception as err: # noqa: BLE001 signature_error = str(err) - ok = ( - alg_matches + return { + "ok": alg_matches and canonical_matches and hash_matches and receipt_id_matches - and signature_valid - ) - - return { - "ok": ok, + and signature_valid, "checks": { "hash_matches": hash_matches, "signature_valid": signature_valid, @@ -303,8 +274,8 @@ def verify_receipt( "canonical_matches": canonical_matches, }, "values": { - "verb": ((receipt.get("x402") or {}).get("verb")) - if isinstance(receipt, dict) + "verb": ((target.get("x402") or {}).get("verb")) + if isinstance(target, dict) else None, "signer_id": signer_id, "alg": alg, @@ -322,6 +293,10 @@ def verify_receipt( }, } except Exception as err: # noqa: BLE001 + proof = ( + ((target.get("metadata") or {}).get("proof") or {}) if isinstance(target, dict) else {} + ) + metadata = target.get("metadata") if isinstance(target, dict) else None return { "ok": False, "checks": { @@ -332,35 +307,23 @@ def verify_receipt( "canonical_matches": False, }, "values": { - "verb": ((receipt.get("x402") or {}).get("verb")) - if isinstance(receipt, dict) + "verb": ((target.get("x402") or {}).get("verb")) + if isinstance(target, dict) + else None, + "signer_id": proof.get("signer_id") + if isinstance(proof.get("signer_id"), str) + else None, + "alg": proof.get("alg") if isinstance(proof.get("alg"), str) else None, + "canonical": proof.get("canonical") + if isinstance(proof.get("canonical"), str) + else None, + "claimed_hash": proof.get("hash_sha256") + if isinstance(proof.get("hash_sha256"), str) else None, - "signer_id": ( - (((receipt.get("metadata") or {}).get("proof") or {}).get("signer_id")) - if isinstance(receipt, dict) - else None - ), - "alg": ( - (((receipt.get("metadata") or {}).get("proof") or {}).get("alg")) - if isinstance(receipt, dict) - else None - ), - "canonical": ( - (((receipt.get("metadata") or {}).get("proof") or {}).get("canonical")) - if isinstance(receipt, dict) - else None - ), - "claimed_hash": ( - (((receipt.get("metadata") or {}).get("proof") or {}).get("hash_sha256")) - if isinstance(receipt, dict) - else None - ), "recomputed_hash": None, - "receipt_id": ( - ((receipt.get("metadata") or {}).get("receipt_id")) - if isinstance(receipt, dict) - else None - ), + "receipt_id": metadata.get("receipt_id") + if isinstance(metadata, dict) and isinstance(metadata.get("receipt_id"), str) + else None, "pubkey_source": None, "ens_txt_key": None, }, diff --git a/python-sdk/docs/client.md b/python-sdk/docs/client.md index b8d6704..811d7f3 100644 --- a/python-sdk/docs/client.md +++ b/python-sdk/docs/client.md @@ -4,13 +4,26 @@ `CommandLayerClient(runtime, actor, timeout_ms, headers, retries, verify_receipts, verify)` -- `runtime`: Base runtime URL. -- `actor`: Default actor ID used in requests. -- `timeout_ms`: Request timeout. -- `headers`: Additional request headers. -- `retries`: Retry count for transport/timeout errors. -- `verify_receipts`: If true, verify every returned receipt. -- `verify`: Verification options (`public_key` / `ens`). +- `runtime`: Runtime base URL. Defaults to `https://runtime.commandlayer.org`. +- `actor`: Actor ID attached to requests. +- `timeout_ms`: Request timeout in milliseconds. +- `headers`: Additional headers. +- `retries`: Retry count for transport and timeout failures. +- `verify_receipts`: Verify every returned canonical receipt before returning. +- `verify`: Verification options (`public_key`/`publicKey` or `ens`). + +## Return shape + +Every verb method returns: + +```python +{ + "receipt": {...}, + "runtime_metadata": {...}, # optional +} +``` + +`receipt` is the canonical signed payload. `runtime_metadata` is execution context and is not part of the signed receipt hash. ## Verbs diff --git a/python-sdk/docs/getting-started.md b/python-sdk/docs/getting-started.md index 399ca7a..5977a35 100644 --- a/python-sdk/docs/getting-started.md +++ b/python-sdk/docs/getting-started.md @@ -12,11 +12,11 @@ pip install commandlayer from commandlayer import create_client client = create_client(actor="my-app") -receipt = client.summarize(content="Hello world", style="bullet_points") -print(receipt["status"]) +response = client.summarize(content="Hello world", style="bullet_points") +print(response["receipt"]["status"]) ``` -## Verify receipts (recommended in production) +## Verify receipts in production ```python from commandlayer import CommandLayerClient diff --git a/python-sdk/docs/verification.md b/python-sdk/docs/verification.md index b68d58b..d0c2792 100644 --- a/python-sdk/docs/verification.md +++ b/python-sdk/docs/verification.md @@ -1,10 +1,12 @@ # Verification -The SDK verifies signed receipts using: +The SDK verifies canonical signed receipts using: - canonical JSON: `cl-stable-json-v1` -- hash: `sha256` over unsigned receipt -- signature: `ed25519` over the hash string +- hash: `sha256` over the unsigned receipt +- signature: `ed25519` over the resulting hash string + +`runtime_metadata` is not part of the signed payload. ## ENS key resolution flow @@ -19,6 +21,9 @@ Use `resolve_signer_key(name, rpc_url)` for direct key resolution. ```python from commandlayer import verify_receipt -result = verify_receipt(receipt, ens={"name": "summarizeagent.eth", "rpcUrl": "https://..."}) +result = verify_receipt( + response["receipt"], + ens={"name": "summarizeagent.eth", "rpcUrl": "https://..."}, +) print(result["ok"]) ``` diff --git a/python-sdk/pyproject.toml b/python-sdk/pyproject.toml index 0de7d4e..16f2b59 100644 --- a/python-sdk/pyproject.toml +++ b/python-sdk/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta" [project] name = "commandlayer" -version = "1.0.0" +version = "1.1.0" description = "CommandLayer Python SDK — semantic verbs, signed receipts, and verification helpers." readme = "README.md" requires-python = ">=3.10" license = { text = "MIT" } authors = [{ name = "CommandLayer", email = "security@commandlayer.org" }] -keywords = ["commandlayer", "agents", "receipts", "x402", "ens", "sdk"] +keywords = ["commandlayer", "agents", "receipts", "protocol-commons", "ens", "sdk"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -41,9 +41,9 @@ dev = [ [project.urls] Homepage = "https://commandlayer.org" -Documentation = "https://commandlayer.org/docs.html" -Repository = "https://github.com/commandlayer" -Issues = "https://github.com/commandlayer/issues" +Documentation = "https://github.com/commandlayer/sdk/tree/main/python-sdk" +Repository = "https://github.com/commandlayer/sdk" +Issues = "https://github.com/commandlayer/sdk/issues" [tool.setuptools.packages.find] where = ["."] diff --git a/python-sdk/tests/test_client.py b/python-sdk/tests/test_client.py index ae12b07..a894b8b 100644 --- a/python-sdk/tests/test_client.py +++ b/python-sdk/tests/test_client.py @@ -32,9 +32,14 @@ def handler(request: httpx.Request) -> httpx.Response: return httpx.Response( 200, json={ - "status": "success", - "x402": {"verb": "summarize"}, - "metadata": {"proof": {"alg": "ed25519-sha256", "canonical": "cl-stable-json-v1"}}, + "receipt": { + "status": "success", + "x402": {"verb": "summarize"}, + "metadata": { + "proof": {"alg": "ed25519-sha256", "canonical": "cl-stable-json-v1"} + }, + }, + "runtime_metadata": {"duration_ms": 12}, }, ) @@ -43,13 +48,15 @@ def handler(request: httpx.Request) -> httpx.Response: runtime="https://runtime.commandlayer.org", actor="tester", http_client=http ) - client.summarize(content="hello", style="bullet_points") + response = client.summarize(content="hello", style="bullet_points") - assert captured["url"] == "https://runtime.commandlayer.org/summarize/v1.0.0" + assert captured["url"] == "https://runtime.commandlayer.org/summarize/v1.1.0" sent = captured["json"] assert isinstance(sent, dict) assert sent["actor"] == "tester" assert sent["x402"]["verb"] == "summarize" + assert response["receipt"]["status"] == "success" + assert response["runtime_metadata"]["duration_ms"] == 12 def test_client_surfaces_error_message() -> None: @@ -66,7 +73,9 @@ def handler(_: httpx.Request) -> httpx.Response: def test_client_verify_receipts_failure(monkeypatch: pytest.MonkeyPatch) -> None: def handler(_: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"status": "success", "metadata": {"proof": {}}}) + return httpx.Response( + 200, json={"receipt": {"status": "success", "metadata": {"proof": {}}}} + ) monkeypatch.setattr( "commandlayer.client.verify_receipt", diff --git a/python-sdk/tests/test_verification.py b/python-sdk/tests/test_verification.py index cfff59f..6803fa4 100644 --- a/python-sdk/tests/test_verification.py +++ b/python-sdk/tests/test_verification.py @@ -33,6 +33,12 @@ def test_valid_receipt_verifies() -> None: assert result["ok"] is True +def test_valid_envelope_verifies() -> None: + receipt = load_fixture("receipt_valid.json") + result = verify_receipt({"receipt": receipt}, public_key=f"ed25519:{load_pubkey()}") + assert result["ok"] is True + + def test_invalid_signature_fails() -> None: receipt = load_fixture("receipt_invalid_sig.json") result = verify_receipt(receipt, public_key=f"ed25519:{load_pubkey()}") @@ -59,10 +65,7 @@ def test_malformed_pubkey_fails() -> None: def test_wrong_kid_detected() -> None: receipt = load_fixture("receipt_wrong_kid.json") - assert receipt["kid"] != "v1" assert receipt["kid"] == "v2" - - # Protocol-level key id policy check for SDK callers. with pytest.raises(ValueError, match="Unknown key id"): if receipt["kid"] != "v1": raise ValueError("Unknown key id") diff --git a/runtime/tests/receipt-verification.test.mjs b/runtime/tests/receipt-verification.test.mjs index 746aafd..83881c3 100644 --- a/runtime/tests/receipt-verification.test.mjs +++ b/runtime/tests/receipt-verification.test.mjs @@ -9,7 +9,7 @@ const { verifyReceipt } = require("../../typescript-sdk/dist/index.cjs"); const publicKey = `ed25519:${loadTextFixture("public_key_base64.txt")}`; async function verifyReceiptWithKid(receipt) { - if (receipt.kid !== "v1") { + if (receipt.kid && receipt.kid !== "v1") { return { valid: false, error: "Unknown key id" }; } const result = await verifyReceipt(receipt, { publicKey }); diff --git a/test_vectors/expected_hash.txt b/test_vectors/expected_hash.txt index 556f52a..729e04e 100644 --- a/test_vectors/expected_hash.txt +++ b/test_vectors/expected_hash.txt @@ -1 +1 @@ -aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a +915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1 diff --git a/test_vectors/public_key_base64.txt b/test_vectors/public_key_base64.txt index 8efd023..5a46896 100644 --- a/test_vectors/public_key_base64.txt +++ b/test_vectors/public_key_base64.txt @@ -1 +1 @@ -6kpsY+KcUgq+9VB7Ey7F+ZVHdq6+vnuSQh7qaRRG0iw= +jgkIXxvTfIC0U08Xp181TaltbLtMbSq/yJ9JRPS8wNE= diff --git a/test_vectors/receipt_invalid_sig.json b/test_vectors/receipt_invalid_sig.json index 1e2d859..a977fe0 100644 --- a/test_vectors/receipt_invalid_sig.json +++ b/test_vectors/receipt_invalid_sig.json @@ -1,17 +1,10 @@ { - "issuer": "parseagent.eth", - "verb": "summarize", - "version": "1.0.0", - "timestamp": "2026-01-01T00:00:00Z", - "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", - "receipt_hash": "", - "alg": "ed25519-sha256", "kid": "v1", - "sig": "", + "status": "success", "x402": { "verb": "summarize", - "version": "1.0.0", - "entry": "x402://parseagent.eth/summarize/v1.0.0" + "version": "1.1.0", + "entry": "x402://parseagent.eth/summarize/v1.1.0" }, "result": { "summary": "fixture" @@ -21,9 +14,9 @@ "alg": "ed25519-sha256", "canonical": "cl-stable-json-v1", "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", + "hash_sha256": "915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1", "signature_b64": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, - "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" + "receipt_id": "915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1" } } diff --git a/test_vectors/receipt_malformed_pubkey.json b/test_vectors/receipt_malformed_pubkey.json index 339d679..0350f6d 100644 --- a/test_vectors/receipt_malformed_pubkey.json +++ b/test_vectors/receipt_malformed_pubkey.json @@ -1,17 +1,10 @@ { - "issuer": "parseagent.eth", - "verb": "summarize", - "version": "1.0.0", - "timestamp": "2026-01-01T00:00:00Z", - "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", - "receipt_hash": "", - "alg": "ed25519-sha256", "kid": "v1", - "sig": "", + "status": "success", "x402": { "verb": "summarize", - "version": "1.0.0", - "entry": "x402://parseagent.eth/summarize/v1.0.0" + "version": "1.1.0", + "entry": "x402://parseagent.eth/summarize/v1.1.0" }, "result": { "summary": "fixture" @@ -21,9 +14,9 @@ "alg": "ed25519-sha256", "canonical": "cl-stable-json-v1", "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", - "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" + "hash_sha256": "915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1", + "signature_b64": "JwwLYGdplh7Yy5U42DAov6V30trEdRbA5Qzh8owG6X0DkGl2/TsaOhtWRJU9Z1uykJVeDFdqijR9ZqoktqjhAg==" }, - "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" + "receipt_id": "915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1" } } diff --git a/test_vectors/receipt_valid.json b/test_vectors/receipt_valid.json index 339d679..0350f6d 100644 --- a/test_vectors/receipt_valid.json +++ b/test_vectors/receipt_valid.json @@ -1,17 +1,10 @@ { - "issuer": "parseagent.eth", - "verb": "summarize", - "version": "1.0.0", - "timestamp": "2026-01-01T00:00:00Z", - "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", - "receipt_hash": "", - "alg": "ed25519-sha256", "kid": "v1", - "sig": "", + "status": "success", "x402": { "verb": "summarize", - "version": "1.0.0", - "entry": "x402://parseagent.eth/summarize/v1.0.0" + "version": "1.1.0", + "entry": "x402://parseagent.eth/summarize/v1.1.0" }, "result": { "summary": "fixture" @@ -21,9 +14,9 @@ "alg": "ed25519-sha256", "canonical": "cl-stable-json-v1", "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", - "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" + "hash_sha256": "915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1", + "signature_b64": "JwwLYGdplh7Yy5U42DAov6V30trEdRbA5Qzh8owG6X0DkGl2/TsaOhtWRJU9Z1uykJVeDFdqijR9ZqoktqjhAg==" }, - "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" + "receipt_id": "915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1" } } diff --git a/test_vectors/receipt_valid_v1.json b/test_vectors/receipt_valid_v1.json index 339d679..0350f6d 100644 --- a/test_vectors/receipt_valid_v1.json +++ b/test_vectors/receipt_valid_v1.json @@ -1,17 +1,10 @@ { - "issuer": "parseagent.eth", - "verb": "summarize", - "version": "1.0.0", - "timestamp": "2026-01-01T00:00:00Z", - "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", - "receipt_hash": "", - "alg": "ed25519-sha256", "kid": "v1", - "sig": "", + "status": "success", "x402": { "verb": "summarize", - "version": "1.0.0", - "entry": "x402://parseagent.eth/summarize/v1.0.0" + "version": "1.1.0", + "entry": "x402://parseagent.eth/summarize/v1.1.0" }, "result": { "summary": "fixture" @@ -21,9 +14,9 @@ "alg": "ed25519-sha256", "canonical": "cl-stable-json-v1", "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", - "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" + "hash_sha256": "915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1", + "signature_b64": "JwwLYGdplh7Yy5U42DAov6V30trEdRbA5Qzh8owG6X0DkGl2/TsaOhtWRJU9Z1uykJVeDFdqijR9ZqoktqjhAg==" }, - "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" + "receipt_id": "915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1" } } diff --git a/test_vectors/receipt_wrong_kid.json b/test_vectors/receipt_wrong_kid.json index 8b303f6..e0bc10e 100644 --- a/test_vectors/receipt_wrong_kid.json +++ b/test_vectors/receipt_wrong_kid.json @@ -1,17 +1,10 @@ { - "issuer": "parseagent.eth", - "verb": "summarize", - "version": "1.0.0", - "timestamp": "2026-01-01T00:00:00Z", - "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", - "receipt_hash": "", - "alg": "ed25519-sha256", "kid": "v2", - "sig": "", + "status": "success", "x402": { "verb": "summarize", - "version": "1.0.0", - "entry": "x402://parseagent.eth/summarize/v1.0.0" + "version": "1.1.0", + "entry": "x402://parseagent.eth/summarize/v1.1.0" }, "result": { "summary": "fixture" @@ -21,9 +14,9 @@ "alg": "ed25519-sha256", "canonical": "cl-stable-json-v1", "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", - "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" + "hash_sha256": "915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1", + "signature_b64": "JwwLYGdplh7Yy5U42DAov6V30trEdRbA5Qzh8owG6X0DkGl2/TsaOhtWRJU9Z1uykJVeDFdqijR9ZqoktqjhAg==" }, - "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" + "receipt_id": "915ce5bd5d71ac3622a1d3918630dd69ff0d6d22218de84a751817ae60bd54e1" } } diff --git a/typescript-sdk/README.md b/typescript-sdk/README.md index 4359154..2c3a7f8 100644 --- a/typescript-sdk/README.md +++ b/typescript-sdk/README.md @@ -1,331 +1,119 @@ # CommandLayer TypeScript SDK -Semantic verbs. Typed schemas. Signed receipts. +Official TypeScript/JavaScript SDK for CommandLayer Commons v1.1.0. -This package provides the official TypeScript/JavaScript SDK for **CommandLayer Commons v1.0.0**. +Use this package to: +- call CommandLayer Commons verbs, +- receive a canonical signed receipt, +- capture optional runtime metadata separately, +- verify receipts offline or through ENS, and +- reproduce calls from the CLI. -Install → call a verb → receive a signed receipt → verify it. - ---- - -## Overview - -CommandLayer is the semantic verb layer for autonomous agents. - -The SDK provides: - -- Standardized Commons verbs (`summarize`, `analyze`, `fetch`, etc.) -- Strict JSON Schemas (requests + receipts) -- Cryptographically signed receipts (Ed25519 + SHA-256) -- Deterministic canonicalization (`cl-stable-json-v1`) -- Verification helpers (offline or ENS-based) -- CLI for reproducible local testing - ---- - -## Installation +## Install ```bash npm install @commandlayer/sdk ``` -### Quickstart (TypeScript) -``` -import { createClient } from "@commandlayer/sdk"; +Supported runtime: Node.js 20+. -const client = createClient({ - actor: "my-app" -}); +## Quick start -const receipt = await client.summarize({ - content: "CommandLayer turns agent actions into verifiable receipts.", - style: "bullet_points" -}); +```ts +import { createClient, verifyReceipt } from "@commandlayer/sdk"; -console.log(receipt.result.summary); -console.log(receipt.metadata.receipt_id); -``` ---- +const client = createClient({ actor: "docs-example" }); -### Runtime Configuration +const response = await client.summarize({ + content: "CommandLayer makes agent execution verifiable.", + style: "bullet_points" +}); -Default runtime: -``` -https://runtime.commandlayer.org -``` +console.log(response.receipt.result?.summary); +console.log(response.receipt.metadata?.receipt_id); +console.log(response.runtime_metadata?.duration_ms); -Override if needed: -``` -const client = createClient({ - actor: "my-app", - runtime: "https://your-runtime.example", - verifyReceipts: true +const verification = await verifyReceipt(response.receipt, { + publicKey: process.env.COMMANDLAYER_PUBLIC_KEY! }); -``` - -verifyReceipts should remain enabled in production. ---- +console.log(verification.ok); +``` -### Receipt Structure +## Return shape -Every call returns a signed receipt: +Client methods return: -``` +```json { - "status": "success", - "x402": { - "verb": "summarize", - "version": "1.0.0", - "entry": "x402://summarizeagent.eth/summarize/v1.0.0" - }, - "trace": { - "trace_id": "trace_ab12cd34", - "duration_ms": 118 - }, - "result": { - "summary": "..." - }, - "metadata": { - "receipt_id": "8f0a...", - "proof": { - "alg": "ed25519-sha256", - "canonical": "cl-stable-json-v1", - "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "...", - "signature_b64": "..." + "receipt": { + "status": "success", + "x402": { + "verb": "summarize", + "version": "1.1.0" + }, + "result": {}, + "metadata": { + "receipt_id": "...", + "proof": { + "alg": "ed25519-sha256", + "canonical": "cl-stable-json-v1", + "signer_id": "runtime.commandlayer.eth", + "hash_sha256": "...", + "signature_b64": "..." + } } + }, + "runtime_metadata": { + "trace_id": "trace_123", + "duration_ms": 118, + "provider": "runtime.commandlayer.org" } } ``` -Receipt guarantees: - -- Stable canonical hashing -- SHA-256 digest over unsigned receipt -- Ed25519 signature over the hash -- Deterministic validation across runtimes +`verifyReceipt()` accepts the canonical `receipt` object. The SDK also accepts a whole response envelope for legacy compatibility, but new integrations should pass `response.receipt` explicitly. ---- +## Verification modes -### Verifying Receipts - +### Offline -**Option A — Offline (explicit public key)** - -Fastest method. No RPC required. -``` -import { verifyReceipt } from "@commandlayer/sdk"; - -const result = await verifyReceipt(receipt, { - publicKey: "ed25519:7Vkkmt6R02Iltp/+i3D5mraZyvLjfuTSVB33KwfzQC8=" +```ts +const result = await verifyReceipt(response.receipt, { + publicKey: "ed25519:BASE64_PUBLIC_KEY" }); - -console.log(result.ok); ``` ---- - -**Option B — ENS-based Verification** - -Resolves signer metadata from ENS TXT records. - -Required ENS records: -- Agent ENS TXT: `cl.receipt.signer` -- Signer ENS TXT: `cl.sig.pub` -- Signer ENS TXT: `cl.sig.kid` +### ENS-backed -Example: -``` -import { verifyReceipt } from "@commandlayer/sdk"; - -const out = await verifyReceipt(receipt, { +```ts +const result = await verifyReceipt(response.receipt, { ens: { name: "summarizeagent.eth", - rpcUrl: process.env.ETH_RPC_URL! + rpcUrl: process.env.MAINNET_RPC_URL! } }); - -console.log(out.ok, out.values.pubkey_source); -``` - -ENS affects verification correctness — not build or publishing. - ---- - -**Commons Verbs** - -All verbs return signed receipts. -``` -await client.summarize({ content, style: "bullet_points" }); - -await client.analyze({ - content, - dimensions: ["sentiment", "tone"] -}); - -await client.classify({ - content, - categories: ["support", "billing"] -}); - -await client.clean({ - content, - operations: ["trim", "normalize_newlines"] -}); - -await client.convert({ - content, - from: "json", - to: "csv" -}); - -await client.describe({ - subject, - detail_level: "medium" -}); - -await client.explain({ - subject, - style: "step-by-step" -}); - -await client.format({ - content, - to: "table" -}); - -await client.parse({ - content, - content_type: "json" -}); - -await client.fetch({ - source: "https://example.com" -}); -``` - -See `EXAMPLES.md` for full technical payloads. - -### CLI - -The SDK includes a CLI for local testing. - -**Build First** -``` -npm run build ``` -Expected output: +The ENS flow resolves: +1. `cl.receipt.signer` on the agent ENS name, +2. `cl.sig.pub` on the signer ENS name, +3. `cl.sig.kid` on the signer ENS name. -- `dist/index.cjs` -- `dist/index.mjs` -- `dist/cli.cjs` -- `dist/index.d.ts` +## CLI -**Run** -``` -node dist/cli.cjs summarize --content "test" --style bullet_points --json -``` - -***Global Link (Optional)** -``` -npm link -commandlayer --help -commandlayer summarize --content "test" --style bullet_points -``` +The package ships the `commandlayer` CLI. -To unlink: -``` -npm unlink -g @commandlayer/sdk || true -``` -### Windows esbuild EBUSY Fix - -- If install fails with EBUSY: -- Run terminal as Administrator -- Temporarily disable Defender real-time protection -- Close processes locking `node_modules` -- Delete `node_modules` -- Retry `npm install` - -If you see: -``` -'tsup' is not recognized +```bash +commandlayer summarize --content "hello" --style bullet_points --json +commandlayer verify --file receipt.json --public-key "ed25519:BASE64_PUBLIC_KEY" ``` -`npm install` did not complete successfully. +## Development ---- - -### Local Development Workflow -``` -sdk/ - typescript-sdk/ - src/ - dist/ - bin/ -``` - -Typical flow: -``` +```bash cd typescript-sdk -npm install -npm run build -npm run test:cli-smoke -node dist/cli.cjs summarize --content "test" --style bullet_points --json +npm ci +npm run typecheck +npm test ``` ---- - -### Publishing to npm (Optional) - -Ensure `typescript-sdk/package.json` includes: - -- name -- version -- main / module / exports → dist/* -- types → dist/index.d.ts -- bin → bin/cli.js -- files → dist/ and bin/ - -Then: -``` -npm login -npm publish --access public -``` ---- - -### Versioning - -Use Semantic Versioning: - -- Patch → bug fixes -- Minor → backward-compatible additions -- Major → breaking changes - -Release flow: - -Update `CHANGELOG.md` -`npm version patch|minor|major` -`npm run build` -CLI smoke test -Tag + push - -Publish - -### Definition of Done - -You are deployed when: - -- `npm install` succeeds cleanly -- `npm run build` produces `dist/` -- CLI returns valid receipt JSON -- CI reproduces the same steps on push - ---- -License - -MIT - -CommandLayer turns agent actions into verifiable infrastructure. - -Ship APIs that can prove what they did. diff --git a/typescript-sdk/package.json b/typescript-sdk/package.json index 20079ad..26ef0ea 100644 --- a/typescript-sdk/package.json +++ b/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@commandlayer/sdk", - "version": "0.1.0", + "version": "1.1.0", "private": false, "license": "MIT", "type": "module", @@ -21,7 +21,7 @@ "dist" ], "engines": { - "node": ">=22" + "node": ">=20" }, "scripts": { "build": "tsup", @@ -30,7 +30,7 @@ "prepack": "npm run build", "test:cli-smoke": "node scripts/cli-smoke.mjs", "test:unit": "npm run build && node scripts/unit-tests.mjs && npm run test:template", - "test": "npm run test:unit && npm run test:cli-smoke", + "test": "npm run test:unit && npm run test:cli-smoke && node --test ../runtime/tests/*.mjs", "test:template": "node scripts/template-tests.mjs" }, "dependencies": { diff --git a/typescript-sdk/scripts/cli-smoke.mjs b/typescript-sdk/scripts/cli-smoke.mjs index a6c0469..9ea7aaa 100644 --- a/typescript-sdk/scripts/cli-smoke.mjs +++ b/typescript-sdk/scripts/cli-smoke.mjs @@ -1,38 +1,32 @@ import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import path from "node:path"; +import fs from "node:fs"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const sdkDir = path.resolve(__dirname, ".."); const cliPath = path.join(sdkDir, "dist", "cli.cjs"); +const fixturePath = path.resolve(sdkDir, "..", "test_vectors", "receipt_valid.json"); +const publicKey = `ed25519:${fs.readFileSync(path.resolve(sdkDir, "..", "test_vectors", "public_key_base64.txt"), "utf8").trim()}`; function runCase(name, args, expected) { - const result = spawnSync("node", [cliPath, ...args], { - cwd: sdkDir, - encoding: "utf8" - }); - + const result = spawnSync("node", [cliPath, ...args], { cwd: sdkDir, encoding: "utf8" }); const output = `${result.stdout || ""}\n${result.stderr || ""}`; - if (result.status !== expected.exitCode) { - throw new Error( - `${name}: expected exit code ${expected.exitCode}, got ${result.status}.\nOutput:\n${output}` - ); + throw new Error(`${name}: expected exit code ${expected.exitCode}, got ${result.status}.\nOutput:\n${output}`); } - for (const snippet of expected.includes) { if (!output.includes(snippet)) { throw new Error(`${name}: missing expected output snippet: "${snippet}"\nOutput:\n${output}`); } } - console.log(`PASS: ${name}`); } runCase("help output", ["--help"], { exitCode: 0, - includes: ["Usage: commandlayer", "CommandLayer TypeScript SDK CLI"] + includes: ["Usage: commandlayer", "CommandLayer CLI for calling Commons verbs and verifying signed receipts"] }); runCase("argument validation", ["summarize"], { @@ -45,4 +39,9 @@ runCase("bad JSON path", ["call", "--verb", "summarize", "--body", "{not-json}"] includes: ["commandlayer:", "Expected property name or '}' in JSON"] }); +runCase("verify fixture", ["verify", "--file", fixturePath, "--public-key", publicKey], { + exitCode: 0, + includes: ['"ok": true'] +}); + console.log("CLI smoke tests passed."); diff --git a/typescript-sdk/src/cli.ts b/typescript-sdk/src/cli.ts index fdda2b4..c8815ec 100644 --- a/typescript-sdk/src/cli.ts +++ b/typescript-sdk/src/cli.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node import { Command } from "commander"; -import { createClient, type Receipt } from "./index"; +import { createClient, verifyReceipt, type CommandResponse } from "./index"; function parseIntSafe(value: string, fallback: number): number { const n = Number(value); @@ -8,93 +8,75 @@ function parseIntSafe(value: string, fallback: number): number { } const program = new Command(); - program .name("commandlayer") - .description("CommandLayer TypeScript SDK CLI") + .description("CommandLayer CLI for calling Commons verbs and verifying signed receipts") .option("--runtime ", "CommandLayer runtime base URL", "https://runtime.commandlayer.org") .option("--actor ", "Actor id used in requests", "sdk-cli") .option("--timeout-ms ", "Request timeout in milliseconds", "30000") - .option("--json", "Print full JSON receipt", false); + .option("--json", "Print full JSON output", false); -function printResult(receipt: Receipt, jsonOutput: boolean) { +function printCommandResponse(response: CommandResponse, jsonOutput: boolean) { if (jsonOutput) { - console.log(JSON.stringify(receipt, null, 2)); + console.log(JSON.stringify(response, null, 2)); return; } - - if (receipt.status) console.log(`status: ${receipt.status}`); - if (receipt.metadata?.receipt_id) console.log(`receipt_id: ${receipt.metadata.receipt_id}`); - if (receipt.result !== undefined) { + console.log(`status: ${response.receipt.status}`); + if (response.receipt.metadata?.receipt_id) console.log(`receipt_id: ${response.receipt.metadata.receipt_id}`); + if (response.runtime_metadata?.duration_ms !== undefined) { + console.log(`duration_ms: ${response.runtime_metadata.duration_ms}`); + } + if (response.receipt.result !== undefined) { console.log("result:"); - console.log(JSON.stringify(receipt.result, null, 2)); + console.log(JSON.stringify(response.receipt.result, null, 2)); } } +function createConfiguredClient() { + const root = program.opts(); + return createClient({ runtime: root.runtime, actor: root.actor, timeoutMs: parseIntSafe(root.timeoutMs, 30_000) }); +} + function withCommonOptions(cmd: Command) { return cmd.requiredOption("--content ", "Input content").option("--max-tokens ", "Max output tokens", "1000"); } -withCommonOptions( - program - .command("summarize") - .description("Summarize content") - .option("--style