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.
- agent_dna-0.1.0.dist-info/METADATA +143 -0
- agent_dna-0.1.0.dist-info/RECORD +11 -0
- agent_dna-0.1.0.dist-info/WHEEL +5 -0
- agent_dna-0.1.0.dist-info/licenses/LICENSE +21 -0
- agent_dna-0.1.0.dist-info/top_level.txt +1 -0
- agentdna/__init__.py +16 -0
- agentdna/core.py +120 -0
- agentdna/handler.py +478 -0
- agentdna/node_client.py +67 -0
- agentdna/token.txt +0 -0
- agentdna/trust.py +217 -0
|
@@ -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,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
|
+
}
|
agentdna/node_client.py
ADDED
|
@@ -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
|