Skip to content

Commit ac9d373

Browse files
authored
Merge pull request #15 from commandlayer/codex/add-cross-sdk-parity-validation
Add cross-SDK parity validation, expand Python public API tests, and add repo Start Here map
2 parents 58ba956 + cdc85f4 commit ac9d373

File tree

12 files changed

+580
-6
lines changed

12 files changed

+580
-6
lines changed

.github/workflows/parity-check.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: parity-check
2+
3+
on:
4+
push:
5+
paths:
6+
- "typescript-sdk/**"
7+
- "python-sdk/**"
8+
- "test_vectors/**"
9+
- "scripts/**"
10+
- ".github/workflows/parity-check.yml"
11+
pull_request:
12+
paths:
13+
- "typescript-sdk/**"
14+
- "python-sdk/**"
15+
- "test_vectors/**"
16+
- "scripts/**"
17+
- ".github/workflows/parity-check.yml"
18+
19+
jobs:
20+
parity:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@v4
25+
26+
- name: Setup Node
27+
uses: actions/setup-node@v4
28+
with:
29+
node-version: 20
30+
cache: npm
31+
cache-dependency-path: typescript-sdk/package-lock.json
32+
33+
- name: Setup Python
34+
uses: actions/setup-python@v5
35+
with:
36+
python-version: "3.11"
37+
38+
- name: Install TypeScript dependencies
39+
working-directory: typescript-sdk
40+
run: npm ci
41+
42+
- name: Build TypeScript SDK
43+
working-directory: typescript-sdk
44+
run: npm run build
45+
46+
- name: Install Python dependencies
47+
working-directory: python-sdk
48+
run: pip install -e '.[dev]'
49+
50+
- name: Run cross-SDK parity validation
51+
run: node scripts/parity-check.mjs

CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Contributing
2+
3+
See `DEVELOPER_EXPERIENCE.md` for the current contributor workflow, local validation commands, and repo conventions.

MAINTAINER_GUIDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Maintainer Guide
2+
3+
See `DEVELOPER_EXPERIENCE.md` for maintainer-facing architecture notes and `DEPLOYMENT_GUIDE.md` for release execution details.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

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

5+
## Start Here
6+
7+
- Quickstart → `QUICKSTART.md`
8+
- Full usage → `EXAMPLES.md`
9+
- Contributing → `CONTRIBUTING.md`
10+
- Maintainers → `MAINTAINER_GUIDE.md`
11+
- Releases → `RELEASE_GUIDE.md`
12+
- Test vectors → `test_vectors/README.md`
13+
- Changelog → `CHANGELOG.md`
14+
515
This repository ships the public developer surfaces for CommandLayer:
616
- the TypeScript SDK: `@commandlayer/sdk`,
717
- the Python SDK: `commandlayer`,

RELEASE_GUIDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Release Guide
2+
3+
See `DEPLOYMENT_GUIDE.md` for the current build, release, and publish workflow.

