commandlayer 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: commandlayer
3
+ Version: 1.1.0
4
+ Summary: CommandLayer Python SDK — semantic verbs, signed receipts, and verification helpers.
5
+ Author-email: CommandLayer <security@commandlayer.org>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/commandlayer/sdk
8
+ Project-URL: Repository, https://github.com/commandlayer/sdk
9
+ Project-URL: Documentation, https://github.com/commandlayer/sdk#readme
10
+ Project-URL: Changelog, https://github.com/commandlayer/sdk/blob/main/CHANGELOG.md
11
+ Project-URL: Issues, https://github.com/commandlayer/sdk/issues
12
+ Keywords: commandlayer,agents,receipts,protocol-commons,ens,sdk
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Topic :: Security :: Cryptography
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: httpx>=0.27.0
26
+ Requires-Dist: pynacl>=1.5.0
27
+ Requires-Dist: web3>=6.20.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
30
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
31
+ Requires-Dist: mypy>=1.10.0; extra == "dev"
32
+ Requires-Dist: build>=1.1.0; extra == "dev"
33
+ Requires-Dist: twine>=5.0.0; extra == "dev"
34
+
35
+ # CommandLayer Python SDK
36
+
37
+ Official Python SDK for CommandLayer Commons v1.1.0.
38
+
39
+ The Python package mirrors the TypeScript SDK's protocol model:
40
+ - client methods return `{ "receipt": ..., "runtime_metadata": ... }`,
41
+ - the signed `receipt` is the canonical verification payload,
42
+ - `runtime_metadata` is optional execution context, and
43
+ - verification can use an explicit Ed25519 key or ENS discovery.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install commandlayer
49
+ ```
50
+
51
+ Supported Python versions: 3.10+.
52
+
53
+ ## Quick start
54
+
55
+ ```python
56
+ from commandlayer import create_client, verify_receipt
57
+
58
+ client = create_client(actor="docs-example")
59
+ response = client.summarize(
60
+ content="CommandLayer makes agent execution verifiable.",
61
+ style="bullet_points",
62
+ )
63
+
64
+ print(response["receipt"]["result"]["summary"])
65
+ print(response["receipt"]["metadata"]["receipt_id"])
66
+ print(response.get("runtime_metadata", {}).get("duration_ms"))
67
+
68
+ verification = verify_receipt(
69
+ response["receipt"],
70
+ public_key="ed25519:BASE64_PUBLIC_KEY",
71
+ )
72
+ print(verification["ok"])
73
+ ```
74
+
75
+ ## Verification
76
+
77
+ ```python
78
+ result = verify_receipt(
79
+ response["receipt"],
80
+ ens={
81
+ "name": "summarizeagent.eth",
82
+ "rpcUrl": "https://mainnet.infura.io/v3/YOUR_KEY",
83
+ },
84
+ )
85
+ ```
86
+
87
+ ## Development
88
+
89
+ ```bash
90
+ cd python-sdk
91
+ python -m venv .venv
92
+ source .venv/bin/activate
93
+ pip install -e '.[dev]'
94
+ ruff check .
95
+ mypy commandlayer
96
+ pytest
97
+ ```
@@ -0,0 +1,63 @@
1
+ # CommandLayer Python SDK
2
+
3
+ Official Python SDK for CommandLayer Commons v1.1.0.
4
+
5
+ The Python package mirrors the TypeScript SDK's protocol model:
6
+ - client methods return `{ "receipt": ..., "runtime_metadata": ... }`,
7
+ - the signed `receipt` is the canonical verification payload,
8
+ - `runtime_metadata` is optional execution context, and
9
+ - verification can use an explicit Ed25519 key or ENS discovery.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install commandlayer
15
+ ```
16
+
17
+ Supported Python versions: 3.10+.
18
+
19
+ ## Quick start
20
+
21
+ ```python
22
+ from commandlayer import create_client, verify_receipt
23
+
24
+ client = create_client(actor="docs-example")
25
+ response = client.summarize(
26
+ content="CommandLayer makes agent execution verifiable.",
27
+ style="bullet_points",
28
+ )
29
+
30
+ print(response["receipt"]["result"]["summary"])
31
+ print(response["receipt"]["metadata"]["receipt_id"])
32
+ print(response.get("runtime_metadata", {}).get("duration_ms"))
33
+
34
+ verification = verify_receipt(
35
+ response["receipt"],
36
+ public_key="ed25519:BASE64_PUBLIC_KEY",
37
+ )
38
+ print(verification["ok"])
39
+ ```
40
+
41
+ ## Verification
42
+
43
+ ```python
44
+ result = verify_receipt(
45
+ response["receipt"],
46
+ ens={
47
+ "name": "summarizeagent.eth",
48
+ "rpcUrl": "https://mainnet.infura.io/v3/YOUR_KEY",
49
+ },
50
+ )
51
+ ```
52
+
53
+ ## Development
54
+
55
+ ```bash
56
+ cd python-sdk
57
+ python -m venv .venv
58
+ source .venv/bin/activate
59
+ pip install -e '.[dev]'
60
+ ruff check .
61
+ mypy commandlayer
62
+ pytest
63
+ ```
@@ -0,0 +1,43 @@
1
+ """CommandLayer Python SDK."""
2
+
3
+ from .client import CommandLayerClient, create_client, normalize_command_response
4
+ from .errors import CommandLayerError
5
+ from .types import (
6
+ CanonicalReceipt,
7
+ CommandResponse,
8
+ EnsVerifyOptions,
9
+ RuntimeMetadata,
10
+ SignerKeyResolution,
11
+ VerifyOptions,
12
+ VerifyResult,
13
+ )
14
+ from .verify import (
15
+ canonicalize_stable_json_v1,
16
+ parse_ed25519_pubkey,
17
+ recompute_receipt_hash_sha256,
18
+ resolve_signer_key,
19
+ sha256_hex_utf8,
20
+ verify_receipt,
21
+ )
22
+
23
+ __all__ = [
24
+ "CanonicalReceipt",
25
+ "CommandLayerClient",
26
+ "CommandLayerError",
27
+ "CommandResponse",
28
+ "EnsVerifyOptions",
29
+ "RuntimeMetadata",
30
+ "VerifyOptions",
31
+ "SignerKeyResolution",
32
+ "VerifyResult",
33
+ "canonicalize_stable_json_v1",
34
+ "create_client",
35
+ "normalize_command_response",
36
+ "sha256_hex_utf8",
37
+ "parse_ed25519_pubkey",
38
+ "recompute_receipt_hash_sha256",
39
+ "resolve_signer_key",
40
+ "verify_receipt",
41
+ ]
42
+
43
+ __version__ = "1.1.0"
@@ -0,0 +1,349 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from collections.abc import Mapping
6
+ from typing import Any, cast
7
+
8
+ import httpx
9
+
10
+ from .errors import CommandLayerError
11
+ from .types import CommandResponse, RuntimeMetadata, VerifyOptions
12
+ from .verify import verify_receipt
13
+
14
+ COMMONS_VERSION = "1.1.0"
15
+ PACKAGE_VERSION = "1.1.0"
16
+ DEFAULT_RUNTIME = "https://runtime.commandlayer.org"
17
+ VERBS = {
18
+ "summarize",
19
+ "analyze",
20
+ "classify",
21
+ "clean",
22
+ "convert",
23
+ "describe",
24
+ "explain",
25
+ "format",
26
+ "parse",
27
+ "fetch",
28
+ }
29
+
30
+
31
+ def _normalize_base(url: str) -> str:
32
+ return str(url or "").rstrip("/")
33
+
34
+
35
+ def normalize_command_response(payload: Any) -> CommandResponse:
36
+ if not isinstance(payload, dict):
37
+ raise CommandLayerError("Runtime response must be a JSON object", 502, payload)
38
+
39
+ if isinstance(payload.get("receipt"), dict):
40
+ response: CommandResponse = {"receipt": payload["receipt"]}
41
+ if isinstance(payload.get("runtime_metadata"), dict):
42
+ response["runtime_metadata"] = cast(RuntimeMetadata, dict(payload["runtime_metadata"]))
43
+ return response
44
+
45
+ receipt = dict(payload)
46
+ runtime_metadata = receipt.pop("trace", None)
47
+ response = {"receipt": receipt}
48
+ if isinstance(runtime_metadata, dict):
49
+ response["runtime_metadata"] = cast(RuntimeMetadata, runtime_metadata)
50
+ return response
51
+
52
+
53
+ class CommandLayerClient:
54
+ """Synchronous CommandLayer client for Protocol-Commons v1.1.0 verbs."""
55
+
56
+ def __init__(
57
+ self,
58
+ runtime: str = DEFAULT_RUNTIME,
59
+ actor: str = "sdk-user",
60
+ timeout_ms: int = 30_000,
61
+ headers: Mapping[str, str] | None = None,
62
+ retries: int = 0,
63
+ verify_receipts: bool = False,
64
+ verify: VerifyOptions | None = None,
65
+ http_client: httpx.Client | None = None,
66
+ ):
67
+ self.runtime = _normalize_base(runtime)
68
+ self.actor = actor
69
+ self.timeout_ms = timeout_ms
70
+ self.retries = max(0, retries)
71
+ self.verify_receipts = verify_receipts is True
72
+ self.verify_defaults: VerifyOptions = verify or {}
73
+ self.default_headers = {
74
+ "Content-Type": "application/json",
75
+ "User-Agent": f"commandlayer-py/{PACKAGE_VERSION}",
76
+ }
77
+ if headers:
78
+ self.default_headers.update(dict(headers))
79
+ self._http = http_client or httpx.Client(timeout=self.timeout_ms / 1000)
80
+
81
+ def _ensure_verify_config_if_enabled(self) -> None:
82
+ if not self.verify_receipts:
83
+ return
84
+ explicit_public_key = self.verify_defaults.get("public_key") or self.verify_defaults.get(
85
+ "publicKey"
86
+ )
87
+ has_explicit = bool(str(explicit_public_key or "").strip())
88
+ ens = self.verify_defaults.get("ens") or {}
89
+ has_ens = bool(ens.get("name") and (ens.get("rpcUrl") or ens.get("rpc_url")))
90
+ if not has_explicit and not has_ens:
91
+ raise CommandLayerError(
92
+ "verify_receipts is enabled but no verification key config provided. "
93
+ "Set verify.public_key (or verify.publicKey) or verify.ens {name, rpcUrl}.",
94
+ 400,
95
+ )
96
+
97
+ def summarize(
98
+ self,
99
+ *,
100
+ content: str,
101
+ style: str | None = None,
102
+ format: str | None = None,
103
+ max_tokens: int = 1000,
104
+ ) -> CommandResponse:
105
+ return self.call(
106
+ "summarize",
107
+ {
108
+ "input": {
109
+ "content": content,
110
+ "summary_style": style,
111
+ "format_hint": format,
112
+ },
113
+ "limits": {"max_output_tokens": max_tokens},
114
+ },
115
+ )
116
+
117
+ def analyze(
118
+ self,
119
+ *,
120
+ content: str,
121
+ goal: str | None = None,
122
+ hints: list[str] | None = None,
123
+ max_tokens: int = 1000,
124
+ ) -> CommandResponse:
125
+ payload: dict[str, Any] = {
126
+ "input": content,
127
+ "limits": {"max_output_tokens": max_tokens},
128
+ }
129
+ if goal:
130
+ payload["goal"] = goal
131
+ if hints:
132
+ payload["hints"] = hints
133
+ return self.call("analyze", payload)
134
+
135
+ def classify(
136
+ self, *, content: str, max_labels: int = 5, max_tokens: int = 1000
137
+ ) -> CommandResponse:
138
+ return self.call(
139
+ "classify",
140
+ {
141
+ "actor": self.actor,
142
+ "input": {"content": content},
143
+ "limits": {"max_labels": max_labels, "max_output_tokens": max_tokens},
144
+ },
145
+ )
146
+
147
+ def clean(
148
+ self,
149
+ *,
150
+ content: str,
151
+ operations: list[str] | None = None,
152
+ max_tokens: int = 1000,
153
+ ) -> CommandResponse:
154
+ return self.call(
155
+ "clean",
156
+ {
157
+ "input": {
158
+ "content": content,
159
+ "operations": operations
160
+ or ["normalize_newlines", "collapse_whitespace", "trim"],
161
+ },
162
+ "limits": {"max_output_tokens": max_tokens},
163
+ },
164
+ )
165
+
166
+ def convert(
167
+ self,
168
+ *,
169
+ content: str,
170
+ from_format: str,
171
+ to_format: str,
172
+ max_tokens: int = 1000,
173
+ ) -> CommandResponse:
174
+ return self.call(
175
+ "convert",
176
+ {
177
+ "input": {
178
+ "content": content,
179
+ "source_format": from_format,
180
+ "target_format": to_format,
181
+ },
182
+ "limits": {"max_output_tokens": max_tokens},
183
+ },
184
+ )
185
+
186
+ def describe(
187
+ self,
188
+ *,
189
+ subject: str,
190
+ audience: str = "general",
191
+ detail: str = "medium",
192
+ max_tokens: int = 1000,
193
+ ) -> CommandResponse:
194
+ return self.call(
195
+ "describe",
196
+ {
197
+ "input": {
198
+ "subject": (subject or "")[:140],
199
+ "audience": audience,
200
+ "detail_level": detail,
201
+ },
202
+ "limits": {"max_output_tokens": max_tokens},
203
+ },
204
+ )
205
+
206
+ def explain(
207
+ self,
208
+ *,
209
+ subject: str,
210
+ audience: str = "general",
211
+ style: str = "step-by-step",
212
+ detail: str = "medium",
213
+ max_tokens: int = 1000,
214
+ ) -> CommandResponse:
215
+ return self.call(
216
+ "explain",
217
+ {
218
+ "input": {
219
+ "subject": (subject or "")[:140],
220
+ "audience": audience,
221
+ "style": style,
222
+ "detail_level": detail,
223
+ },
224
+ "limits": {"max_output_tokens": max_tokens},
225
+ },
226
+ )
227
+
228
+ def format(self, *, content: str, to: str, max_tokens: int = 1000) -> CommandResponse:
229
+ return self.call(
230
+ "format",
231
+ {
232
+ "input": {"content": content, "target_style": to},
233
+ "limits": {"max_output_tokens": max_tokens},
234
+ },
235
+ )
236
+
237
+ def parse(
238
+ self,
239
+ *,
240
+ content: str,
241
+ content_type: str = "text",
242
+ mode: str = "best_effort",
243
+ target_schema: str | None = None,
244
+ max_tokens: int = 1000,
245
+ ) -> CommandResponse:
246
+ payload: dict[str, Any] = {
247
+ "input": {
248
+ "content": content,
249
+ "content_type": content_type,
250
+ "mode": mode,
251
+ },
252
+ "limits": {"max_output_tokens": max_tokens},
253
+ }
254
+ if target_schema:
255
+ payload["input"]["target_schema"] = target_schema
256
+ return self.call("parse", payload)
257
+
258
+ def fetch(
259
+ self,
260
+ *,
261
+ source: str,
262
+ query: str | None = None,
263
+ include_metadata: bool | None = None,
264
+ max_tokens: int = 1000,
265
+ ) -> CommandResponse:
266
+ input_obj: dict[str, Any] = {"source": source}
267
+ if query is not None:
268
+ input_obj["query"] = query
269
+ if include_metadata is not None:
270
+ input_obj["include_metadata"] = include_metadata
271
+ return self.call(
272
+ "fetch",
273
+ {"input": input_obj, "limits": {"max_output_tokens": max_tokens}},
274
+ )
275
+
276
+ def _build_payload(self, verb: str, body: dict[str, Any]) -> dict[str, Any]:
277
+ return {
278
+ "x402": {
279
+ "verb": verb,
280
+ "version": COMMONS_VERSION,
281
+ "entry": f"x402://{verb}agent.eth/{verb}/v{COMMONS_VERSION}",
282
+ },
283
+ "actor": body.get("actor", self.actor),
284
+ **body,
285
+ }
286
+
287
+ def _request(self, verb: str, payload: dict[str, Any]) -> httpx.Response:
288
+ url = f"{self.runtime}/{verb}/v{COMMONS_VERSION}"
289
+ attempt = 0
290
+ while True:
291
+ try:
292
+ return self._http.post(url, headers=self.default_headers, json=payload)
293
+ except httpx.TimeoutException as err:
294
+ if attempt >= self.retries:
295
+ raise CommandLayerError("Request timed out", 408) from err
296
+ except httpx.HTTPError as err:
297
+ if attempt >= self.retries:
298
+ raise CommandLayerError(f"HTTP transport error: {err}") from err
299
+ attempt += 1
300
+ time.sleep(min(0.2 * attempt, 1.0))
301
+
302
+ def call(self, verb: str, body: dict[str, Any]) -> CommandResponse:
303
+ if verb not in VERBS:
304
+ raise CommandLayerError(f"Unsupported verb: {verb}", 400)
305
+ self._ensure_verify_config_if_enabled()
306
+ payload = self._build_payload(verb, body)
307
+ response = self._request(verb, payload)
308
+
309
+ try:
310
+ data: Any = response.json()
311
+ except json.JSONDecodeError:
312
+ data = {}
313
+
314
+ if not response.is_success:
315
+ message = (
316
+ (data.get("message") if isinstance(data, dict) else None)
317
+ or (
318
+ (data.get("error") or {}).get("message")
319
+ if isinstance(data, dict) and isinstance(data.get("error"), dict)
320
+ else None
321
+ )
322
+ or f"HTTP {response.status_code}"
323
+ )
324
+ raise CommandLayerError(str(message), response.status_code, data)
325
+
326
+ normalized = normalize_command_response(data)
327
+ if self.verify_receipts:
328
+ verify_result = verify_receipt(
329
+ normalized["receipt"],
330
+ public_key=self.verify_defaults.get("public_key")
331
+ or self.verify_defaults.get("publicKey"),
332
+ ens=self.verify_defaults.get("ens"),
333
+ )
334
+ if not verify_result["ok"]:
335
+ raise CommandLayerError("Receipt verification failed", 422, verify_result)
336
+ return normalized
337
+
338
+ def close(self) -> None:
339
+ self._http.close()
340
+
341
+ def __enter__(self) -> CommandLayerClient:
342
+ return self
343
+
344
+ def __exit__(self, *args: object) -> None:
345
+ self.close()
346
+
347
+
348
+ def create_client(**kwargs: Any) -> CommandLayerClient:
349
+ return CommandLayerClient(**kwargs)
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class CommandLayerError(Exception):
7
+ """Top-level SDK error with optional HTTP metadata."""
8
+
9
+ def __init__(self, message: str, status_code: int | None = None, details: Any = None):
10
+ super().__init__(message)
11
+ self.status_code = status_code
12
+ self.details = details
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Literal, TypedDict
5
+
6
+ CanonicalReceipt = dict[str, Any]
7
+
8
+
9
+ class RuntimeMetadata(TypedDict, total=False):
10
+ trace_id: str
11
+ parent_trace_id: str | None
12
+ started_at: str
13
+ completed_at: str
14
+ duration_ms: int
15
+ provider: str
16
+ runtime: str
17
+ request_id: str
18
+
19
+
20
+ class CommandResponse(TypedDict, total=False):
21
+ receipt: CanonicalReceipt
22
+ runtime_metadata: RuntimeMetadata
23
+
24
+
25
+ class EnsVerifyOptions(TypedDict, total=False):
26
+ name: str
27
+ rpc_url: str
28
+ rpcUrl: str
29
+
30
+
31
+ class VerifyOptions(TypedDict, total=False):
32
+ public_key: str
33
+ publicKey: str
34
+ ens: EnsVerifyOptions
35
+
36
+
37
+ class VerifyChecks(TypedDict):
38
+ hash_matches: bool
39
+ signature_valid: bool
40
+ receipt_id_matches: bool
41
+ alg_matches: bool
42
+ canonical_matches: bool
43
+
44
+
45
+ class VerifyValues(TypedDict):
46
+ verb: str | None
47
+ signer_id: str | None
48
+ alg: str | None
49
+ canonical: str | None
50
+ claimed_hash: str | None
51
+ recomputed_hash: str | None
52
+ receipt_id: str | None
53
+ pubkey_source: Literal["explicit", "ens"] | None
54
+ ens_txt_key: str | None
55
+
56
+
57
+ class VerifyErrors(TypedDict):
58
+ signature_error: str | None
59
+ ens_error: str | None
60
+ verify_error: str | None
61
+
62
+
63
+ class VerifyResult(TypedDict):
64
+ ok: bool
65
+ checks: VerifyChecks
66
+ values: VerifyValues
67
+ errors: VerifyErrors
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class SignerKeyResolution:
72
+ algorithm: Literal["ed25519"]
73
+ kid: str
74
+ raw_public_key_bytes: bytes