treessera 0.1.0__py3-none-any.whl

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.
treessera/__init__.py ADDED
@@ -0,0 +1,102 @@
1
+ """Tessera SDK — build and consume Verified, transactable A2A agents.
2
+
3
+ Two entry points cover almost everything:
4
+
5
+ * :class:`Agent` — the provider. Give it a capability, a price, and a work
6
+ function; it mints a persistent signed identity, publishes a card, negotiates,
7
+ proves itself under challenge, and self-registers::
8
+
9
+ from treessera import Agent
10
+
11
+ agent = Agent(name="Terraform Bot", description="Generates Terraform",
12
+ capability="terraform.generate", list_price=45, min_price=30,
13
+ registry_url="https://registry.example", keys_dir="./keys")
14
+
15
+ @agent.task
16
+ def handle(task):
17
+ return {"main.tf": generate(task.input)}
18
+
19
+ agent.run(port=8100)
20
+
21
+ * :class:`Client` — the requester. Discover, cryptographically verify, negotiate,
22
+ and delegate — peer to peer, never through the registry::
23
+
24
+ from treessera import Client
25
+
26
+ client = Client(registry_url="https://registry.example")
27
+ agent = client.discover("terraform.generate")[0]
28
+ result = client.delegate(agent, {"provider": "aws"}, max_price=40)
29
+
30
+ Everything below the two facades (Identity, Pricing, Evidence, the protocol
31
+ constants) is exported too, for advanced or clean-room use.
32
+ """
33
+
34
+ from treessera.card import build_card
35
+ from treessera.client import Client, ClientError, RemoteAgent
36
+ from treessera.config import (
37
+ A2A_PROTOCOL_VERSION,
38
+ PRINCIPAL_PREFIX,
39
+ SDK_VERSION,
40
+ TRUST_EXTENSION_URI,
41
+ WELL_KNOWN_CARD_PATH,
42
+ )
43
+ from treessera.evidence import Evidence, sha256_hex
44
+ from treessera.identity import (
45
+ Identity,
46
+ derive_principal_id,
47
+ principal_matches_pubkey,
48
+ verify_signature,
49
+ )
50
+ from treessera.negotiation import Pricing
51
+ from treessera.protocol import (
52
+ A2A_METHOD,
53
+ ProtocolError,
54
+ TASK_ACCEPT,
55
+ TASK_COUNTER,
56
+ TASK_OFFER,
57
+ TASK_REQUEST,
58
+ TASK_RESULT,
59
+ TRUST_CHALLENGE,
60
+ TRUST_PROOF,
61
+ )
62
+ from treessera.provider import Agent, Task
63
+ from treessera.server import AgentServer
64
+
65
+ __version__ = SDK_VERSION
66
+
67
+ __all__ = [
68
+ # facades
69
+ "Agent",
70
+ "Task",
71
+ "Client",
72
+ "RemoteAgent",
73
+ "ClientError",
74
+ # identity + crypto
75
+ "Identity",
76
+ "derive_principal_id",
77
+ "principal_matches_pubkey",
78
+ "verify_signature",
79
+ # building blocks
80
+ "Pricing",
81
+ "Evidence",
82
+ "sha256_hex",
83
+ "build_card",
84
+ "AgentServer",
85
+ "ProtocolError",
86
+ # protocol vocabulary
87
+ "TASK_REQUEST",
88
+ "TASK_OFFER",
89
+ "TASK_COUNTER",
90
+ "TASK_ACCEPT",
91
+ "TASK_RESULT",
92
+ "TRUST_CHALLENGE",
93
+ "TRUST_PROOF",
94
+ "A2A_METHOD",
95
+ # constants
96
+ "TRUST_EXTENSION_URI",
97
+ "A2A_PROTOCOL_VERSION",
98
+ "SDK_VERSION",
99
+ "PRINCIPAL_PREFIX",
100
+ "WELL_KNOWN_CARD_PATH",
101
+ "__version__",
102
+ ]
treessera/card.py ADDED
@@ -0,0 +1,59 @@
1
+ """The A2A agent card — the public identity + capability descriptor an agent
2
+ publishes at ``/.well-known/agent-card.json``.
3
+
4
+ The card is what makes an agent discoverable and callable. Declaring the trust
5
+ extension (with the agent's ``principal_id``) is what makes it eligible for the
6
+ Verified tier; the SDK always includes it.
7
+ """
8
+
9
+ from treessera.config import A2A_PROTOCOL_VERSION, TRUST_EXTENSION_URI
10
+
11
+
12
+ def build_card(name, description, url, principal_id, skills,
13
+ version="0.1.0", verification_url=None, requires_auth=False):
14
+ # type: (str, str, str, str, list, str, str, bool) -> dict
15
+ """Build the A2A card. ``skills`` is a list of ``{"id", "name"?,
16
+ "description"?, "tags"?}`` — the ``id`` is the capability the agent offers
17
+ (dot-namespaced), and the registry indexes discovery off it."""
18
+ trust_params = {"principal_id": principal_id}
19
+ if verification_url:
20
+ trust_params["verification_service_url"] = verification_url
21
+
22
+ card = {
23
+ "name": name,
24
+ "description": description,
25
+ "url": url,
26
+ "version": version,
27
+ "protocolVersion": A2A_PROTOCOL_VERSION,
28
+ "preferredTransport": "JSONRPC",
29
+ "capabilities": {
30
+ "streaming": False,
31
+ "pushNotifications": False,
32
+ "extensions": [
33
+ {
34
+ "uri": TRUST_EXTENSION_URI,
35
+ "description": "Tessera: signed identity, negotiated "
36
+ "work, machine-checkable evidence, independent "
37
+ "verification, and provable per-capability reputation.",
38
+ "required": False,
39
+ "params": trust_params,
40
+ }
41
+ ],
42
+ },
43
+ "defaultInputModes": ["application/json"],
44
+ "defaultOutputModes": ["application/json"],
45
+ "skills": [_normalize_skill(s) for s in skills],
46
+ }
47
+ if requires_auth:
48
+ card["securitySchemes"] = {"bearer": {"type": "http", "scheme": "bearer"}}
49
+ card["security"] = [{"bearer": []}]
50
+ return card
51
+
52
+
53
+ def _normalize_skill(skill):
54
+ # type: (object) -> dict
55
+ if isinstance(skill, str):
56
+ return {"id": skill, "name": skill}
57
+ s = dict(skill)
58
+ s.setdefault("name", s.get("id", ""))
59
+ return s
treessera/client.py ADDED
@@ -0,0 +1,242 @@
1
+ """The requester side — discover, verify, negotiate, and delegate work.
2
+
3
+ A requester never proxies through the registry: it looks up candidates, calls
4
+ each agent *directly* at its card ``url``, proves the agent's identity with a
5
+ signature challenge, negotiates a price, and delegates the task. This is the
6
+ peer-to-peer half of Tessera.
7
+
8
+ from treessera import Client
9
+
10
+ client = Client(registry_url="https://registry.example")
11
+ agent = client.discover("terraform.generate")[0] # a RemoteAgent
12
+ if client.verify(agent): # cryptographic proof
13
+ result = client.delegate(agent, {"provider": "aws"}, max_price=40)
14
+ print(result["output"])
15
+ """
16
+
17
+ import base64
18
+ import json
19
+ import os
20
+ import urllib.error
21
+ import urllib.request
22
+
23
+ from treessera import registry as _registry
24
+ from treessera.config import TRUST_EXTENSION_URI, WELL_KNOWN_CARD_PATH
25
+ from treessera.identity import principal_matches_pubkey, verify_signature
26
+ from treessera.protocol import (
27
+ TASK_ACCEPT,
28
+ TASK_COUNTER,
29
+ TASK_OFFER,
30
+ TASK_REQUEST,
31
+ TASK_RESULT,
32
+ TRUST_CHALLENGE,
33
+ TRUST_PROOF,
34
+ payload_of_result,
35
+ wrap_request,
36
+ )
37
+
38
+ _HEADERS = {
39
+ "User-Agent": "treessera-sdk/0.1",
40
+ "Content-Type": "application/json",
41
+ "Accept": "application/json",
42
+ }
43
+
44
+
45
+ class ClientError(Exception):
46
+ pass
47
+
48
+
49
+ class RemoteAgent(object):
50
+ """A discovered agent: its card plus the endpoints derived from it. Kept
51
+ small and serializable so a requester can cache or pass it around."""
52
+
53
+ def __init__(self, card, reputation=None):
54
+ # type: (dict, dict) -> None
55
+ self.card = card or {}
56
+ self.name = self.card.get("name", "")
57
+ self.reputation = reputation or {}
58
+ self.endpoint_url = self.card.get("url") or ""
59
+ self.principal_id = _principal_of(self.card)
60
+ self.verified = False # flips true once verify() proves the identity
61
+
62
+ @property
63
+ def declares_trust(self):
64
+ # type: () -> bool
65
+ return bool(self.principal_id)
66
+
67
+ def capabilities(self):
68
+ # type: () -> list
69
+ return [s.get("id") for s in self.card.get("skills") or [] if s.get("id")]
70
+
71
+
72
+ class Client(object):
73
+ def __init__(self, registry_url=None, auth_token=None, timeout=10.0):
74
+ # type: (str, str, float) -> None
75
+ self.registry_url = registry_url
76
+ self.auth_token = auth_token
77
+ self.timeout = timeout
78
+
79
+ # -- discovery ------------------------------------------------------------
80
+
81
+ def discover(self, capability, min_reputation=None):
82
+ # type: (str, float) -> list
83
+ """Find agents offering ``capability`` via the registry. Returns
84
+ RemoteAgents, best-reputation first (order as the registry ranks them)."""
85
+ if not self.registry_url:
86
+ raise ClientError("no registry_url configured for discovery")
87
+ candidates = _registry.search(self.registry_url, capability, min_reputation, self.timeout)
88
+ return [RemoteAgent(c.get("agent_card"), c.get("reputation_summary")) for c in candidates]
89
+
90
+ def fetch_card(self, url):
91
+ # type: (str) -> RemoteAgent
92
+ """Fetch an agent card directly by URL (base or full well-known URL) —
93
+ for connecting to an agent you already know, no registry involved."""
94
+ card = _get_json(_card_url(url), self.timeout)
95
+ return RemoteAgent(card)
96
+
97
+ # -- verification (the cryptographic proof) -------------------------------
98
+
99
+ def verify(self, agent, nonce=None):
100
+ # type: (RemoteAgent, str) -> bool
101
+ """Prove the agent holds the private key behind its ``principal_id``.
102
+
103
+ Sends a random nonce as a ``trust.challenge`` and checks the returned
104
+ ``trust.proof``: (a) the public key hashes to the declared principal_id
105
+ (binding) AND (b) the signature over the nonce validates. A copied
106
+ principal_id fails — the attacker cannot sign for a key it doesn't hold.
107
+ Never raises; returns False on any failure. Sets ``agent.verified``.
108
+ """
109
+ if not agent.declares_trust:
110
+ return False
111
+ nonce = nonce or _nonce()
112
+ try:
113
+ envelope = self._call(agent, {"type": TRUST_CHALLENGE, "nonce": nonce})
114
+ except ClientError:
115
+ return False
116
+ proof = payload_of_result(envelope)
117
+ if proof.get("type") != TRUST_PROOF:
118
+ return False
119
+ public_key = proof.get("public_key")
120
+ signature = proof.get("signature")
121
+ if not isinstance(public_key, str) or not isinstance(signature, str):
122
+ return False
123
+ bound = principal_matches_pubkey(agent.principal_id, public_key)
124
+ signed = verify_signature(public_key, nonce.encode("utf-8"), signature)
125
+ agent.verified = bool(bound and signed)
126
+ return agent.verified
127
+
128
+ # -- negotiation + delegation ---------------------------------------------
129
+
130
+ def negotiate(self, agent, task_input, max_price=None, counter_price=None):
131
+ # type: (RemoteAgent, dict, float, float) -> dict
132
+ """Run the negotiation and return an accepted offer dict
133
+ (``{task_id, price, ...}``), or raise ClientError if no acceptable
134
+ price is reached.
135
+
136
+ Sends ``task.request`` -> receives ``task.offer``. If the offer exceeds
137
+ ``max_price`` and a ``counter_price`` is given, sends one
138
+ ``task.counter``; a single counter round is the whole surface. The final
139
+ price must be <= ``max_price`` (when set) or the deal is declined."""
140
+ offer = self._expect(
141
+ self._call(agent, {"type": TASK_REQUEST, "input": task_input}),
142
+ TASK_OFFER, "task.offer",
143
+ )
144
+ if max_price is not None and offer.get("price", 0) > max_price:
145
+ proposed = counter_price if counter_price is not None else max_price
146
+ offer = self._expect(
147
+ self._call(agent, {
148
+ "type": TASK_COUNTER,
149
+ "task_id": offer.get("task_id"),
150
+ "proposed_price": proposed,
151
+ }),
152
+ TASK_OFFER, "task.offer (counter response)",
153
+ )
154
+ if max_price is not None and offer.get("price", 0) > max_price:
155
+ raise ClientError(
156
+ "no acceptable price: agent held %s %s, budget was %s"
157
+ % (offer.get("price"), offer.get("currency", ""), max_price))
158
+ return offer
159
+
160
+ def accept(self, agent, offer):
161
+ # type: (RemoteAgent, dict) -> dict
162
+ """Accept an offer and collect the result payload (includes evidence)."""
163
+ return self._expect(
164
+ self._call(agent, {"type": TASK_ACCEPT, "task_id": offer.get("task_id")}),
165
+ TASK_RESULT, "task.result",
166
+ )
167
+
168
+ def delegate(self, agent, task_input, max_price=None, counter_price=None, require_verified=True):
169
+ # type: (RemoteAgent, dict, float, float, bool) -> dict
170
+ """The whole happy path: (verify) -> negotiate -> accept. Returns the
171
+ result payload. By default refuses to transact with an unverified agent
172
+ — the Verified tier is what makes a result trustworthy."""
173
+ if require_verified and not agent.verified and not self.verify(agent):
174
+ raise ClientError(
175
+ "agent %r is not Verified — refusing to transact "
176
+ "(pass require_verified=False to override)" % (agent.name,))
177
+ offer = self.negotiate(agent, task_input, max_price, counter_price)
178
+ return self.accept(agent, offer)
179
+
180
+ # -- transport ------------------------------------------------------------
181
+
182
+ def _call(self, agent, payload):
183
+ # type: (RemoteAgent, dict) -> dict
184
+ if not agent.endpoint_url:
185
+ raise ClientError("agent %r has no callable url" % (agent.name,))
186
+ request = wrap_request(payload)
187
+ headers = dict(_HEADERS)
188
+ headers["X-A2A-Extensions"] = TRUST_EXTENSION_URI
189
+ if self.auth_token:
190
+ headers["Authorization"] = "Bearer " + self.auth_token
191
+ data = json.dumps(request).encode("utf-8")
192
+ req = urllib.request.Request(agent.endpoint_url, data=data, headers=headers, method="POST")
193
+ try:
194
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
195
+ envelope = json.loads(resp.read().decode("utf-8"))
196
+ except urllib.error.HTTPError as exc:
197
+ raise ClientError("agent returned HTTP %s calling %s" % (exc.code, agent.endpoint_url))
198
+ except Exception as exc:
199
+ raise ClientError("could not reach agent at %s: %s" % (agent.endpoint_url, exc))
200
+ if isinstance(envelope, dict) and envelope.get("error"):
201
+ err = envelope["error"]
202
+ raise ClientError("agent error %s: %s" % (err.get("code"), err.get("message")))
203
+ return envelope
204
+
205
+ def _expect(self, envelope, ptype, label):
206
+ # type: (dict, str, str) -> dict
207
+ payload = payload_of_result(envelope)
208
+ if payload.get("type") != ptype:
209
+ raise ClientError("expected %s, got %r" % (label, payload.get("type")))
210
+ return payload
211
+
212
+
213
+ def _principal_of(card):
214
+ # type: (dict) -> str
215
+ for ext in ((card or {}).get("capabilities") or {}).get("extensions") or []:
216
+ if isinstance(ext, dict) and ext.get("uri") == TRUST_EXTENSION_URI:
217
+ return (ext.get("params") or {}).get("principal_id") or ""
218
+ return ""
219
+
220
+
221
+ def _card_url(url):
222
+ # type: (str) -> str
223
+ url = url.rstrip("/")
224
+ if url.endswith(WELL_KNOWN_CARD_PATH):
225
+ return url
226
+ return url + WELL_KNOWN_CARD_PATH
227
+
228
+
229
+ def _get_json(url, timeout):
230
+ # type: (str, float) -> dict
231
+ req = urllib.request.Request(url, headers={"Accept": "application/json",
232
+ "User-Agent": _HEADERS["User-Agent"]})
233
+ try:
234
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
235
+ return json.loads(resp.read().decode("utf-8"))
236
+ except Exception as exc:
237
+ raise ClientError("could not fetch agent card at %s: %s" % (url, exc))
238
+
239
+
240
+ def _nonce():
241
+ # type: () -> str
242
+ return base64.urlsafe_b64encode(os.urandom(24)).decode("ascii").rstrip("=")
treessera/config.py ADDED
@@ -0,0 +1,28 @@
1
+ """Protocol + brand constants — the ONE place the Tessera identity lives.
2
+
3
+ The trust-extension URI is a namespace identifier (compared as an exact
4
+ string), not a URL that gets fetched. It must match on both sides — an agent
5
+ declares it on its card, and the registry/verification service compare against
6
+ it — so change it here and everything (cards, verification, discovery) follows.
7
+ It can also be overridden per-process via the ``TREESSERA_URI`` env var without
8
+ touching code.
9
+ """
10
+
11
+ import os
12
+
13
+ #: The A2A extension URI that marks an agent as a Tessera participant.
14
+ TRUST_EXTENSION_URI = os.environ.get(
15
+ "TREESSERA_URI", "https://treessera.com/extensions/trust/v1"
16
+ )
17
+
18
+ #: A2A protocol version this SDK speaks.
19
+ A2A_PROTOCOL_VERSION = "0.3.0"
20
+
21
+ #: This SDK's version.
22
+ SDK_VERSION = "0.1.0"
23
+
24
+ #: Prefix for Tessera principal identifiers.
25
+ PRINCIPAL_PREFIX = "atp:principal:"
26
+
27
+ #: Where an agent publishes its card and where verifiers fetch it.
28
+ WELL_KNOWN_CARD_PATH = "/.well-known/agent-card.json"
treessera/evidence.py ADDED
@@ -0,0 +1,56 @@
1
+ """Evidence — the machine-checkable proof that a task result is real.
2
+
3
+ A result carries evidence (artifact hashes, self-checks) so an independent
4
+ verification service can confirm it before it counts toward reputation. This is
5
+ what makes Tessera reputation *provable* rather than self-reported.
6
+ """
7
+
8
+ import hashlib
9
+
10
+ import uuid
11
+
12
+
13
+ def sha256_hex(data):
14
+ # type: (object) -> str
15
+ if isinstance(data, str):
16
+ data = data.encode("utf-8")
17
+ elif not isinstance(data, (bytes, bytearray)):
18
+ data = str(data).encode("utf-8")
19
+ return hashlib.sha256(data).hexdigest()
20
+
21
+
22
+ class Evidence(object):
23
+ """Builds the evidence record for a task result.
24
+
25
+ ``artifacts`` maps a filename to its content; each is hashed. ``schema_valid``
26
+ and ``tests_passed`` are the agent's self-checks — the verifier re-runs its
27
+ own, but the agent asserts what it believes.
28
+ """
29
+
30
+ def __init__(self, capability_id, session_id=None):
31
+ # type: (str, str) -> None
32
+ self.capability_id = capability_id
33
+ self.session_id = session_id or "sess-%s" % uuid.uuid4().hex
34
+ self._artifacts = {} # filename -> content
35
+ self.schema_valid = True
36
+ self.tests_passed = True
37
+
38
+ def add_artifact(self, filename, content):
39
+ # type: (str, object) -> Evidence
40
+ self._artifacts[filename] = content
41
+ return self
42
+
43
+ def artifacts(self):
44
+ # type: () -> dict
45
+ return dict(self._artifacts)
46
+
47
+ def to_dict(self):
48
+ # type: () -> dict
49
+ return {
50
+ "evidence_id": "ev-%s" % uuid.uuid4().hex,
51
+ "session_id": self.session_id,
52
+ "capability_id": self.capability_id,
53
+ "schema_valid": bool(self.schema_valid),
54
+ "tests_passed": bool(self.tests_passed),
55
+ "artifact_hashes": {name: sha256_hex(c) for name, c in self._artifacts.items()},
56
+ }
treessera/identity.py ADDED
@@ -0,0 +1,120 @@
1
+ """Agent identity — the ed25519 keypair that makes an agent *provable*.
2
+
3
+ The private key never leaves the process; only the public key and a derived
4
+ ``principal_id`` are ever shared. The id is cryptographically bound to the key
5
+ (``atp:principal:<label>:sha256(b64_pubkey)[:16]``) so a verifier can confirm a
6
+ presented public key really belongs to a claimed id — this is what the
7
+ signature challenge relies on.
8
+ """
9
+
10
+ import base64
11
+ import hashlib
12
+ import os
13
+ import re
14
+
15
+ import nacl.exceptions
16
+ import nacl.signing
17
+
18
+ from treessera.config import PRINCIPAL_PREFIX
19
+
20
+ _KEY_FILENAME = "principal.key"
21
+ _SLUG_RE = re.compile(r"[^a-z0-9]+")
22
+
23
+
24
+ def _b64(raw):
25
+ # type: (bytes) -> str
26
+ return base64.b64encode(raw).decode("ascii")
27
+
28
+
29
+ def _slug(label):
30
+ # type: (str) -> str
31
+ s = _SLUG_RE.sub("-", (label or "agent").lower()).strip("-")
32
+ return s or "agent"
33
+
34
+
35
+ def derive_principal_id(public_key_b64, label="agent"):
36
+ # type: (str, str) -> str
37
+ """Bind a principal id to a public key. The label is cosmetic; only the
38
+ trailing hash is verified, so the id cannot be forged for a key you do not
39
+ hold."""
40
+ digest = hashlib.sha256(public_key_b64.encode("ascii")).hexdigest()[:16]
41
+ return "%s%s:%s" % (PRINCIPAL_PREFIX, _slug(label), digest)
42
+
43
+
44
+ def principal_matches_pubkey(principal_id, public_key_b64):
45
+ # type: (str, str) -> bool
46
+ """True iff ``public_key_b64`` is the key ``principal_id`` was derived
47
+ from. Supports the raw-pubkey form (``principal_id`` == the base64 key)
48
+ for compatibility with user principals."""
49
+ if not isinstance(principal_id, str) or not isinstance(public_key_b64, str):
50
+ return False
51
+ digest = hashlib.sha256(public_key_b64.encode("ascii")).hexdigest()[:16]
52
+ return principal_id.endswith(":" + digest) or principal_id == public_key_b64
53
+
54
+
55
+ class Identity(object):
56
+ """An agent's ed25519 identity, loaded from — or created once in — a
57
+ directory. Reuse the same ``keys_dir`` across restarts to keep a stable
58
+ ``principal_id`` (and therefore stable reputation)."""
59
+
60
+ def __init__(self, signing_key, label="agent"):
61
+ # type: (nacl.signing.SigningKey, str) -> None
62
+ self._sk = signing_key
63
+ self.public_key_b64 = _b64(bytes(signing_key.verify_key))
64
+ self.principal_id = derive_principal_id(self.public_key_b64, label)
65
+
66
+ # -- construction ---------------------------------------------------------
67
+
68
+ @classmethod
69
+ def generate(cls, label="agent"):
70
+ # type: (str) -> Identity
71
+ return cls(nacl.signing.SigningKey.generate(), label)
72
+
73
+ @classmethod
74
+ def load_or_create(cls, keys_dir, label="agent"):
75
+ # type: (str, str) -> Identity
76
+ """Load the key from ``keys_dir`` if present, else create it once. The
77
+ directory gets a ``.gitignore`` so the private key can never be
78
+ committed."""
79
+ os.makedirs(keys_dir, exist_ok=True)
80
+ gitignore = os.path.join(keys_dir, ".gitignore")
81
+ if not os.path.exists(gitignore):
82
+ with open(gitignore, "w") as fh:
83
+ fh.write("*\n")
84
+ path = os.path.join(keys_dir, _KEY_FILENAME)
85
+ if os.path.exists(path):
86
+ with open(path, "rb") as fh:
87
+ seed = base64.b64decode(fh.read().strip())
88
+ return cls(nacl.signing.SigningKey(seed), label)
89
+ sk = nacl.signing.SigningKey.generate()
90
+ with open(path, "wb") as fh:
91
+ fh.write(base64.b64encode(bytes(sk)))
92
+ try:
93
+ os.chmod(path, 0o600)
94
+ except OSError:
95
+ pass
96
+ return cls(sk, label)
97
+
98
+ # -- signing --------------------------------------------------------------
99
+
100
+ def sign(self, message):
101
+ # type: (bytes) -> str
102
+ """Return the base64 signature over ``message``."""
103
+ return _b64(self._sk.sign(message).signature)
104
+
105
+ def sign_text(self, text):
106
+ # type: (str) -> str
107
+ return self.sign(text.encode("utf-8"))
108
+
109
+
110
+ def verify_signature(public_key_b64, message, signature_b64):
111
+ # type: (str, bytes, str) -> bool
112
+ """Verify an ed25519 signature; never raises — returns False on any
113
+ problem."""
114
+ try:
115
+ vk = nacl.signing.VerifyKey(base64.b64decode(public_key_b64))
116
+ vk.verify(message, base64.b64decode(signature_b64))
117
+ return True
118
+ except (ValueError, TypeError, nacl.exceptions.BadSignatureError,
119
+ nacl.exceptions.CryptoError):
120
+ return False
@@ -0,0 +1,60 @@
1
+ """Price negotiation — the offer/counter logic, with a PRIVATE reserve.
2
+
3
+ The reserve (``min_price``) is never serialized: it appears in no card, no
4
+ offer, and no counter response. A counter at or above the reserve is accepted at
5
+ the countered price; below it, the agent holds its list price. This is the whole
6
+ negotiation surface — single-round by design (RFC-0001/KTD6).
7
+ """
8
+
9
+ from treessera.protocol import TASK_OFFER
10
+
11
+
12
+ class Pricing(object):
13
+ def __init__(self, list_price, min_price=None, currency="USD", delivery="immediate"):
14
+ # type: (float, float, str, str) -> None
15
+ if list_price is None:
16
+ raise ValueError("list_price is required")
17
+ self.list_price = float(list_price)
18
+ # reserve defaults to list price (no discounting) and is clamped so it
19
+ # can never exceed the public price
20
+ reserve = self.list_price if min_price is None else float(min_price)
21
+ self.min_price = min(reserve, self.list_price)
22
+ self.currency = currency
23
+ self.delivery = delivery
24
+
25
+ def offer(self, task_id, capability_id, terms=None):
26
+ # type: (str, str, dict) -> dict
27
+ """The public opening offer. Only the list price crosses the wire."""
28
+ payload = {
29
+ "type": TASK_OFFER,
30
+ "task_id": task_id,
31
+ "capability_id": capability_id,
32
+ "price": self.list_price,
33
+ "currency": self.currency,
34
+ "delivery": self.delivery,
35
+ }
36
+ if terms:
37
+ payload["terms"] = terms
38
+ return payload
39
+
40
+ def respond_to_counter(self, task_id, capability_id, proposed_price, terms=None):
41
+ # type: (str, str, float, dict) -> dict
42
+ """Evaluate a counter-offer. At/above the private reserve -> re-offer at
43
+ the countered price (a deal). Below it -> hold the list price. Either way
44
+ the reserve itself is never revealed."""
45
+ try:
46
+ proposed = float(proposed_price)
47
+ except (TypeError, ValueError):
48
+ proposed = None
49
+ price = proposed if (proposed is not None and proposed >= self.min_price) else self.list_price
50
+ payload = {
51
+ "type": TASK_OFFER,
52
+ "task_id": task_id,
53
+ "capability_id": capability_id,
54
+ "price": price,
55
+ "currency": self.currency,
56
+ "delivery": self.delivery,
57
+ }
58
+ if terms:
59
+ payload["terms"] = terms
60
+ return payload