inamprotocol 0.2.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,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: inamprotocol
3
+ Version: 0.2.0
4
+ Summary: Reference Python SDK for the INAM Protocol — agent identity, execution receipts, jobs, and reputation
5
+ Author: INAM Protocol
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://inamprotocol.org
8
+ Project-URL: Documentation, https://docs.inamprotocol.org
9
+ Project-URL: Repository, https://github.com/inamprotocol/inam-protocol
10
+ Project-URL: Specification, https://docs.inamprotocol.org/spec/
11
+ Project-URL: Changelog, https://github.com/inamprotocol/inam-protocol/blob/main/CHANGELOG.md
12
+ Keywords: inam,agent,ai-agent,identity,reputation,did,protocol
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: cryptography>=42.0.0
23
+ Requires-Dist: base58>=2.1.0
24
+
25
+ # inamprotocol (Python SDK)
26
+
27
+ Reference Python client for the INAM Protocol — parity with the TypeScript `InamClient` in `../src/sdk/client.ts`. See [`../SPEC.md`](../SPEC.md) for the full protocol specification.
28
+
29
+ ## Install (development)
30
+
31
+ ```
32
+ python -m venv .venv
33
+ ./.venv/Scripts/python.exe -m pip install -e . pytest # Windows
34
+ # ./.venv/bin/python -m pip install -e . pytest # macOS/Linux
35
+ ```
36
+
37
+ ## Test
38
+
39
+ ```
40
+ ./.venv/Scripts/python.exe -m pytest -v
41
+ ```
42
+
43
+ `tests/test_interop.py` is the important one: it checks this SDK's `did:key` encoding, canonical JSON, and Ed25519 signing against fixed values generated once by the TypeScript reference implementation (`../scripts/interop-vectors.ts`). If a change here ever breaks that test, a receipt signed by this SDK would stop verifying against a registry or another SDK written in a different language — that's the whole point of the test.
44
+
45
+ ## Usage
46
+
47
+ ```python
48
+ from inamprotocol import InamClient, generate_keypair
49
+
50
+ keypair = generate_keypair()
51
+ client = InamClient("http://localhost:4021", keypair)
52
+
53
+ profile = client.register_agent(["document-extraction"], {"name": "My Agent"})
54
+ print(profile["id"]) # did:key:z...
55
+
56
+ reputation = client.get_reputation(profile["id"])
57
+ ```
58
+
59
+ See `examples/interop_worker.py` for a full worker-side flow (register, link an external identity, submit signed Execution Receipt drafts), and `../scripts/run-interop-demo.sh` for the end-to-end cross-language demo (a TypeScript requester and this Python worker doing real business through the same live registry).
@@ -0,0 +1,35 @@
1
+ # inamprotocol (Python SDK)
2
+
3
+ Reference Python client for the INAM Protocol — parity with the TypeScript `InamClient` in `../src/sdk/client.ts`. See [`../SPEC.md`](../SPEC.md) for the full protocol specification.
4
+
5
+ ## Install (development)
6
+
7
+ ```
8
+ python -m venv .venv
9
+ ./.venv/Scripts/python.exe -m pip install -e . pytest # Windows
10
+ # ./.venv/bin/python -m pip install -e . pytest # macOS/Linux
11
+ ```
12
+
13
+ ## Test
14
+
15
+ ```
16
+ ./.venv/Scripts/python.exe -m pytest -v
17
+ ```
18
+
19
+ `tests/test_interop.py` is the important one: it checks this SDK's `did:key` encoding, canonical JSON, and Ed25519 signing against fixed values generated once by the TypeScript reference implementation (`../scripts/interop-vectors.ts`). If a change here ever breaks that test, a receipt signed by this SDK would stop verifying against a registry or another SDK written in a different language — that's the whole point of the test.
20
+
21
+ ## Usage
22
+
23
+ ```python
24
+ from inamprotocol import InamClient, generate_keypair
25
+
26
+ keypair = generate_keypair()
27
+ client = InamClient("http://localhost:4021", keypair)
28
+
29
+ profile = client.register_agent(["document-extraction"], {"name": "My Agent"})
30
+ print(profile["id"]) # did:key:z...
31
+
32
+ reputation = client.get_reputation(profile["id"])
33
+ ```
34
+
35
+ See `examples/interop_worker.py` for a full worker-side flow (register, link an external identity, submit signed Execution Receipt drafts), and `../scripts/run-interop-demo.sh` for the end-to-end cross-language demo (a TypeScript requester and this Python worker doing real business through the same live registry).
@@ -0,0 +1,14 @@
1
+ from .keys import Keypair, generate_keypair, public_key_to_did, did_to_public_key, sign, verify
2
+ from .canonical import canonicalize
3
+ from .client import InamClient
4
+
5
+ __all__ = [
6
+ "Keypair",
7
+ "generate_keypair",
8
+ "public_key_to_did",
9
+ "did_to_public_key",
10
+ "sign",
11
+ "verify",
12
+ "canonicalize",
13
+ "InamClient",
14
+ ]
@@ -0,0 +1,32 @@
1
+ """Canonical JSON serialization — must match src/crypto/canonical.ts byte-for-byte.
2
+
3
+ This is the one piece of logic every INAM SDK, in any language, has to agree
4
+ on precisely: receipt signatures are verified by independently re-canonicalizing
5
+ the same structured data on the other side, so any divergence here would make
6
+ cross-language signatures fail to verify. See SPEC.md section 7.
7
+ """
8
+
9
+ import json
10
+ from typing import Any
11
+
12
+
13
+ def canonicalize(value: Any) -> str:
14
+ return _stringify(value)
15
+
16
+
17
+ def _stringify(value: Any) -> str:
18
+ if isinstance(value, dict):
19
+ keys = sorted(value.keys())
20
+ entries = []
21
+ for key in keys:
22
+ v = value[key]
23
+ if v is None:
24
+ # Mirrors the JS side filtering out `undefined`-valued keys —
25
+ # None is our cross-language stand-in for "field omitted",
26
+ # never a meaningful JSON null in these schemas.
27
+ continue
28
+ entries.append(json.dumps(key, ensure_ascii=False) + ":" + _stringify(v))
29
+ return "{" + ",".join(entries) + "}"
30
+ if isinstance(value, list):
31
+ return "[" + ",".join(_stringify(v) for v in value) + "]"
32
+ return json.dumps(value, ensure_ascii=False)
@@ -0,0 +1,203 @@
1
+ """INAM Protocol reference client — parity with src/sdk/client.ts (InamClient).
2
+
3
+ Everything here is transport plumbing plus the two signature schemes (HTTP
4
+ request signing, receipt content signing). An agent framework's tool-calling
5
+ layer would wrap these same calls as `search_jobs` / `verify_agent` /
6
+ `submit_work`-style tools.
7
+ """
8
+
9
+ import json
10
+ import time
11
+ import urllib.error
12
+ import urllib.parse
13
+ import urllib.request
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from .canonical import canonicalize
17
+ from .keys import Keypair, sha256_hex, sign, to_base64
18
+ from .receipt import build_signable_content
19
+
20
+
21
+ class InamApiError(Exception):
22
+ def __init__(self, method: str, path: str, status: int, payload: Any):
23
+ self.status = status
24
+ self.payload = payload
25
+ super().__init__(f"{method} {path} -> {status}: {payload}")
26
+
27
+
28
+ class InamClient:
29
+ def __init__(self, base_url: str, keypair: Keypair):
30
+ self.base_url = base_url.rstrip("/")
31
+ self.keypair = keypair
32
+
33
+ @property
34
+ def did(self) -> str:
35
+ return self.keypair.did
36
+
37
+ def _request(
38
+ self,
39
+ method: str,
40
+ path: str,
41
+ body: Optional[Dict[str, Any]] = None,
42
+ idempotency_key: Optional[str] = None,
43
+ ) -> Any:
44
+ raw_body = json.dumps(body, separators=(",", ":")) if body is not None else ""
45
+ timestamp = str(int(time.time() * 1000))
46
+ body_hash = sha256_hex(raw_body)
47
+ signing_string = f"{method.upper()}\n{path}\n{timestamp}\n{body_hash}"
48
+ signature = to_base64(sign(signing_string.encode("utf-8"), self.keypair.private_key))
49
+
50
+ headers = {
51
+ "content-type": "application/json",
52
+ # Cloudflare's bot protection on *.workers.dev flags Python's
53
+ # default `Python-urllib/x.y` User-Agent; identify honestly instead.
54
+ "user-agent": "inamprotocol-python-sdk/0.1.0",
55
+ "inam-agent": self.keypair.did,
56
+ "inam-timestamp": timestamp,
57
+ "inam-signature": signature,
58
+ }
59
+ if idempotency_key:
60
+ headers["idempotency-key"] = idempotency_key
61
+
62
+ url = f"{self.base_url}{path}"
63
+ data = raw_body.encode("utf-8") if body is not None else None
64
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
65
+ try:
66
+ with urllib.request.urlopen(req) as res:
67
+ return json.loads(res.read().decode("utf-8"))
68
+ except urllib.error.HTTPError as e:
69
+ payload_text = e.read().decode("utf-8")
70
+ try:
71
+ payload = json.loads(payload_text)
72
+ except json.JSONDecodeError:
73
+ payload = payload_text
74
+ raise InamApiError(method, path, e.code, payload) from None
75
+
76
+ def register_agent(self, capabilities: List[str], metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
77
+ # Omit the key entirely rather than sending it as JSON null: the
78
+ # server's zod schema treats `metadata` as optional-if-absent, not
79
+ # nullable, and Python's json.dumps (unlike JS's JSON.stringify,
80
+ # which drops `undefined`-valued keys) serializes None as null.
81
+ body: Dict[str, Any] = {"capabilities": capabilities}
82
+ if metadata is not None:
83
+ body["metadata"] = metadata
84
+ return self._request("POST", "/v1/agents", body, idempotency_key=f"register:{self.keypair.did}")
85
+
86
+ def get_agent(self, agent_id: str) -> Dict[str, Any]:
87
+ return self._request("GET", f"/v1/agents/{urllib.parse.quote(agent_id, safe='')}")
88
+
89
+ def link_identity(self, protocol: str, value: str) -> Dict[str, Any]:
90
+ return self._request(
91
+ "POST",
92
+ f"/v1/agents/{urllib.parse.quote(self.did, safe='')}/link",
93
+ {"protocol": protocol, "value": value},
94
+ idempotency_key=f"link:{protocol}:{value}",
95
+ )
96
+
97
+ def search_agents(
98
+ self,
99
+ capability: Optional[str] = None,
100
+ min_reputation: Optional[float] = None,
101
+ supports: Optional[str] = None,
102
+ ) -> Dict[str, Any]:
103
+ params: Dict[str, str] = {}
104
+ if capability:
105
+ params["capability"] = capability
106
+ if min_reputation is not None:
107
+ params["min_reputation"] = str(min_reputation)
108
+ if supports:
109
+ params["supports"] = supports
110
+ return self._request("GET", f"/v1/agents/search?{urllib.parse.urlencode(params)}")
111
+
112
+ def get_reputation(self, agent_id: str) -> Dict[str, Any]:
113
+ return self._request("GET", f"/v1/agents/{urllib.parse.quote(agent_id, safe='')}/reputation")
114
+
115
+ def list_receipts(self, agent_id: str) -> Dict[str, Any]:
116
+ return self._request("GET", f"/v1/agents/{urllib.parse.quote(agent_id, safe='')}/receipts")
117
+
118
+ def submit_work(self, agent_a_id: str, input: Dict[str, Any]) -> Dict[str, Any]:
119
+ """Called by the worker (agent_b) once a job is complete, off-network."""
120
+ content = build_signable_content(agent_a_id, self.did, input)
121
+ signing_bytes = canonicalize({**content, "dispute": None}).encode("utf-8")
122
+ signature = to_base64(sign(signing_bytes, self.keypair.private_key))
123
+ body = {**input, "agentAId": agent_a_id, "signature": signature}
124
+ return self._request("POST", "/v1/receipts", body, idempotency_key=f"receipt:{input['jobId']}")
125
+
126
+ def accept_work(self, receipt: Dict[str, Any]) -> Dict[str, Any]:
127
+ """Called by the requester (agent_a) to accept the worker's submitted result."""
128
+ content = {**receipt, "signatures": None, "status": None, "dispute": None}
129
+ signing_bytes = canonicalize(content).encode("utf-8")
130
+ signature = to_base64(sign(signing_bytes, self.keypair.private_key))
131
+ receipt_id = urllib.parse.quote(receipt["receiptId"], safe="")
132
+ return self._request(
133
+ "POST",
134
+ f"/v1/receipts/{receipt_id}/countersign",
135
+ {"signature": signature},
136
+ idempotency_key=f"countersign:{receipt['receiptId']}",
137
+ )
138
+
139
+ def dispute_receipt(self, receipt_id: str, reason: str) -> Dict[str, Any]:
140
+ encoded = urllib.parse.quote(receipt_id, safe="")
141
+ return self._request(
142
+ "POST",
143
+ f"/v1/receipts/{encoded}/dispute",
144
+ {"reason": reason},
145
+ idempotency_key=f"dispute:{receipt_id}",
146
+ )
147
+
148
+ # ---- Jobs (SPEC.md section 3) -- optional pre-work discovery/offer/accept ----
149
+
150
+ def post_job(
151
+ self,
152
+ capability: str,
153
+ spec_hash: str,
154
+ budget: Optional[Dict[str, Any]] = None,
155
+ expires_at: Optional[str] = None,
156
+ ) -> Dict[str, Any]:
157
+ body: Dict[str, Any] = {"capability": capability, "specHash": spec_hash}
158
+ if budget is not None:
159
+ body["budget"] = budget
160
+ if expires_at is not None:
161
+ body["expiresAt"] = expires_at
162
+ return self._request("POST", "/v1/jobs", body, idempotency_key=f"job:{capability}:{spec_hash}:{time.time()}")
163
+
164
+ def get_job(self, job_id: str) -> Dict[str, Any]:
165
+ return self._request("GET", f"/v1/jobs/{urllib.parse.quote(job_id, safe='')}")
166
+
167
+ def search_jobs(self, capability: Optional[str] = None, status: Optional[str] = None) -> Dict[str, Any]:
168
+ params: Dict[str, str] = {}
169
+ if capability:
170
+ params["capability"] = capability
171
+ if status:
172
+ params["status"] = status
173
+ return self._request("GET", f"/v1/jobs/search?{urllib.parse.urlencode(params)}")
174
+
175
+ def submit_offer(self, job_id: str, message: Optional[str] = None) -> Dict[str, Any]:
176
+ encoded = urllib.parse.quote(job_id, safe="")
177
+ body: Dict[str, Any] = {}
178
+ if message is not None:
179
+ body["message"] = message
180
+ return self._request(
181
+ "POST",
182
+ f"/v1/jobs/{encoded}/offers",
183
+ body,
184
+ idempotency_key=f"offer:{job_id}:{self.did}",
185
+ )
186
+
187
+ def list_offers(self, job_id: str) -> Dict[str, Any]:
188
+ return self._request("GET", f"/v1/jobs/{urllib.parse.quote(job_id, safe='')}/offers")
189
+
190
+ def accept_offer(self, job_id: str, agent_id: str) -> Dict[str, Any]:
191
+ """Called by the job's poster to accept one offer."""
192
+ encoded = urllib.parse.quote(job_id, safe="")
193
+ return self._request(
194
+ "POST",
195
+ f"/v1/jobs/{encoded}/accept",
196
+ {"agentId": agent_id},
197
+ idempotency_key=f"accept:{job_id}:{agent_id}",
198
+ )
199
+
200
+ def cancel_job(self, job_id: str) -> Dict[str, Any]:
201
+ """Called by the job's poster to cancel a not-yet-completed job."""
202
+ encoded = urllib.parse.quote(job_id, safe="")
203
+ return self._request("POST", f"/v1/jobs/{encoded}/cancel", None, idempotency_key=f"cancel:{job_id}")
@@ -0,0 +1,79 @@
1
+ """did:key (Ed25519) identity — must match src/crypto/keys.ts."""
2
+
3
+ import base64
4
+ import hashlib
5
+ from dataclasses import dataclass
6
+ from typing import Union
7
+
8
+ import base58
9
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
10
+ from cryptography.hazmat.primitives import serialization
11
+
12
+ # multicodec value for ed25519-pub (0xed) as a varint: [0xed, 0x01].
13
+ ED25519_MULTICODEC_PREFIX = bytes([0xED, 0x01])
14
+
15
+
16
+ @dataclass
17
+ class Keypair:
18
+ did: str
19
+ public_key: bytes
20
+ private_key: Ed25519PrivateKey
21
+
22
+
23
+ def public_key_to_did(public_key: bytes) -> str:
24
+ prefixed = ED25519_MULTICODEC_PREFIX + public_key
25
+ return "did:key:z" + base58.b58encode(prefixed).decode("ascii")
26
+
27
+
28
+ def did_to_public_key(did: str) -> bytes:
29
+ if not did.startswith("did:key:z"):
30
+ raise ValueError(f"Unsupported DID method: {did}")
31
+ decoded = base58.b58decode(did[len("did:key:z") :])
32
+ if decoded[0:2] != ED25519_MULTICODEC_PREFIX:
33
+ raise ValueError(f"Unsupported key type in DID: {did}")
34
+ return decoded[2:]
35
+
36
+
37
+ def generate_keypair() -> Keypair:
38
+ private_key = Ed25519PrivateKey.generate()
39
+ return keypair_from_private_key(private_key)
40
+
41
+
42
+ def keypair_from_raw_private_key(raw_private_key: bytes) -> Keypair:
43
+ private_key = Ed25519PrivateKey.from_private_bytes(raw_private_key)
44
+ return keypair_from_private_key(private_key)
45
+
46
+
47
+ def keypair_from_private_key(private_key: Ed25519PrivateKey) -> Keypair:
48
+ public_bytes = private_key.public_key().public_bytes(
49
+ encoding=serialization.Encoding.Raw,
50
+ format=serialization.PublicFormat.Raw,
51
+ )
52
+ return Keypair(did=public_key_to_did(public_bytes), public_key=public_bytes, private_key=private_key)
53
+
54
+
55
+ def sign(message: bytes, private_key: Ed25519PrivateKey) -> bytes:
56
+ return private_key.sign(message)
57
+
58
+
59
+ def verify(signature: bytes, message: bytes, did: str) -> bool:
60
+ try:
61
+ public_key = Ed25519PublicKey.from_public_bytes(did_to_public_key(did))
62
+ public_key.verify(signature, message)
63
+ return True
64
+ except Exception:
65
+ return False
66
+
67
+
68
+ def sha256_hex(data: Union[str, bytes]) -> str:
69
+ if isinstance(data, str):
70
+ data = data.encode("utf-8")
71
+ return hashlib.sha256(data).hexdigest()
72
+
73
+
74
+ def to_base64(data: bytes) -> str:
75
+ return base64.b64encode(data).decode("ascii")
76
+
77
+
78
+ def from_base64(s: str) -> bytes:
79
+ return base64.b64decode(s)
@@ -0,0 +1,35 @@
1
+ """Pure receipt-content logic — must match src/core/receiptContent.ts."""
2
+
3
+ from typing import Any, Dict
4
+
5
+ from .canonical import canonicalize
6
+ from .keys import sha256_hex
7
+
8
+
9
+ def compute_receipt_id(agent_a_id: str, agent_b_id: str, input: Dict[str, Any]) -> str:
10
+ base = {
11
+ "jobId": input["jobId"],
12
+ "agentA": {"id": agent_a_id, "role": "requester"},
13
+ "agentB": {"id": agent_b_id, "role": "worker"},
14
+ "task": input["task"],
15
+ "result": input["result"],
16
+ "settlement": input.get("settlement"),
17
+ "verification": input["verification"],
18
+ }
19
+ return f"sha256:{sha256_hex(canonicalize(base))}"
20
+
21
+
22
+ def build_signable_content(agent_a_id: str, agent_b_id: str, input: Dict[str, Any]) -> Dict[str, Any]:
23
+ receipt_id = compute_receipt_id(agent_a_id, agent_b_id, input)
24
+ return {
25
+ "receiptVersion": "1.0",
26
+ "receiptId": receipt_id,
27
+ "jobId": input["jobId"],
28
+ "agentA": {"id": agent_a_id, "role": "requester"},
29
+ "agentB": {"id": agent_b_id, "role": "worker"},
30
+ "task": input["task"],
31
+ "result": input["result"],
32
+ "settlement": input.get("settlement"),
33
+ "verification": input["verification"],
34
+ "dispute": {"status": "none", "windowClosesAt": ""},
35
+ }
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: inamprotocol
3
+ Version: 0.2.0
4
+ Summary: Reference Python SDK for the INAM Protocol — agent identity, execution receipts, jobs, and reputation
5
+ Author: INAM Protocol
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://inamprotocol.org
8
+ Project-URL: Documentation, https://docs.inamprotocol.org
9
+ Project-URL: Repository, https://github.com/inamprotocol/inam-protocol
10
+ Project-URL: Specification, https://docs.inamprotocol.org/spec/
11
+ Project-URL: Changelog, https://github.com/inamprotocol/inam-protocol/blob/main/CHANGELOG.md
12
+ Keywords: inam,agent,ai-agent,identity,reputation,did,protocol
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: cryptography>=42.0.0
23
+ Requires-Dist: base58>=2.1.0
24
+
25
+ # inamprotocol (Python SDK)
26
+
27
+ Reference Python client for the INAM Protocol — parity with the TypeScript `InamClient` in `../src/sdk/client.ts`. See [`../SPEC.md`](../SPEC.md) for the full protocol specification.
28
+
29
+ ## Install (development)
30
+
31
+ ```
32
+ python -m venv .venv
33
+ ./.venv/Scripts/python.exe -m pip install -e . pytest # Windows
34
+ # ./.venv/bin/python -m pip install -e . pytest # macOS/Linux
35
+ ```
36
+
37
+ ## Test
38
+
39
+ ```
40
+ ./.venv/Scripts/python.exe -m pytest -v
41
+ ```
42
+
43
+ `tests/test_interop.py` is the important one: it checks this SDK's `did:key` encoding, canonical JSON, and Ed25519 signing against fixed values generated once by the TypeScript reference implementation (`../scripts/interop-vectors.ts`). If a change here ever breaks that test, a receipt signed by this SDK would stop verifying against a registry or another SDK written in a different language — that's the whole point of the test.
44
+
45
+ ## Usage
46
+
47
+ ```python
48
+ from inamprotocol import InamClient, generate_keypair
49
+
50
+ keypair = generate_keypair()
51
+ client = InamClient("http://localhost:4021", keypair)
52
+
53
+ profile = client.register_agent(["document-extraction"], {"name": "My Agent"})
54
+ print(profile["id"]) # did:key:z...
55
+
56
+ reputation = client.get_reputation(profile["id"])
57
+ ```
58
+
59
+ See `examples/interop_worker.py` for a full worker-side flow (register, link an external identity, submit signed Execution Receipt drafts), and `../scripts/run-interop-demo.sh` for the end-to-end cross-language demo (a TypeScript requester and this Python worker doing real business through the same live registry).
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ inamprotocol/__init__.py
4
+ inamprotocol/canonical.py
5
+ inamprotocol/client.py
6
+ inamprotocol/keys.py
7
+ inamprotocol/receipt.py
8
+ inamprotocol.egg-info/PKG-INFO
9
+ inamprotocol.egg-info/SOURCES.txt
10
+ inamprotocol.egg-info/dependency_links.txt
11
+ inamprotocol.egg-info/requires.txt
12
+ inamprotocol.egg-info/top_level.txt
13
+ tests/test_canonical.py
14
+ tests/test_interop.py
15
+ tests/test_keys.py
@@ -0,0 +1,2 @@
1
+ cryptography>=42.0.0
2
+ base58>=2.1.0
@@ -0,0 +1 @@
1
+ inamprotocol
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "inamprotocol"
7
+ version = "0.2.0"
8
+ description = "Reference Python SDK for the INAM Protocol — agent identity, execution receipts, jobs, and reputation"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "INAM Protocol" }]
13
+ keywords = ["inam", "agent", "ai-agent", "identity", "reputation", "did", "protocol"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ "Topic :: Internet :: WWW/HTTP",
22
+ ]
23
+ dependencies = [
24
+ "cryptography>=42.0.0",
25
+ "base58>=2.1.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://inamprotocol.org"
30
+ Documentation = "https://docs.inamprotocol.org"
31
+ Repository = "https://github.com/inamprotocol/inam-protocol"
32
+ "Specification" = "https://docs.inamprotocol.org/spec/"
33
+ Changelog = "https://github.com/inamprotocol/inam-protocol/blob/main/CHANGELOG.md"
34
+
35
+ [tool.setuptools.packages.find]
36
+ include = ["inamprotocol*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ from inamprotocol.canonical import canonicalize
2
+
3
+
4
+ def test_key_order_independence():
5
+ a = canonicalize({"b": 1, "a": 2, "c": {"z": 1, "y": 2}})
6
+ b = canonicalize({"c": {"y": 2, "z": 1}, "a": 2, "b": 1})
7
+ assert a == b
8
+
9
+
10
+ def test_drops_none_valued_keys():
11
+ assert canonicalize({"a": 1, "b": None}) == '{"a":1}'
12
+
13
+
14
+ def test_preserves_array_order():
15
+ assert canonicalize([3, 1, 2]) == "[3,1,2]"
16
+
17
+
18
+ def test_sensitive_to_value_change():
19
+ assert canonicalize({"amount": "12.50"}) != canonicalize({"amount": "12.51"})
@@ -0,0 +1,65 @@
1
+ """Cross-language correctness: these expected values were generated once by the
2
+ TypeScript reference implementation (see ../../scripts/interop-vectors.ts) from
3
+ a fixed test-only private key. If the Python SDK's did:key encoding,
4
+ canonical-JSON serialization, or Ed25519 signing ever diverges from the
5
+ TypeScript side, this test fails — that divergence is exactly what would make
6
+ a receipt signed by one language's SDK fail to verify against a registry (or
7
+ another SDK) written in the other language.
8
+ """
9
+
10
+ from inamprotocol.canonical import canonicalize
11
+ from inamprotocol.keys import keypair_from_raw_private_key, sign, to_base64, verify
12
+
13
+ # --- Ground truth from `npx tsx scripts/interop-vectors.ts` ---
14
+ PRIVATE_KEY_HEX = "01" * 32
15
+ EXPECTED_PUBLIC_KEY_HEX = "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c"
16
+ EXPECTED_DID = "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX"
17
+ EXPECTED_CANONICAL = (
18
+ '{"agentA":{"id":"did:key:zExampleA","role":"requester"},'
19
+ '"agentB":{"id":"did:key:zExampleB","role":"worker"},'
20
+ '"jobId":"job_interop_1",'
21
+ '"result":{"completedAt":"2026-08-22T00:01:00.000Z","outputHash":"sha256:out"},'
22
+ '"settlement":{"amount":"12.50","currency":"USDC"},'
23
+ '"task":{"capability":"translation.tr-en","createdAt":"2026-08-22T00:00:00.000Z","specHash":"sha256:spec"},'
24
+ '"verification":{"method":"payer_confirmation","outcome":"success"}}'
25
+ )
26
+ MESSAGE = b"inam-interop-test-message"
27
+ EXPECTED_SIGNATURE_B64 = "V3nIJGvwXXN+dNRM5gxyLeECY3fMVLu8baci/JrlArgeElTU3bEThOR7ipfJxGtHvWDIcrEzyIDq/vNkHVAJDA=="
28
+
29
+ SAMPLE_OBJECT = {
30
+ "jobId": "job_interop_1",
31
+ "agentA": {"id": "did:key:zExampleA", "role": "requester"},
32
+ "agentB": {"id": "did:key:zExampleB", "role": "worker"},
33
+ "task": {"capability": "translation.tr-en", "specHash": "sha256:spec", "createdAt": "2026-08-22T00:00:00.000Z"},
34
+ "result": {"outputHash": "sha256:out", "completedAt": "2026-08-22T00:01:00.000Z"},
35
+ "settlement": {"amount": "12.50", "currency": "USDC"},
36
+ "verification": {"method": "payer_confirmation", "outcome": "success"},
37
+ }
38
+
39
+
40
+ def test_did_key_matches_typescript():
41
+ kp = keypair_from_raw_private_key(bytes.fromhex(PRIVATE_KEY_HEX))
42
+ assert kp.public_key.hex() == EXPECTED_PUBLIC_KEY_HEX
43
+ assert kp.did == EXPECTED_DID
44
+
45
+
46
+ def test_canonical_json_matches_typescript():
47
+ assert canonicalize(SAMPLE_OBJECT) == EXPECTED_CANONICAL
48
+
49
+
50
+ def test_signature_is_byte_identical_to_typescript():
51
+ # Ed25519 signatures are deterministic (RFC 8032) — the same key and
52
+ # message MUST produce the exact same signature in any correct
53
+ # implementation, not just a signature that happens to verify.
54
+ kp = keypair_from_raw_private_key(bytes.fromhex(PRIVATE_KEY_HEX))
55
+ signature = sign(MESSAGE, kp.private_key)
56
+ assert to_base64(signature) == EXPECTED_SIGNATURE_B64
57
+
58
+
59
+ def test_python_verifies_a_typescript_produced_signature():
60
+ import base64
61
+
62
+ kp_did = EXPECTED_DID
63
+ signature = base64.b64decode(EXPECTED_SIGNATURE_B64)
64
+ assert verify(signature, MESSAGE, kp_did) is True
65
+ assert verify(signature, b"tampered message", kp_did) is False
@@ -0,0 +1,28 @@
1
+ from inamprotocol.keys import generate_keypair, did_to_public_key, sign, verify
2
+
3
+
4
+ def test_did_key_round_trip():
5
+ kp = generate_keypair()
6
+ assert kp.did.startswith("did:key:z")
7
+ assert did_to_public_key(kp.did) == kp.public_key
8
+
9
+
10
+ def test_sign_and_verify():
11
+ kp = generate_keypair()
12
+ message = b"hello inam"
13
+ signature = sign(message, kp.private_key)
14
+ assert verify(signature, message, kp.did) is True
15
+
16
+
17
+ def test_rejects_tampered_message():
18
+ kp = generate_keypair()
19
+ signature = sign(b"original", kp.private_key)
20
+ assert verify(signature, b"tampered", kp.did) is False
21
+
22
+
23
+ def test_rejects_wrong_signer():
24
+ signer = generate_keypair()
25
+ impostor = generate_keypair()
26
+ message = b"hello inam"
27
+ signature = sign(message, signer.private_key)
28
+ assert verify(signature, message, impostor.did) is False