python-sdk/tests/parity_report.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import annotations
2+
3+
import base64
4+
import json
5+
from pathlib import Path
6+
from typing import Any
7+
8+
from commandlayer.verify import parse_ed25519_pubkey, recompute_receipt_hash_sha256, resolve_signer_key, verify_receipt
9+
10+
ROOT = Path(__file__).resolve().parents[2]
11+
VECTORS = ROOT / "test_vectors"
12+
MANIFEST = json.loads((VECTORS / "parity_manifest.json").read_text(encoding="utf-8"))
13+
PUBLIC_KEY = f"ed25519:{(VECTORS / 'public_key_base64.txt').read_text(encoding='utf-8').strip()}"
14+
15+
ENS_FIXTURES: dict[str, dict[str, str]] = {
16+
"parseagent.eth": {"cl.receipt.signer": "runtime.commandlayer.eth"},
17+
"runtime.commandlayer.eth": {"cl.sig.pub": PUBLIC_KEY, "cl.sig.kid": "v1"},
18+
"invalidagent.eth": {},
19+
"malformed.eth": {"cl.receipt.signer": "malformed-signer.eth"},
20+
"malformed-signer.eth": {"cl.sig.pub": "ed25519:not-base64", "cl.sig.kid": "v1"},
21+
}
22+
23+
24+
class FakeResolver:
25+
def get_text(self, name: str, key: str) -> str | None:
26+
return ENS_FIXTURES.get(name, {}).get(key)
27+
28+
29+
resolver = FakeResolver()
30+
31+
32+
def load_fixture(name: str) -> dict[str, Any]:
33+
return json.loads((VECTORS / name).read_text(encoding="utf-8"))
34+
35+
36+
vector_results: list[dict[str, Any]] = []
37+
for vector in MANIFEST["verification_vectors"]:
38+
receipt = load_fixture(vector["name"])
39+
verification = verify_receipt(receipt, public_key=PUBLIC_KEY)
40+
recomputed = recompute_receipt_hash_sha256(receipt)
41+
vector_results.append(
42+
{
43+
"name": vector["name"],
44+
"expected_ok": vector["expected_ok"],
45+
"ok": verification["ok"],
46+
"checks": verification["checks"],
47+
"values": verification["values"],
48+
"errors": verification["errors"],
49+
"recomputed_hash": recomputed["hash_sha256"],
50+
}
51+
)
52+
53+
ens_results: list[dict[str, Any]] = []
54+
for case in MANIFEST["ens_resolution_cases"]:
55+
try:
56+
resolution = resolve_signer_key(case["name"], "https://rpc.example", resolver=resolver)
57+
signer_name = resolver.get_text(case["name"], "cl.receipt.signer")
58+
ens_results.append(
59+
{
60+
"name": case["name"],
61+
"ok": True,
62+
"algorithm": resolution.algorithm,
63+
"kid": resolution.kid,
64+
"signer_name": signer_name,
65+
"public_key_b64": base64.b64encode(resolution.raw_public_key_bytes).decode("utf-8"),
66+
"error": None,
67+
}
68+
)
69+
except Exception as exc: # noqa: BLE001
70+
ens_results.append(
71+
{
72+
"name": case["name"],
73+
"ok": False,
74+
"algorithm": None,
75+
"kid": None,
76+
"signer_name": resolver.get_text(case["name"], "cl.receipt.signer"),
77+
"public_key_b64": None,
78+
"error": str(exc),
79+
}
80+
)
81+
82+
print(
83+
json.dumps(
84+
{
85+
"sdk": "python",
86+
"public_key_length": len(parse_ed25519_pubkey(PUBLIC_KEY)),
87+
"vector_results": vector_results,
88+
"ens_results": ens_results,
89+
},
90+
sort_keys=True,
91+
indent=2,
92+
)
93+
)
Lines changed: 152 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,159 @@
11
from __future__ import annotations
22

3-
from commandlayer import CommandLayerClient, create_client
3+
import json
4+
from pathlib import Path
45

6+
import httpx
7+
8+
from commandlayer import (
9+
CommandLayerClient,
10+
canonicalize_stable_json_v1,
11+
create_client,
12+
normalize_command_response,
13+
recompute_receipt_hash_sha256,
14+
verify_receipt,
15+
)
16+
17+
ROOT = Path(__file__).resolve().parents[2]
18+
VECTORS = ROOT / "test_vectors"
19+
EXPECTED_EXPORTS = {
20+
"CommandLayerClient": CommandLayerClient,
21+
"create_client": create_client,
22+
"verify_receipt": verify_receipt,
23+
"normalize_command_response": normalize_command_response,
24+
"canonicalize_stable_json_v1": canonicalize_stable_json_v1,
25+
"recompute_receipt_hash_sha256": recompute_receipt_hash_sha256,
26+
}
27+
EXPECTED_VERBS = [
28+
"summarize",
29+
"analyze",
30+
"classify",
31+
"clean",
32+
"convert",
33+
"describe",
34+
"explain",
35+
"format",
36+
"parse",
37+
"fetch",
38+
]
39+
40+
41+
def load_fixture(name: str) -> dict:
42+
return json.loads((VECTORS / name).read_text(encoding="utf-8"))
43+
44+
45+
def load_pubkey() -> str:
46+
return f"ed25519:{(VECTORS / 'public_key_base64.txt').read_text(encoding='utf-8').strip()}"
47+
48+
49+
def test_expected_symbols_are_importable() -> None:
50+
for export_name, export_value in EXPECTED_EXPORTS.items():
51+
assert export_value is not None, export_name
52+
53+
54+
55+
def test_create_client_accepts_basic_configuration() -> None:
56+
client = create_client(
57+
actor="api-user",
58+
runtime="https://runtime.example",
59+
timeout_ms=12_345,
60+
headers={"X-Test": "1"},
61+
verify_receipts=False,
62+
)
563

