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.
- commandlayer-1.1.0/PKG-INFO +97 -0
- commandlayer-1.1.0/README.md +63 -0
- commandlayer-1.1.0/commandlayer/__init__.py +43 -0
- commandlayer-1.1.0/commandlayer/client.py +349 -0
- commandlayer-1.1.0/commandlayer/errors.py +12 -0
- commandlayer-1.1.0/commandlayer/types.py +74 -0
- commandlayer-1.1.0/commandlayer/verify.py +335 -0
- commandlayer-1.1.0/commandlayer.egg-info/PKG-INFO +97 -0
- commandlayer-1.1.0/commandlayer.egg-info/SOURCES.txt +16 -0
- commandlayer-1.1.0/commandlayer.egg-info/dependency_links.txt +1 -0
- commandlayer-1.1.0/commandlayer.egg-info/requires.txt +10 -0
- commandlayer-1.1.0/commandlayer.egg-info/top_level.txt +1 -0
- commandlayer-1.1.0/pyproject.toml +72 -0
- commandlayer-1.1.0/setup.cfg +4 -0
- commandlayer-1.1.0/tests/test_client.py +97 -0
- commandlayer-1.1.0/tests/test_public_api.py +162 -0
- commandlayer-1.1.0/tests/test_verification.py +76 -0
- commandlayer-1.1.0/tests/test_verify.py +119 -0
|
@@ -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
|