Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/parity-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: parity-check

on:
push:
paths:
- "typescript-sdk/**"
- "python-sdk/**"
- "test_vectors/**"
- "scripts/**"
- ".github/workflows/parity-check.yml"
pull_request:
paths:
- "typescript-sdk/**"
- "python-sdk/**"
- "test_vectors/**"
- "scripts/**"
- ".github/workflows/parity-check.yml"

jobs:
parity:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: typescript-sdk/package-lock.json

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install TypeScript dependencies
working-directory: typescript-sdk
run: npm ci

- name: Build TypeScript SDK
working-directory: typescript-sdk
run: npm run build

- name: Install Python dependencies
working-directory: python-sdk
run: pip install -e '.[dev]'

- name: Run cross-SDK parity validation
run: node scripts/parity-check.mjs
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Contributing

See `DEVELOPER_EXPERIENCE.md` for the current contributor workflow, local validation commands, and repo conventions.
3 changes: 3 additions & 0 deletions MAINTAINER_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Maintainer Guide

See `DEVELOPER_EXPERIENCE.md` for maintainer-facing architecture notes and `DEPLOYMENT_GUIDE.md` for release execution details.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

Official SDK repo for CommandLayer Protocol-Commons v1.1.0.

## Start Here

- Quickstart → `QUICKSTART.md`
- Full usage → `EXAMPLES.md`
- Contributing → `CONTRIBUTING.md`
- Maintainers → `MAINTAINER_GUIDE.md`
- Releases → `RELEASE_GUIDE.md`
- Test vectors → `test_vectors/README.md`
- Changelog → `CHANGELOG.md`

This repository ships the public developer surfaces for CommandLayer:
- the TypeScript SDK: `@commandlayer/sdk`,
- the Python SDK: `commandlayer`,
Expand Down
3 changes: 3 additions & 0 deletions RELEASE_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Release Guide

See `DEPLOYMENT_GUIDE.md` for the current build, release, and publish workflow.
93 changes: 93 additions & 0 deletions python-sdk/tests/parity_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from __future__ import annotations

import base64
import json
from pathlib import Path
from typing import Any

from commandlayer.verify import parse_ed25519_pubkey, recompute_receipt_hash_sha256, resolve_signer_key, verify_receipt

ROOT = Path(__file__).resolve().parents[2]
VECTORS = ROOT / "test_vectors"
MANIFEST = json.loads((VECTORS / "parity_manifest.json").read_text(encoding="utf-8"))
PUBLIC_KEY = f"ed25519:{(VECTORS / 'public_key_base64.txt').read_text(encoding='utf-8').strip()}"

ENS_FIXTURES: dict[str, dict[str, str]] = {
"parseagent.eth": {"cl.receipt.signer": "runtime.commandlayer.eth"},
"runtime.commandlayer.eth": {"cl.sig.pub": PUBLIC_KEY, "cl.sig.kid": "v1"},
"invalidagent.eth": {},
"malformed.eth": {"cl.receipt.signer": "malformed-signer.eth"},
"malformed-signer.eth": {"cl.sig.pub": "ed25519:not-base64", "cl.sig.kid": "v1"},
}


class FakeResolver:
def get_text(self, name: str, key: str) -> str | None:
return ENS_FIXTURES.get(name, {}).get(key)


resolver = FakeResolver()


def load_fixture(name: str) -> dict[str, Any]:
return json.loads((VECTORS / name).read_text(encoding="utf-8"))


vector_results: list[dict[str, Any]] = []
for vector in MANIFEST["verification_vectors"]:
receipt = load_fixture(vector["name"])
verification = verify_receipt(receipt, public_key=PUBLIC_KEY)
recomputed = recompute_receipt_hash_sha256(receipt)
vector_results.append(
{
"name": vector["name"],
"expected_ok": vector["expected_ok"],
"ok": verification["ok"],
"checks": verification["checks"],
"values": verification["values"],
"errors": verification["errors"],
"recomputed_hash": recomputed["hash_sha256"],
}
)

ens_results: list[dict[str, Any]] = []
for case in MANIFEST["ens_resolution_cases"]:
try:
resolution = resolve_signer_key(case["name"], "https://rpc.example", resolver=resolver)
signer_name = resolver.get_text(case["name"], "cl.receipt.signer")
ens_results.append(
{
"name": case["name"],
"ok": True,
"algorithm": resolution.algorithm,
"kid": resolution.kid,
"signer_name": signer_name,
"public_key_b64": base64.b64encode(resolution.raw_public_key_bytes).decode("utf-8"),
"error": None,
}
)
except Exception as exc: # noqa: BLE001
ens_results.append(
{
"name": case["name"],
"ok": False,
"algorithm": None,
"kid": None,
"signer_name": resolver.get_text(case["name"], "cl.receipt.signer"),
"public_key_b64": None,
"error": str(exc),
}
)

