agent-dna 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.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-dna
3
+ Version: 0.1.0
4
+ Summary: AgentDNA: Verifiable and Auditable AI Agents
5
+ Author: Aparna Ullas
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/rubixchain/agentdna
8
+ Project-URL: Repository, https://github.com/rubixchain/agentdna
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: requests>=2.0.0
13
+ Requires-Dist: python-dotenv>=1.0.0
14
+ Requires-Dist: rubix-py
15
+ Dynamic: license-file
16
+
17
+
18
+ # AgentDNA
19
+
20
+ AgentDNA is a package that helps in auditing and verifiability of Agents within a multi-agent communication system.
21
+
22
+
23
+ ## Key Features
24
+ - **Unified trust layer** for multi-agent systems
25
+ - Automatically manages:
26
+ - DID setup
27
+ - Signing traces of conversation
28
+ - Verification of Agent conversation
29
+ - Immutable audit logging
30
+ - Fully compatible with:
31
+ - Google A2A protocol
32
+ - ADK, Crew and LangGraph frameworks
33
+
34
+ ## Installation
35
+ ```
36
+ # Installation via pip
37
+ pip install agent-dna
38
+
39
+ # Installation via uv
40
+ uv add agent-dna
41
+ ```
42
+
43
+ ## Architecture
44
+ AgentDNA internally wraps several layers to keep agent developers free from Rubix-specific complexity.
45
+
46
+ ```
47
+ ┌─────────────────────────┐
48
+ │ Agent Developer │
49
+ │ dna.build() / handle() │
50
+ └─────────────┬───────────┘
51
+ │
52
+ ┌─────────────────────────┐
53
+ │ AgentDNA │
54
+ │ role = "host" | "remote"│
55
+ └─────────────┬───────────┘
56
+ │
57
+ ┌─────────────────────────┐
58
+ │ RubixMessageHandler |
59
+ │ - build host/agent msgs │
60
+ │ - parse & verify inbound│
61
+ │ - NFT execution (host) │
62
+ └─────────────┬───────────┘
63
+ │
64
+ ┌───────────────────────────┐
65
+ │ RubixTrustService │
66
+ │ - DID management │
67
+ │ - sign_envelope() │
68
+ │ - verify_envelope() │
69
+ │ - verify_message_payload()│
70
+ └─────────────┬─────────────┘
71
+ │
72
+ ┌─────────────────────────┐
73
+ │ rubix-py SDK │
74
+ │ RubixClient │
75
+ │ Signer │
76
+ │ Querier │
77
+ └─────────────────────────┘
78
+ ```
79
+
80
+ ## Agent Roles
81
+ AgentDNA supports two roles:
82
+
83
+ ### **Host Agent**
84
+ Responsible for:
85
+ - Signing and sending outgoing **host_request**
86
+ - Verifying inbound **agent_response**
87
+ - Optional **NFT execution** for audit logging
88
+
89
+ ### **Remote Agent**
90
+ Responsible for:
91
+ - Verifying inbound **host_request**
92
+ - Signing outbound **agent_response**
93
+
94
+ ## Quick Start Example
95
+ ### **Initializing**
96
+ ```python
97
+ from agentdna import AgentDNA
98
+
99
+ dna = AgentDNA(alias="<agent alias>", role="<host/remote>")
100
+
101
+ ```
102
+ ### **Remote Agents**
103
+ ```python
104
+ verify_info = await dna.handle(raw_text=raw_message, verify_mode="<light/heavy>")
105
+
106
+ reply = run_llm(original_msg)
107
+
108
+ built = dna.build(
109
+ original_message=original_msg,
110
+ response=reply,
111
+ host_block=host_block,
112
+ )
113
+ ```
114
+
115
+ ### **Host Agent**
116
+ ```python
117
+ outbound = dna.build(
118
+ original_message="Are you free tomorrow?",
119
+ state={"task_id": tid, "context_id": cid}
120
+ )
121
+
122
+ result = await self.dna.handle(
123
+ resp_parts=resp_parts,
124
+ original_task=task,
125
+ remote_name=agent_name,
126
+ )
127
+ ```
128
+
129
+ ## Project Structure
130
+ ```
131
+ agentdna/
132
+ ├── core.py
133
+ ├── handler.py
134
+ ├── trust.py
135
+ ├── node_client.py
136
+ └── ...
137
+ ```
138
+
139
+ ## Examples
140
+ Full Host, ADK, and LangGraph examples can be found inside `examples/`.
141
+
142
+ ## License
143
+ MIT
@@ -0,0 +1,11 @@
1
+ agent_dna-0.1.0.dist-info/licenses/LICENSE,sha256=z-etQrfD3bh3LfZy8QAeZFfOXc756qqm8nF3nmfosVM,1056
2
+ agentdna/__init__.py,sha256=SEjlUt6r4nPJLyQtEDY7AQZI4T5q6d7yiGpdeStgk_o,313
3
+ agentdna/core.py,sha256=e7hm8FRJqV1DyPRzU8n4vn2LFwvQavhfFdR-PaOYgZw,3877
4
+ agentdna/handler.py,sha256=QXsY8Bz93sVFoEpioOLUTjeCwkTko9X1f3nXS5N-Rdc,16450
5
+ agentdna/node_client.py,sha256=otELq96XzxNi0rWJ2tGnzUuB-utUQ-HL3KlX1t5Zy_c,2240
6
+ agentdna/token.txt,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ agentdna/trust.py,sha256=ZtHLXh9SkFoYkdxhu40c0Kn8Szp_7NQPMXwTRjkIbAg,7227
8
+ agent_dna-0.1.0.dist-info/METADATA,sha256=RPJFECGENL1KrJZ3gofAixNwkrt3cx5Q5SDVrvbqqI8,3838
9
+ agent_dna-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ agent_dna-0.1.0.dist-info/top_level.txt,sha256=GXaMD8TQm5cw2l98vgE7d7nYahdrYCuJAmh2-7Z0iS4,9
11
+ agent_dna-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ agentdna
agentdna/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """
2
+ rubix.agent.dna
3
+ Public interface for Rubix agent DNA utilities.
4
+ """
5
+
6
+ from .trust import RubixTrustService
7
+ from .handler import RubixMessageHandler
8
+ from .node_client import NodeClient
9
+ from .core import AgentDNA
10
+
11
+ __all__ = [
12
+ "RubixTrustService",
13
+ "RubixMessageHandler",
14
+ "NodeClient",
15
+ "AgentDNA"
16
+ ]
agentdna/core.py ADDED
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Dict
3
+
4
+ from .trust import RubixTrustService
5
+ from .handler import RubixMessageHandler
6
+
7
+
8
+ class AgentDNA:
9
+ """
10
+ Single entry point for agent developers.
11
+
12
+ They only call:
13
+ dna.build(...) # outbound message (request or response)
14
+ await dna.handle(...) # inbound message (request or response)
15
+
16
+ Behavior is decided purely by `role`:
17
+
18
+ role="host":
19
+ - build -> host_request (host → remote)
20
+ - handle -> host (remote → host, verify + NFT)
21
+
22
+ role="remote":
23
+ - build -> agent_response (remote → host)
24
+ - handle -> remote (host → remote, verify host)
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ alias: str,
30
+ role: str = "remote", # "host" or "remote"
31
+ token_filename: str = "token.txt",
32
+ ) -> None:
33
+ if role not in ("host", "remote"):
34
+ raise ValueError("AgentDNA.role must be 'host' or 'remote'")
35
+
36
+ self.role = role
37
+ self.trust = RubixTrustService(alias=alias)
38
+
39
+ self.handler = RubixMessageHandler(
40
+ alias=alias,
41
+ token_filename=token_filename,
42
+ trust_service=self.trust,
43
+ enable_nft=(role == "host"),
44
+ )
45
+
46
+ # ------------------------------------------------------------------
47
+ # BUILD: outbound messages
48
+ # ------------------------------------------------------------------
49
+ def build(self, **kwargs) -> Dict[str, Any]:
50
+ """
51
+ Host:
52
+ dna.build(
53
+ original_message=task,
54
+ state=tool_context.state or {},
55
+ )
56
+
57
+ Remote:
58
+ dna.build(
59
+ original_message=original_message,
60
+ response=agent_reply_text,
61
+ host_block=host_block,
62
+ extra={...},
63
+ )
64
+ """
65
+ if self.role == "host":
66
+ if "original_message" not in kwargs:
67
+ raise ValueError("Host.build() requires original_message")
68
+ return self.handler.build(
69
+ kind="host_request",
70
+ **kwargs,
71
+ )
72
+
73
+ # remote
74
+ if "original_message" not in kwargs or "response" not in kwargs:
75
+ raise ValueError("Remote.build() requires original_message and response")
76
+ return self.handler.build(
77
+ kind="agent_response",
78
+ **kwargs,
79
+ )
80
+
81
+ # ------------------------------------------------------------------
82
+ # HANDLE: inbound messages
83
+ # ------------------------------------------------------------------
84
+ async def handle(self, **kwargs) -> Dict[str, Any]:
85
+ """
86
+ Host:
87
+ result = await dna.handle(
88
+ resp_parts=resp_parts,
89
+ original_task=task,
90
+ remote_name=agent_name,
91
+ execute_nft=True,
92
+ )
93
+
94
+ Remote:
95
+ verify_info = await dna.handle(
96
+ raw_text=raw,
97
+ verify_mode="light",
98
+ )
99
+ """
100
+ if self.role == "remote":
101
+ # Remote handling inbound from host
102
+ raw_text = kwargs.get("raw_text")
103
+ if raw_text is None:
104
+ raise ValueError("Remote.handle() requires raw_text")
105
+
106
+ mode = kwargs.get("verify_mode", "light")
107
+ # trust.verify_message_payload is sync, but it's fine to call
108
+ return self.trust.verify_message_payload(
109
+ raw_text=raw_text,
110
+ mode=mode,
111
+ )
112
+
113
+ # Host handling inbound from remotes
114
+ for required in ("resp_parts", "original_task", "remote_name"):
115
+ if required not in kwargs:
116
+ raise ValueError(f"Host.handle() requires {required}")
117
+ return await self.handler.handle(
118
+ kind="host",
119
+ **kwargs,
120
+ )
agentdna/handler.py ADDED
@@ -0,0 +1,478 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import uuid
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from .trust import RubixTrustService
10
+
11
+
12
+ def load_nft_config() -> dict:
13
+ """
14
+ Load NFT settings (paths + value + init data) from env,
15
+ with defaults suitable for the pickleball demo.
16
+ """
17
+ return {
18
+ "artifact_path": os.getenv("NFT_ARTIFACT_PATH", "artifacts/pickleball.json"),
19
+ "metadata_path": os.getenv("NFT_METADATA_PATH", "artifacts/pickleball-meta.json"),
20
+ "value": float(os.getenv("NFT_VALUE", "0.001")),
21
+ "data": os.getenv("NFT_INIT_DATA", "init data"),
22
+ }
23
+
24
+
25
+ class RubixMessageHandler:
26
+ """
27
+ High-level Rubix helper for A2A agents.
28
+
29
+ It exposes ONLY two main calls for developers:
30
+
31
+ - build(...)
32
+ - handle(...)
33
+
34
+ and hides all Rubix / NFT details inside.
35
+
36
+ Usage patterns
37
+ --------------
38
+
39
+ 1) Host → Remote: build the signed host block, then embed into A2A message:
40
+
41
+ dna = RubixMessageHandler(alias="host")
42
+
43
+ built = dna.build(
44
+ kind="host_request",
45
+ original_message=task_text,
46
+ state={"task_id": task_id, "context_id": context_id}, # optional
47
+ )
48
+
49
+ # built["host_json"] is what you put into the A2A text part
50
+ host_json = built["host_json"]
51
+ message_id = built["message_id"]
52
+ context_id = built["context_id"]
53
+
54
+ # A2A payload (example)
55
+ payload = {
56
+ "message": {
57
+ "role": "user",
58
+ "parts": [
59
+ {"type": "text", "text": host_json},
60
+ ],
61
+ "messageId": message_id,
62
+ "contextId": context_id,
63
+ }
64
+ }
65
+
66
+ 2) Remote agent (Karley / Nate / Kaitlyn) when receiving a message:
67
+
68
+ dna = RubixMessageHandler(alias="karley", enable_nft=False)
69
+
70
+ verify_info = await dna.handle(
71
+ kind="incoming_request",
72
+ raw_text=raw_from_context,
73
+ verify_mode="light", # or "heavy" in future
74
+ )
75
+
76
+ original_message = verify_info["original_message"]
77
+ host_block = verify_info["host_block"]
78
+ host_ok = verify_info["host_ok"]
79
+ trust_issues = verify_info["trust_issues"]
80
+
81
+ # Use original_message for your local LLM/ADK agent
82
+
83
+ 3) Remote agent → Host: sign its response in a symmetric way:
84
+
85
+ built_resp = dna.build(
86
+ kind="agent_response",
87
+ original_message=original_message,
88
+ response=agent_result,
89
+ host_block=host_block,
90
+ extra={"host_trust_issues": trust_issues},
91
+ )
92
+
93
+ # Put built_resp["combined_json"] into your A2A text part
94
+
95
+ 4) Host handling remote responses (verify + optional NFT):
96
+
97
+ result = await dna.handle(
98
+ kind="host_response",
99
+ resp_parts=resp_parts, # list of {"text": "..."} from A2A artifacts
100
+ original_task=task_text,
101
+ remote_name=agent_name,
102
+ execute_nft=True, # will store in NFT if verification passes
103
+ )
104
+
105
+ messages = result["messages"] # verified messages w/ host+agent blocks
106
+ trust_issues = result["trust_issues"] # any issues
107
+ nft_result = result["nft_result"] # optional: NFT execution response / None
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ alias: str,
113
+ token_filename: str = "token.txt",
114
+ trust_service: Optional[RubixTrustService] = None,
115
+ enable_nft: bool = True,
116
+ ) -> None:
117
+ # Trust layer (sign + verify + verify_message_payload)
118
+ self.trust = trust_service or RubixTrustService(alias=alias)
119
+ self.did = self.trust.did
120
+ self.signer = self.trust.signer
121
+
122
+ # NFT config
123
+ self.enable_nft = enable_nft
124
+ self.nft_cfg = load_nft_config()
125
+ self.last_parts: List[Dict[str, Any]] = []
126
+
127
+ # Where to store NFT token (host project, not site-packages)
128
+ self.token_path: Optional[Path] = None
129
+ self.nft_token: Optional[str] = None
130
+
131
+ if self.enable_nft:
132
+ env_path = os.getenv("AGENTDNA_TOKEN_PATH")
133
+ if env_path:
134
+ self.token_path = Path(env_path)
135
+ else:
136
+ project_root = Path.cwd() # e.g. /.../host_agent_adk
137
+ token_dir = project_root / ".agentdna"
138
+ token_dir.mkdir(parents=True, exist_ok=True)
139
+ self.token_path = token_dir / token_filename
140
+
141
+ print("Path:", self.token_path)
142
+ self.nft_token = self._load_or_deploy_nft()
143
+ print("✅ Rubix NFT for alias", alias, ":", self.nft_token)
144
+ else:
145
+ print("ℹ️ RubixMessageHandler(enable_nft=False) – NFT operations disabled")
146
+
147
+ # ======================================================================
148
+ # Public API: BUILD
149
+ # ======================================================================
150
+
151
+ def build(
152
+ self,
153
+ *,
154
+ kind: str,
155
+ original_message: str,
156
+ state: Optional[Dict[str, Any]] = None,
157
+ response: Optional[str] = None,
158
+ host_block: Optional[Dict[str, Any]] = None,
159
+ extra: Optional[Dict[str, Any]] = None,
160
+ ) -> Dict[str, Any]:
161
+ """
162
+ Unified build() method.
163
+
164
+ kind:
165
+ - "host_request" : host builds + signs a host envelope
166
+ - "agent_response" : remote builds + signs its agent envelope and
167
+ combines it with the host block
168
+ """
169
+ kind = (kind or "").lower().strip()
170
+ state = state or {}
171
+ extra = extra or {}
172
+ print(f"[RubixMessageHandler.build] kind={kind}")
173
+ if kind == "host_request":
174
+ # host → remote
175
+ return self._build_host_request(
176
+ original_message=original_message,
177
+ state=state,
178
+ )
179
+
180
+ if kind == "agent_response":
181
+ # remote → host
182
+ if response is None:
183
+ raise ValueError("build(kind='agent_response') requires `response`.")
184
+ return self._build_agent_response(
185
+ original_message=original_message,
186
+ response=response,
187
+ host_block=host_block,
188
+ extra=extra,
189
+ )
190
+
191
+ raise ValueError(f"Unsupported build kind: {kind}")
192
+ # ======================================================================
193
+ # API: HANDLE
194
+ # ======================================================================
195
+
196
+ async def handle(
197
+ self,
198
+ *,
199
+ kind: str,
200
+ raw_text: Optional[str] = None,
201
+ resp_parts: Optional[List[Dict[str, Any]]] = None,
202
+ original_task: Optional[str] = None,
203
+ remote_name: Optional[str] = None,
204
+ verify_mode: str = "light",
205
+ execute_nft: bool = True,
206
+ ) -> Dict[str, Any]:
207
+ kind = (kind or "").lower().strip()
208
+
209
+ if kind == "remote":
210
+ # remote agent handling inbound from host
211
+ if raw_text is None:
212
+ raise ValueError("handle(kind='remote') requires raw_text.")
213
+ return self.trust.verify_message_payload(raw_text=raw_text, mode=verify_mode)
214
+
215
+ if kind == "host":
216
+ # host handling inbound from remote
217
+ if resp_parts is None or original_task is None or remote_name is None:
218
+ raise ValueError(
219
+ "handle(kind='host') requires resp_parts, original_task, remote_name."
220
+ )
221
+ return await self._handle_host_response(
222
+ resp_parts=resp_parts,
223
+ original_task=original_task,
224
+ remote_name=remote_name,
225
+ execute_nft=execute_nft,
226
+ )
227
+
228
+ raise ValueError(f"Unsupported handle kind: {kind}")
229
+
230
+ # ======================================================================
231
+ # Internal: host request building
232
+ # ======================================================================
233
+
234
+ def _build_host_request(
235
+ self,
236
+ original_message: str,
237
+ state: Dict[str, Any],
238
+ ) -> Dict[str, Any]:
239
+ task_id = state.get("task_id") or str(uuid.uuid4())
240
+ context_id = state.get("context_id") or str(uuid.uuid4())
241
+ message_id = str(uuid.uuid4())
242
+
243
+ host_envelope = {
244
+ "original_message": original_message,
245
+ "task_id": task_id,
246
+ "context_id": context_id,
247
+ "message_id": message_id,
248
+ "timestamp": datetime.utcnow().isoformat() + "Z",
249
+ }
250
+
251
+ host_block = self.trust.sign_envelope(host_envelope)
252
+
253
+ host_json = json.dumps(
254
+ {"host": host_block},
255
+ separators=(",", ":"),
256
+ sort_keys=True,
257
+ )
258
+
259
+ return {
260
+ "kind": "host_request",
261
+ "host_block": host_block,
262
+ "host_json": host_json,
263
+ "task_id": task_id,
264
+ "context_id": context_id,
265
+ "message_id": message_id,
266
+ }
267
+
268
+ # ======================================================================
269
+ # Internal: agent response building
270
+ # ======================================================================
271
+
272
+ def _build_agent_response(
273
+ self,
274
+ original_message: str,
275
+ response: str,
276
+ host_block: Optional[Dict[str, Any]],
277
+ extra: Dict[str, Any],
278
+ ) -> Dict[str, Any]:
279
+ envelope = {
280
+ "original_message": original_message,
281
+ "response": response,
282
+ }
283
+ # Optionally attach trust metadata (e.g. host_trust_issues)
284
+ envelope.update(extra)
285
+
286
+ agent_block = self.trust.sign_envelope(envelope)
287
+
288
+ combined: Dict[str, Any] = {
289
+ "agent": agent_block,
290
+ }
291
+ if host_block is not None:
292
+ combined["host"] = host_block
293
+
294
+ combined_json = json.dumps(
295
+ combined,
296
+ separators=(",", ":"),
297
+ sort_keys=True,
298
+ )
299
+
300
+ return {
301
+ "kind": "agent_response",
302
+ "host_block": host_block,
303
+ "agent_block": agent_block,
304
+ "envelope": envelope,
305
+ "combined_json": combined_json,
306
+ }
307
+
308
+ # ======================================================================
309
+ # Internal: NFT helpers
310
+ # ======================================================================
311
+
312
+ def execute_nft(self, nft_address: str, payload: Any) -> Dict[str, Any]:
313
+ """
314
+ Execute an NFT using the signer from the trust service.
315
+ """
316
+ nft_data = json.dumps(payload)
317
+
318
+ try:
319
+ print("NFT address:", nft_address)
320
+ print("NFT data:", nft_data)
321
+ response = self.signer.execute_nft(
322
+ nft_address=nft_address,
323
+ nft_data=nft_data,
324
+ )
325
+ except Exception as e:
326
+ raise RuntimeError(f"Rubix execute_nft call failed: {e}")
327
+
328
+ if not response.get("status", False):
329
+ raise RuntimeError(
330
+ f"NFT Execution Failed: {response.get('message', '<no message>')}"
331
+ )
332
+
333
+ return response
334
+
335
+ def _load_or_deploy_nft(self) -> str:
336
+ if not self.token_path:
337
+ raise RuntimeError("NFT token path not initialized (enable_nft=False?)")
338
+
339
+ if self.token_path.exists():
340
+ token = self.token_path.read_text(encoding="utf-8").strip()
341
+ if token:
342
+ print("ℹ️ Using existing NFT token:", token)
343
+ return token
344
+ else:
345
+ print(f"⚠️ {self.token_path} is empty, deploying new NFT…")
346
+ else:
347
+ print(f"⚠️ token.txt not found at {self.token_path}, deploying new NFT…")
348
+
349
+ print("Creating a new NFT")
350
+ print("Artifact path:", self.nft_cfg["artifact_path"])
351
+ print("Metadata path:", self.nft_cfg["metadata_path"])
352
+
353
+ resp = self.signer.deploy_nft(
354
+ artifact_file=self.nft_cfg["artifact_path"],
355
+ metadata_file=self.nft_cfg["metadata_path"],
356
+ nft_value=self.nft_cfg["value"] or 0.001,
357
+ nft_data=self.nft_cfg["data"] or "init data",
358
+ )
359
+ if resp.get("error"):
360
+ raise RuntimeError(f"NFT deployment failed: {resp['error']}")
361
+
362
+ nft_address = resp["nft_address"]
363
+ self.token_path.write_text(nft_address, encoding="utf-8")
364
+ print("🚀 Deployed new NFT:", nft_address)
365
+ return nft_address
366
+
367
+ # ======================================================================
368
+ # Internal: host handling remote responses
369
+ # ======================================================================
370
+
371
+ async def _handle_host_response(
372
+ self,
373
+ resp_parts: List[Dict[str, Any]],
374
+ original_task: str,
375
+ remote_name: str,
376
+ execute_nft: bool,
377
+ ) -> Dict[str, Any]:
378
+ """
379
+ Old handle_response_parts, but wrapped into handle(kind="host_response").
380
+ """
381
+ verified: List[Dict[str, Any]] = []
382
+ trust_issues: List[str] = []
383
+ error_msg: Optional[str] = None
384
+ nft_result: Optional[Dict[str, Any]] = None
385
+
386
+ for part in resp_parts:
387
+ raw_text = part.get("text") or part.get("content", "")
388
+ try:
389
+ payload = json.loads(raw_text)
390
+ except (TypeError, json.JSONDecodeError):
391
+ continue
392
+
393
+ # Expect {"host": {...}, "agent": {...}}
394
+ if not isinstance(payload, dict) or "agent" not in payload:
395
+ continue
396
+
397
+ host_block = payload.get("host")
398
+ agent_block = payload["agent"]
399
+
400
+ signer_did = agent_block.get("agent")
401
+ env = agent_block.get("envelope", {})
402
+ sig = agent_block.get("signature")
403
+
404
+ if not (signer_did and env and sig):
405
+ trust_issues.append("Missing fields in agent block")
406
+ print("Missing fields in agent block")
407
+ continue
408
+
409
+ # verify via trust service
410
+ if not self.trust.verify_envelope(signer_did, env, sig):
411
+ trust_issues.append(f"Invalid signature from {signer_did}")
412
+ print(f"Invalid signature from {signer_did}")
413
+ continue
414
+
415
+ # Optional consistency check
416
+ if env.get("original_message") != original_task:
417
+ trust_issues.append("Original message mismatch")
418
+ print("Original message mismatch")
419
+
420
+ verified.append(
421
+ {
422
+ "host": host_block,
423
+ "agent": agent_block,
424
+ "agent_sig_valid": True,
425
+ }
426
+ )
427
+ print("Verified agent block from", signer_did)
428
+
429
+ if not verified and not trust_issues:
430
+ error_msg = "No valid envelope response"
431
+ print("No valid envelope response")
432
+
433
+ self.last_parts = verified
434
+
435
+ # Optional NFT execution on host
436
+ if (
437
+ self.enable_nft
438
+ and execute_nft
439
+ and verified
440
+ and self.nft_token is not None
441
+ ):
442
+ try:
443
+ payload = self._build_nft_payload(remote_name)
444
+ nft_result = await asyncio.to_thread(
445
+ self.execute_nft,
446
+ self.nft_token,
447
+ payload,
448
+ )
449
+ print("🚀 NFT execution result:", nft_result)
450
+ except Exception as e:
451
+ print("⚠️ NFT execution failed:", e)
452
+
453
+ return {
454
+ "messages": verified,
455
+ "trust_issues": trust_issues or None,
456
+ "error": error_msg,
457
+ "nft_result": nft_result,
458
+ }
459
+
460
+ def _build_nft_payload(self, remote_name: str) -> Dict[str, Any]:
461
+ """
462
+ Build the JSON that will be stored in the NFT.
463
+ """
464
+ host_block = None
465
+ responses: List[Dict[str, Any]] = []
466
+ for entry in self.last_parts:
467
+ if not host_block and entry.get("host"):
468
+ host_block = entry["host"]
469
+ if entry.get("agent"):
470
+ responses.append(entry["agent"])
471
+
472
+ return {
473
+ "comment": f"Pickleball scheduling with {remote_name}",
474
+ "executor": "host_agent",
475
+ "did": self.did,
476
+ "host": host_block,
477
+ "responses": responses,
478
+ }
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import os, json, requests
4
+ from typing import Any, Dict, Optional, List
5
+
6
+ class NodeClient:
7
+ """
8
+ Minimal client for your local node.
9
+ Resolve base_url from (in order): explicit base_url -> framework name -> port -> config.json -> ENV.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ alias: Optional[str] = None,
15
+ base_url: Optional[str] = None,
16
+ config_path: Optional[Union[str, Path]] = None,
17
+ ):
18
+ if not config_path:
19
+
20
+ config_path = Path(__file__).resolve().parent.parent / "config.json"
21
+
22
+ print("config path:", config_path)
23
+
24
+ port = self._read_port(alias, config_path)
25
+
26
+ self.base_url = (
27
+ base_url
28
+ or os.getenv("BASE_URL")
29
+ or f"http://localhost:{port or self._read_port_from_config(alias, config_path)}"
30
+ )
31
+
32
+ def get_base_url(self) -> str:
33
+ return self.base_url
34
+
35
+
36
+ @staticmethod
37
+ def _read_port(alias: str, config_path: Optional[Union[str, Path]]) -> Optional[int]:
38
+ """
39
+ Reads the port from config.json for the given framework name.
40
+ Example: framework="host" → key="host_port"
41
+ """
42
+ path = Path(os.getenv("CONFIG_PATH") or (config_path or NodeClient._default_config_path()))
43
+ try:
44
+ with Path(path).open("r", encoding="utf-8") as f:
45
+ cfg = json.load(f)
46
+ key = f"{alias.lower()}"
47
+ return int(cfg.get(key))
48
+ except Exception:
49
+ return None
50
+
51
+ @staticmethod
52
+ def _read_port_from_config(alias: str, config_path: Optional[Union[str, Path]]) -> Optional[int]:
53
+ """
54
+ Fallback to reading generic port or langgraph_port if no framework given.
55
+ """
56
+ path = Path(os.getenv("CONFIG_PATH") or (config_path or NodeClient._default_config_path()))
57
+ try:
58
+ with Path(path).open("r", encoding="utf-8") as f:
59
+ cfg = json.load(f)
60
+ return int(cfg.get("port") or cfg.get(alias))
61
+ except Exception:
62
+ return None
63
+
64
+ @staticmethod
65
+ def _default_config_path() -> Path:
66
+ here = Path(__file__).resolve()
67
+ return here.parents[2] / "config.json"
agentdna/token.txt ADDED
File without changes
agentdna/trust.py ADDED
@@ -0,0 +1,217 @@
1
+ import json
2
+ import os
3
+ from typing import Any, Dict, Optional, List
4
+ from pathlib import Path
5
+ import requests
6
+ from rubix.client import RubixClient
7
+ from rubix.signer import Signer
8
+ from rubix.did import online_signature_verify, signatureResponseError
9
+
10
+ from .node_client import NodeClient
11
+
12
+
13
+ class RubixTrustService:
14
+ """
15
+ Handles:
16
+ - RubixClient + Signer
17
+ - DID management
18
+ - Signing envelopes
19
+ - Verifying signatures via /api/verify-signature
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ alias: str,
25
+ config_path: str = "",
26
+ timeout: float = 300.0,
27
+ ) -> None:
28
+ node = NodeClient(alias=alias)
29
+ self.base_url = node.get_base_url().rstrip("/")
30
+ self.timeout = timeout
31
+
32
+ config_dir = ""
33
+ if config_path == "":
34
+ home_dir = Path.home()
35
+ config_dir = os.path.join(home_dir, ".agentdna")
36
+ else:
37
+ config_dir = config_path
38
+
39
+ client = RubixClient(node_url=self.base_url, timeout=timeout)
40
+
41
+ self.signer = Signer(rubixClient=client, alias=alias, config_path=config_dir)
42
+ self.did = self.signer.did
43
+
44
+ print("✅ RubixTrustService DID:", self.did)
45
+ print("✅ RubixTrustService base URL:", self.base_url)
46
+
47
+ # ---------- signing ----------
48
+
49
+ def sign_envelope(self, envelope: Dict[str, Any]) -> Dict[str, Any]:
50
+ """
51
+ Sign an envelope dict and return a block:
52
+ { "agent": <did>, "envelope": {...}, "signature": "<hex>" }
53
+ """
54
+ print("Agent is going to sign")
55
+ print("Public Key (hex): ", self.signer.get_keypair().public_key)
56
+ keypair = self.signer.get_keypair()
57
+
58
+ envelope_json = json.dumps(envelope, sort_keys=True)
59
+ envelope_bytes = envelope_json.encode("utf-8")
60
+ signature_bytes = keypair.sign(envelope_bytes)
61
+ signature_hex = signature_bytes.hex()
62
+ return {
63
+ "agent": self.did,
64
+ "envelope": envelope,
65
+ "signature": signature_hex,
66
+ }
67
+
68
+ # ---------- verification ----------
69
+
70
+ def verify_envelope(
71
+ self,
72
+ signer_did: str,
73
+ envelope: Dict[str, Any],
74
+ signature: str,
75
+ timeout: Optional[float] = None,
76
+ ) -> bool:
77
+ envelope_json = json.dumps(envelope, sort_keys=True)
78
+ message_bytes = envelope_json.encode("utf-8")
79
+
80
+ try:
81
+ signature_bytes = bytes.fromhex(signature)
82
+ except ValueError:
83
+ print("⚠️ verify_envelope: invalid hex signature string")
84
+ return False
85
+
86
+ try:
87
+ is_valid = online_signature_verify(
88
+ rubixNodeBaseUrl=self.base_url,
89
+ did=signer_did,
90
+ message=message_bytes,
91
+ signature=signature_bytes,
92
+ )
93
+ return bool(is_valid)
94
+ except signatureResponseError as e:
95
+ print(f"⚠️ verify_envelope error: {e}")
96
+ return False
97
+
98
+ def verify_message_payload(
99
+ self,
100
+ raw_text: str,
101
+ mode: str = "light",
102
+ ) -> Dict[str, Any]:
103
+ effective_mode = (mode or os.getenv("AGENTDNA_VERIFY_MODE") or "light").lower()
104
+
105
+ result: Dict[str, Any] = {
106
+ "original_message": raw_text,
107
+ "host_block": None,
108
+ "host_ok": None,
109
+ "trust_issues": [],
110
+ "agent_checks": [],
111
+ "verified": False, # final overall flag
112
+ }
113
+
114
+ if not raw_text:
115
+ return result
116
+
117
+ try:
118
+ payload = json.loads(raw_text)
119
+ except Exception:
120
+ # plain text: nothing to verify
121
+ return result
122
+
123
+ if not isinstance(payload, dict):
124
+ return result
125
+
126
+ host_block = None
127
+
128
+ if "host" in payload and isinstance(payload["host"], dict):
129
+ host_block = payload["host"]
130
+ elif all(k in payload for k in ("agent", "envelope", "signature")):
131
+ host_block = payload
132
+
133
+ result["host_block"] = host_block
134
+
135
+ env = None
136
+ if isinstance(host_block, dict):
137
+ env = host_block.get("envelope", {})
138
+ if isinstance(env, dict):
139
+ orig = env.get("original_message")
140
+ if isinstance(orig, str):
141
+ result["original_message"] = orig
142
+
143
+ # Start optimistic and flip to False when something fails
144
+ overall_ok = True
145
+
146
+ # ---- Host verification (always done) ----
147
+ if isinstance(host_block, dict) and isinstance(env, dict):
148
+ signer_did = host_block.get("agent")
149
+ sig = host_block.get("signature")
150
+
151
+ if signer_did and sig:
152
+ ok = self.verify_envelope(signer_did, env, sig)
153
+ result["host_ok"] = bool(ok)
154
+ if not ok:
155
+ overall_ok = False
156
+ result["trust_issues"].append(
157
+ f"Invalid host signature for DID {signer_did}"
158
+ )
159
+ else:
160
+ overall_ok = False
161
+ result["trust_issues"].append(
162
+ "Host block missing agent/envelope/signature"
163
+ )
164
+ else:
165
+ overall_ok = False
166
+ result["trust_issues"].append("No host block found in payload")
167
+
168
+ # ---- Agent verification (heavy mode) ----
169
+ if effective_mode == "heavy":
170
+ agent_blocks: List[Dict[str, Any]] = []
171
+
172
+ if "agent" in payload and isinstance(payload["agent"], dict):
173
+ agent_blocks.append(payload["agent"])
174
+
175
+ responses = payload.get("responses")
176
+ if isinstance(responses, list):
177
+ for r in responses:
178
+ if isinstance(r, dict):
179
+ agent_blocks.append(r)
180
+
181
+ for ab in agent_blocks:
182
+ a_env = ab.get("envelope", {})
183
+ a_sig = ab.get("signature")
184
+ a_did = ab.get("agent")
185
+
186
+ if not (a_did and a_sig and isinstance(a_env, dict)):
187
+ overall_ok = False
188
+ result["agent_checks"].append(
189
+ {
190
+ "agent": a_did or "<unknown>",
191
+ "ok": False,
192
+ "envelope": a_env if isinstance(a_env, dict) else {},
193
+ "reason": "Agent block missing agent/envelope/signature",
194
+ }
195
+ )
196
+ result["trust_issues"].append(
197
+ "Agent block missing agent/envelope/signature"
198
+ )
199
+ continue
200
+
201
+ ok = self.verify_envelope(a_did, a_env, a_sig)
202
+ result["agent_checks"].append(
203
+ {
204
+ "agent": a_did,
205
+ "ok": bool(ok),
206
+ "envelope": a_env,
207
+ "reason": None if ok else "Agent signature invalid",
208
+ }
209
+ )
210
+ if not ok:
211
+ overall_ok = False
212
+ result["trust_issues"].append(
213
+ f"Invalid signature from agent {a_did}"
214
+ )
215
+
216
+ result["verified"] = overall_ok
217
+ return result