6-
def test_create_client_factory() -> None:
7-
client = create_client(actor="api-user")
864
assert isinstance(client, CommandLayerClient)
965
assert client.actor == "api-user"
66+
assert client.runtime == "https://runtime.example"
67+
assert client.timeout_ms == 12_345
68+
assert client.default_headers["X-Test"] == "1"
1069
client.close()
70+
71+
72+
73+
def test_public_client_verbs_exist_and_are_callable() -> None:
74+
client = create_client(actor="verb-check")
75+
try:
76+
for verb in EXPECTED_VERBS:
77+
method = getattr(client, verb)
78+
assert callable(method), verb
79+
finally:
80+
client.close()
81+
82+
83+
84+
def test_mocked_client_response_matches_public_envelope_shape() -> None:
85+
def handler(_: httpx.Request) -> httpx.Response:
86+
return httpx.Response(
87+
200,
88+
json={
89+
"receipt": {
90+
"status": "success",
91+
"x402": {"verb": "summarize", "version": "1.1.0"},
92+
"result": {"summary": "done"},
93+
"metadata": {
94+
"proof": {
95+
"alg": "ed25519-sha256",
96+
"canonical": "cl-stable-json-v1",
97+
}
98+
},
99+
},
100+
"runtime_metadata": {"duration_ms": 7, "provider": "mock-runtime"},
101+
},
102+
)
103+
104+
client = create_client(
105+
actor="shape-check",
106+
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
107+
)
108+
109+
try:
110+
response = client.summarize(content="Hello", style="bullet_points")
111+
finally:
112+
client.close()
113+
114+
assert set(response.keys()) == {"receipt", "runtime_metadata"}
115+
assert response["receipt"]["x402"]["verb"] == "summarize"
116+
assert response["receipt"]["result"]["summary"] == "done"
117+
assert response["runtime_metadata"]["duration_ms"] == 7
118+
119+
120+
121+
def test_verify_receipt_is_importable_callable_and_matches_vector_contract() -> None:
122+
receipt = load_fixture("receipt_valid.json")
123+
124+
result = verify_receipt(receipt, public_key=load_pubkey())
125+
126+
assert callable(verify_receipt)
127+
assert result["ok"] is True
128+
assert result["values"]["recomputed_hash"] == recompute_receipt_hash_sha256(receipt)["hash_sha256"]
129+
assert result["values"]["signer_id"] == "runtime.commandlayer.eth"
130+
assert result["errors"]["verify_error"] is None
131+
132+
133+
134+
def test_mocked_end_to_end_flow_uses_vector_shaped_response() -> None:
135+
receipt = load_fixture("receipt_valid.json")
136+
137+
def handler(_: httpx.Request) -> httpx.Response:
138+
return httpx.Response(
139+
200,
140+
json={
141+
"receipt": receipt,
142+
"runtime_metadata": {"duration_ms": 11, "provider": "mock-runtime"},
143+
},
144+
)
145+
146+
client = create_client(
147+
actor="vector-flow",
148+
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
149+
)
150+
151+
try:
152+
response = client.analyze(content="vector-backed", goal="parity")
153+
finally:
154+
client.close()
155+
156+
assert response["receipt"] == receipt
157+
assert response["runtime_metadata"]["provider"] == "mock-runtime"
158+
verification = verify_receipt(response["receipt"], public_key=load_pubkey())
159+
assert verification["ok"] is True

0 commit comments

Comments
 (0)