print(
json.dumps(
{
"sdk": "python",
"public_key_length": len(parse_ed25519_pubkey(PUBLIC_KEY)),
"vector_results": vector_results,
"ens_results": ens_results,
},
sort_keys=True,
indent=2,
)
)
155 changes: 152 additions & 3 deletions python-sdk/tests/test_public_api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,159 @@
from __future__ import annotations

from commandlayer import CommandLayerClient, create_client
import json
from pathlib import Path

import httpx

from commandlayer import (
CommandLayerClient,
canonicalize_stable_json_v1,
create_client,
normalize_command_response,
recompute_receipt_hash_sha256,
verify_receipt,
)

ROOT = Path(__file__).resolve().parents[2]
VECTORS = ROOT / "test_vectors"
EXPECTED_EXPORTS = {
"CommandLayerClient": CommandLayerClient,
"create_client": create_client,
"verify_receipt": verify_receipt,
"normalize_command_response": normalize_command_response,
"canonicalize_stable_json_v1": canonicalize_stable_json_v1,
"recompute_receipt_hash_sha256": recompute_receipt_hash_sha256,
}
EXPECTED_VERBS = [
"summarize",
"analyze",
"classify",
"clean",
"convert",
"describe",
"explain",
"format",
"parse",
"fetch",
]


def load_fixture(name: str) -> dict:
return json.loads((VECTORS / name).read_text(encoding="utf-8"))


def load_pubkey() -> str:
return f"ed25519:{(VECTORS / 'public_key_base64.txt').read_text(encoding='utf-8').strip()}"


def test_expected_symbols_are_importable() -> None:
for export_name, export_value in EXPECTED_EXPORTS.items():
assert export_value is not None, export_name



def test_create_client_accepts_basic_configuration() -> None:
client = create_client(
actor="api-user",
runtime="https://runtime.example",
timeout_ms=12_345,
headers={"X-Test": "1"},
verify_receipts=False,
)

def test_create_client_factory() -> None:
client = create_client(actor="api-user")
assert isinstance(client, CommandLayerClient)
assert client.actor == "api-user"
assert client.runtime == "https://runtime.example"
assert client.timeout_ms == 12_345
assert client.default_headers["X-Test"] == "1"
client.close()



def test_public_client_verbs_exist_and_are_callable() -> None:
client = create_client(actor="verb-check")
try:
for verb in EXPECTED_VERBS:
method = getattr(client, verb)
assert callable(method), verb
finally:
client.close()



def test_mocked_client_response_matches_public_envelope_shape() -> None:
def handler(_: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
json={
"receipt": {
"status": "success",
"x402": {"verb": "summarize", "version": "1.1.0"},
"result": {"summary": "done"},
"metadata": {
"proof": {
"alg": "ed25519-sha256",
"canonical": "cl-stable-json-v1",
}
},
},
"runtime_metadata": {"duration_ms": 7, "provider": "mock-runtime"},
},
)

client = create_client(
actor="shape-check",
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
)

try:
response = client.summarize(content="Hello", style="bullet_points")
finally:
client.close()

assert set(response.keys()) == {"receipt", "runtime_metadata"}
assert response["receipt"]["x402"]["verb"] == "summarize"
assert response["receipt"]["result"]["summary"] == "done"
assert response["runtime_metadata"]["duration_ms"] == 7



def test_verify_receipt_is_importable_callable_and_matches_vector_contract() -> None:
receipt = load_fixture("receipt_valid.json")

result = verify_receipt(receipt, public_key=load_pubkey())

assert callable(verify_receipt)
assert result["ok"] is True
assert result["values"]["recomputed_hash"] == recompute_receipt_hash_sha256(receipt)["hash_sha256"]
assert result["values"]["signer_id"] == "runtime.commandlayer.eth"
assert result["errors"]["verify_error"] is None



def test_mocked_end_to_end_flow_uses_vector_shaped_response() -> None:
receipt = load_fixture("receipt_valid.json")

def handler(_: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
json={
"receipt": receipt,
"runtime_metadata": {"duration_ms": 11, "provider": "mock-runtime"},
},
)

client = create_client(
actor="vector-flow",
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
)

try:
response = client.analyze(content="vector-backed", goal="parity")
finally:
client.close()

assert response["receipt"] == receipt
assert response["runtime_metadata"]["provider"] == "mock-runtime"
verification = verify_receipt(response["receipt"], public_key=load_pubkey())
assert verification["ok"] is True
Loading
Loading