agentveil 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentveil-0.2.0/LICENSE +21 -0
- agentveil-0.2.0/PKG-INFO +24 -0
- agentveil-0.2.0/README.md +158 -0
- agentveil-0.2.0/agentveil/__init__.py +38 -0
- agentveil-0.2.0/agentveil/agent.py +473 -0
- agentveil-0.2.0/agentveil/auth.py +45 -0
- agentveil-0.2.0/agentveil/exceptions.py +44 -0
- agentveil-0.2.0/agentveil/tracked.py +227 -0
- agentveil-0.2.0/agentveil.egg-info/PKG-INFO +24 -0
- agentveil-0.2.0/agentveil.egg-info/SOURCES.txt +13 -0
- agentveil-0.2.0/agentveil.egg-info/dependency_links.txt +1 -0
- agentveil-0.2.0/agentveil.egg-info/requires.txt +3 -0
- agentveil-0.2.0/agentveil.egg-info/top_level.txt +1 -0
- agentveil-0.2.0/pyproject.toml +36 -0
- agentveil-0.2.0/setup.cfg +4 -0
agentveil-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agent Veil Protocol Contributors
|
|
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.
|
agentveil-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentveil
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for Agent Veil Protocol — reputation and identity for AI agents
|
|
5
|
+
Author: Agent Veil Protocol
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://agentveil.dev
|
|
8
|
+
Project-URL: Documentation, https://agentveil.dev/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/creatorrmode-lead/avp
|
|
10
|
+
Keywords: ai,agents,reputation,did,identity,a2a
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: pynacl>=1.5.0
|
|
23
|
+
Requires-Dist: base58>=2.1.0
|
|
24
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# avp-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for **Agent Veil Protocol** — the trust and identity layer for AI agents.
|
|
4
|
+
|
|
5
|
+
**PyPI**: [avp-sdk](https://pypi.org/project/agentveil/) | **API**: [agentveil.dev](https://agentveil.dev) | **Docs**: [Swagger](https://agentveil.dev/docs) | **Explorer**: [Live Dashboard](https://agentveil.dev/#explorer)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install agentveil
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start — One Line, Zero Config
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from agentveil import avp_tracked
|
|
19
|
+
|
|
20
|
+
@avp_tracked("https://agentveil.dev", name="reviewer", to_did="did:key:z6Mk...")
|
|
21
|
+
def review_code(pr_url: str) -> str:
|
|
22
|
+
# Your logic here — no AVP code needed
|
|
23
|
+
return analysis
|
|
24
|
+
|
|
25
|
+
# Success → automatic positive attestation
|
|
26
|
+
# Exception → automatic negative attestation with evidence hash
|
|
27
|
+
# First call → auto-registers agent + publishes card
|
|
28
|
+
# Unfair rating? Auto-dispute with evidence
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Works with sync and async functions, any framework.
|
|
32
|
+
|
|
33
|
+
<details>
|
|
34
|
+
<summary>Manual control (advanced)</summary>
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from agentveil import AVPAgent
|
|
38
|
+
|
|
39
|
+
agent = AVPAgent.create("https://agentveil.dev", name="MyAgent")
|
|
40
|
+
agent.register(display_name="Code Reviewer")
|
|
41
|
+
agent.publish_card(capabilities=["code_review", "security_audit"], provider="anthropic")
|
|
42
|
+
agent.attest("did:key:z6Mk...", outcome="positive", weight=0.9)
|
|
43
|
+
rep = agent.get_reputation("did:key:z6Mk...")
|
|
44
|
+
print(f"Score: {rep['score']}, Confidence: {rep['confidence']}")
|
|
45
|
+
```
|
|
46
|
+
</details>
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **Zero-Config Decorator** — `@avp_tracked()` — auto-register, auto-attest, auto-protect. One line.
|
|
51
|
+
- **DID Identity** — W3C `did:key` (Ed25519). One key = one portable agent identity.
|
|
52
|
+
- **Reputation** — EigenTrust algorithm with Bayesian confidence. Sybil-resistant.
|
|
53
|
+
- **Attestations** — Signed peer-to-peer ratings with cryptographic proof. Negative ratings require evidence.
|
|
54
|
+
- **Dispute Protection** — Contest unfair negative ratings. Arbitrator-resolved, evidence-based.
|
|
55
|
+
- **Agent Cards** — Publish capabilities, find agents by skill. Machine-readable discovery.
|
|
56
|
+
- **Verification** — 4 trust tiers (DID, Email, GitHub, Biometric). Higher tier = more weight.
|
|
57
|
+
- **IPFS Anchoring** — Reputation snapshots anchored to IPFS for public auditability.
|
|
58
|
+
|
|
59
|
+
## API Overview
|
|
60
|
+
|
|
61
|
+
### @avp_tracked Decorator
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from agentveil import avp_tracked
|
|
65
|
+
|
|
66
|
+
# Basic — auto-register + auto-attest on success/failure
|
|
67
|
+
@avp_tracked("https://agentveil.dev", name="my_agent", to_did="did:key:z6Mk...")
|
|
68
|
+
def do_work(task: str) -> str:
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
# With capabilities and custom weight
|
|
72
|
+
@avp_tracked("https://agentveil.dev", name="auditor", to_did="did:key:z6Mk...",
|
|
73
|
+
capabilities=["security_audit"], weight=0.9)
|
|
74
|
+
async def audit(code: str) -> str:
|
|
75
|
+
return await run_audit(code)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Parameters:
|
|
79
|
+
- `base_url` — AVP server URL
|
|
80
|
+
- `name` — Agent name (used for key storage)
|
|
81
|
+
- `to_did` — DID of agent to rate (skip to disable attestation)
|
|
82
|
+
- `capabilities` — Agent capabilities for card (defaults to function name)
|
|
83
|
+
- `weight` — Attestation weight 0.0-1.0 (default 0.8)
|
|
84
|
+
|
|
85
|
+
### Registration (manual)
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
agent = AVPAgent.create(base_url, name="my_agent")
|
|
89
|
+
agent.register(display_name="My Agent")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Keys are saved to `~/.avp/agents/{name}.json` (chmod 0600). Load later with:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
agent = AVPAgent.load(base_url, name="my_agent")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Agent Cards (Discovery)
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
agent.publish_card(capabilities=["code_review"], provider="anthropic")
|
|
102
|
+
results = agent.search_agents(capability="code_review", min_reputation=0.5)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Attestations
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
agent.attest(
|
|
109
|
+
to_did="did:key:z6Mk...",
|
|
110
|
+
outcome="positive", # positive / negative / neutral
|
|
111
|
+
weight=0.9, # 0.0 - 1.0
|
|
112
|
+
context="task_completion",
|
|
113
|
+
evidence_hash="sha256_of_interaction_log",
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Reputation
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
rep = agent.get_reputation("did:key:z6Mk...")
|
|
121
|
+
# {"score": 0.85, "confidence": 0.72, "interpretation": "good"}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Authentication
|
|
125
|
+
|
|
126
|
+
All write operations are signed with Ed25519:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Authorization: AVP-Sig did="did:key:z6Mk...",ts="1710864000",nonce="random",sig="hex..."
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Signature covers: `{method}:{path}:{timestamp}:{nonce}:{body_sha256}`
|
|
133
|
+
|
|
134
|
+
The SDK handles signing automatically.
|
|
135
|
+
|
|
136
|
+
## Error Handling
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from agentveil import AVPAgent, AVPAuthError, AVPRateLimitError, AVPNotFoundError
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
agent.attest(did, outcome="positive")
|
|
143
|
+
except AVPAuthError:
|
|
144
|
+
print("Signature invalid or agent not verified")
|
|
145
|
+
except AVPRateLimitError as e:
|
|
146
|
+
print(f"Rate limited, retry after {e.retry_after}s")
|
|
147
|
+
except AVPNotFoundError:
|
|
148
|
+
print("Agent not found")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Examples
|
|
152
|
+
|
|
153
|
+
- [`examples/quickstart.py`](examples/quickstart.py) — Register, publish card, check reputation
|
|
154
|
+
- [`examples/two_agents.py`](examples/two_agents.py) — Full A2A interaction with attestations
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AVP SDK — Python client for Agent Veil Protocol.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from agentveil import AVPAgent
|
|
6
|
+
|
|
7
|
+
agent = AVPAgent.create("https://avp.example.com", name="MyAgent")
|
|
8
|
+
agent.register()
|
|
9
|
+
agent.publish_card(capabilities=["code_review", "testing"], provider="anthropic")
|
|
10
|
+
|
|
11
|
+
rep = agent.get_reputation(other_agent_did)
|
|
12
|
+
agent.attest(other_agent_did, outcome="positive", weight=0.9)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from agentveil.agent import AVPAgent
|
|
16
|
+
from agentveil.tracked import avp_tracked, clear_agent_cache
|
|
17
|
+
from agentveil.exceptions import (
|
|
18
|
+
AVPError,
|
|
19
|
+
AVPAuthError,
|
|
20
|
+
AVPNotFoundError,
|
|
21
|
+
AVPRateLimitError,
|
|
22
|
+
AVPInsufficientFundsError,
|
|
23
|
+
AVPValidationError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__version__ = "0.2.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"AVPAgent",
|
|
30
|
+
"avp_tracked",
|
|
31
|
+
"clear_agent_cache",
|
|
32
|
+
"AVPError",
|
|
33
|
+
"AVPAuthError",
|
|
34
|
+
"AVPNotFoundError",
|
|
35
|
+
"AVPRateLimitError",
|
|
36
|
+
"AVPInsufficientFundsError",
|
|
37
|
+
"AVPValidationError",
|
|
38
|
+
]
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AVPAgent — main SDK class for interacting with Agent Veil Protocol.
|
|
3
|
+
|
|
4
|
+
Handles key management, authentication, registration, attestations,
|
|
5
|
+
reputation queries, payments, and escrow.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
agent = AVPAgent.create("https://avp.example.com", name="MyAgent")
|
|
9
|
+
agent.register()
|
|
10
|
+
|
|
11
|
+
agent.attest("did:key:z6Mk...", outcome="positive", weight=0.8)
|
|
12
|
+
rep = agent.get_reputation("did:key:z6Mk...")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
from nacl.signing import SigningKey, VerifyKey
|
|
23
|
+
|
|
24
|
+
from agentveil.auth import build_auth_header
|
|
25
|
+
from agentveil.exceptions import (
|
|
26
|
+
AVPError,
|
|
27
|
+
AVPAuthError,
|
|
28
|
+
AVPNotFoundError,
|
|
29
|
+
AVPRateLimitError,
|
|
30
|
+
AVPInsufficientFundsError,
|
|
31
|
+
AVPValidationError,
|
|
32
|
+
AVPServerError,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
log = logging.getLogger("agentveil")
|
|
36
|
+
|
|
37
|
+
# Default storage for agent keys
|
|
38
|
+
AGENTS_DIR = os.path.expanduser("~/.avp/agents")
|
|
39
|
+
|
|
40
|
+
# Multicodec prefix for Ed25519 public key
|
|
41
|
+
ED25519_MULTICODEC = bytes([0xED, 0x01])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _public_key_to_did(public_key: bytes) -> str:
|
|
45
|
+
"""Convert Ed25519 public key to did:key."""
|
|
46
|
+
import base58
|
|
47
|
+
multicodec_key = ED25519_MULTICODEC + public_key
|
|
48
|
+
encoded = base58.b58encode(multicodec_key).decode("ascii")
|
|
49
|
+
return f"did:key:z{encoded}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AVPAgent:
|
|
53
|
+
"""
|
|
54
|
+
AI agent identity on Agent Veil Protocol.
|
|
55
|
+
|
|
56
|
+
Manages Ed25519 keys, signs requests, and provides methods
|
|
57
|
+
for all AVP operations: registration, attestation, reputation,
|
|
58
|
+
payments, and escrow.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
base_url: str,
|
|
64
|
+
private_key: bytes,
|
|
65
|
+
name: str = "agent",
|
|
66
|
+
timeout: float = 15.0,
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Initialize agent with existing private key.
|
|
70
|
+
Use AVPAgent.create() or AVPAgent.load() instead of calling directly.
|
|
71
|
+
"""
|
|
72
|
+
self._base_url = base_url.rstrip("/")
|
|
73
|
+
self._private_key = private_key
|
|
74
|
+
self._name = name
|
|
75
|
+
self._timeout = timeout
|
|
76
|
+
|
|
77
|
+
# Derive public key and DID
|
|
78
|
+
signing_key = SigningKey(private_key)
|
|
79
|
+
self._public_key = bytes(signing_key.verify_key)
|
|
80
|
+
self._did = _public_key_to_did(self._public_key)
|
|
81
|
+
self._is_registered = False
|
|
82
|
+
self._is_verified = False
|
|
83
|
+
|
|
84
|
+
# === Factory methods ===
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def create(cls, base_url: str, name: str = "agent", save: bool = True) -> "AVPAgent":
|
|
88
|
+
"""
|
|
89
|
+
Create a new agent with fresh Ed25519 keys.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
base_url: AVP server URL (e.g. "https://avp.example.com")
|
|
93
|
+
name: Agent name (used for local key storage)
|
|
94
|
+
save: Save keys to ~/.avp/agents/{name}.json
|
|
95
|
+
"""
|
|
96
|
+
signing_key = SigningKey.generate()
|
|
97
|
+
private_key = bytes(signing_key)
|
|
98
|
+
agent = cls(base_url, private_key, name=name)
|
|
99
|
+
if save:
|
|
100
|
+
agent.save()
|
|
101
|
+
log.info(f"Created new agent: {agent.did[:40]}...")
|
|
102
|
+
return agent
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def load(cls, base_url: str, name: str = "agent") -> "AVPAgent":
|
|
106
|
+
"""
|
|
107
|
+
Load agent from saved keys.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
base_url: AVP server URL
|
|
111
|
+
name: Agent name (matches saved file)
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
FileNotFoundError: If no saved agent with this name
|
|
115
|
+
"""
|
|
116
|
+
path = os.path.join(AGENTS_DIR, f"{name}.json")
|
|
117
|
+
if not os.path.exists(path):
|
|
118
|
+
raise FileNotFoundError(f"No saved agent '{name}' at {path}")
|
|
119
|
+
|
|
120
|
+
with open(path) as f:
|
|
121
|
+
data = json.load(f)
|
|
122
|
+
|
|
123
|
+
private_key = bytes.fromhex(data["private_key_hex"])
|
|
124
|
+
agent = cls(base_url, private_key, name=name)
|
|
125
|
+
agent._is_registered = data.get("registered", False)
|
|
126
|
+
agent._is_verified = data.get("verified", False)
|
|
127
|
+
log.info(f"Loaded agent: {agent.did[:40]}...")
|
|
128
|
+
return agent
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_private_key(cls, base_url: str, private_key_hex: str, name: str = "agent") -> "AVPAgent":
|
|
132
|
+
"""Create agent from hex-encoded private key."""
|
|
133
|
+
return cls(base_url, bytes.fromhex(private_key_hex), name=name)
|
|
134
|
+
|
|
135
|
+
# === Properties ===
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def did(self) -> str:
|
|
139
|
+
"""Agent's DID (did:key:z6Mk...)."""
|
|
140
|
+
return self._did
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def public_key_hex(self) -> str:
|
|
144
|
+
"""Agent's public key as hex string."""
|
|
145
|
+
return self._public_key.hex()
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def is_registered(self) -> bool:
|
|
149
|
+
return self._is_registered
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def is_verified(self) -> bool:
|
|
153
|
+
return self._is_verified
|
|
154
|
+
|
|
155
|
+
# === Key management ===
|
|
156
|
+
|
|
157
|
+
def save(self) -> str:
|
|
158
|
+
"""Save agent keys to disk. Returns file path."""
|
|
159
|
+
os.makedirs(AGENTS_DIR, exist_ok=True)
|
|
160
|
+
path = os.path.join(AGENTS_DIR, f"{self._name}.json")
|
|
161
|
+
with open(path, "w") as f:
|
|
162
|
+
json.dump({
|
|
163
|
+
"name": self._name,
|
|
164
|
+
"did": self._did,
|
|
165
|
+
"public_key_hex": self._public_key.hex(),
|
|
166
|
+
"private_key_hex": self._private_key.hex(),
|
|
167
|
+
"registered": self._is_registered,
|
|
168
|
+
"verified": self._is_verified,
|
|
169
|
+
"base_url": self._base_url,
|
|
170
|
+
}, f, indent=2)
|
|
171
|
+
os.chmod(path, 0o600) # Owner read/write only
|
|
172
|
+
return path
|
|
173
|
+
|
|
174
|
+
# === HTTP helpers ===
|
|
175
|
+
|
|
176
|
+
def _auth_headers(self, method: str, path: str, body: bytes = b"") -> dict:
|
|
177
|
+
"""Build authenticated headers for a request."""
|
|
178
|
+
return build_auth_header(self._private_key, self._did, method, path, body)
|
|
179
|
+
|
|
180
|
+
def _handle_response(self, response: httpx.Response) -> dict:
|
|
181
|
+
"""Parse response and raise appropriate exceptions."""
|
|
182
|
+
if response.status_code == 200:
|
|
183
|
+
return response.json()
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
detail = response.json().get("detail", response.text)
|
|
187
|
+
except Exception:
|
|
188
|
+
detail = response.text
|
|
189
|
+
|
|
190
|
+
if response.status_code == 401:
|
|
191
|
+
raise AVPAuthError(f"Authentication failed: {detail}", 401, detail)
|
|
192
|
+
elif response.status_code == 403:
|
|
193
|
+
raise AVPAuthError(f"Forbidden: {detail}", 403, detail)
|
|
194
|
+
elif response.status_code == 404:
|
|
195
|
+
raise AVPNotFoundError(f"Not found: {detail}", 404, detail)
|
|
196
|
+
elif response.status_code == 409:
|
|
197
|
+
raise AVPValidationError(f"Conflict: {detail}", 409, detail)
|
|
198
|
+
elif response.status_code == 429:
|
|
199
|
+
raise AVPRateLimitError(f"Rate limited: {detail}")
|
|
200
|
+
elif response.status_code == 400:
|
|
201
|
+
if "Insufficient" in str(detail):
|
|
202
|
+
raise AVPInsufficientFundsError(f"Insufficient funds: {detail}", 400, detail)
|
|
203
|
+
raise AVPValidationError(f"Validation error: {detail}", 400, detail)
|
|
204
|
+
elif response.status_code >= 500:
|
|
205
|
+
raise AVPServerError(f"Server error: {detail}", response.status_code, detail)
|
|
206
|
+
else:
|
|
207
|
+
raise AVPError(f"Unexpected error ({response.status_code}): {detail}", response.status_code, detail)
|
|
208
|
+
|
|
209
|
+
# === Registration ===
|
|
210
|
+
|
|
211
|
+
def register(self, display_name: Optional[str] = None) -> dict:
|
|
212
|
+
"""
|
|
213
|
+
Register agent on AVP and verify key ownership.
|
|
214
|
+
Does both steps (register + verify) in one call.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
display_name: Optional human-readable name
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
dict with 'did' and 'agnet_address'
|
|
221
|
+
"""
|
|
222
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
223
|
+
# Step 1: Register
|
|
224
|
+
r = c.post("/v1/agents/register", json={
|
|
225
|
+
"public_key_hex": self.public_key_hex,
|
|
226
|
+
"display_name": display_name or self._name,
|
|
227
|
+
})
|
|
228
|
+
data = self._handle_response(r)
|
|
229
|
+
challenge = data["challenge"]
|
|
230
|
+
|
|
231
|
+
# Step 2: Sign challenge and verify
|
|
232
|
+
signing_key = SigningKey(self._private_key)
|
|
233
|
+
signed = signing_key.sign(challenge.encode())
|
|
234
|
+
sig_hex = signed.signature.hex()
|
|
235
|
+
|
|
236
|
+
r = c.post("/v1/agents/verify", json={
|
|
237
|
+
"did": self._did,
|
|
238
|
+
"challenge": challenge,
|
|
239
|
+
"signature_hex": sig_hex,
|
|
240
|
+
})
|
|
241
|
+
self._handle_response(r)
|
|
242
|
+
|
|
243
|
+
self._is_registered = True
|
|
244
|
+
self._is_verified = True
|
|
245
|
+
self.save()
|
|
246
|
+
log.info(f"Registered and verified: {self._did[:40]}...")
|
|
247
|
+
return data
|
|
248
|
+
|
|
249
|
+
# === Agent Cards ===
|
|
250
|
+
|
|
251
|
+
def publish_card(
|
|
252
|
+
self,
|
|
253
|
+
capabilities: list[str],
|
|
254
|
+
provider: Optional[str] = None,
|
|
255
|
+
endpoint_url: Optional[str] = None,
|
|
256
|
+
) -> dict:
|
|
257
|
+
"""
|
|
258
|
+
Publish or update agent's capability card.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
capabilities: List of capabilities (e.g. ["code_review", "testing"])
|
|
262
|
+
provider: LLM provider (e.g. "anthropic", "openai")
|
|
263
|
+
endpoint_url: URL where this agent can be reached
|
|
264
|
+
"""
|
|
265
|
+
body_data = {"capabilities": capabilities}
|
|
266
|
+
if provider:
|
|
267
|
+
body_data["provider"] = provider
|
|
268
|
+
if endpoint_url:
|
|
269
|
+
body_data["endpoint_url"] = endpoint_url
|
|
270
|
+
|
|
271
|
+
body = json.dumps(body_data).encode()
|
|
272
|
+
headers = self._auth_headers("POST", "/v1/cards", body)
|
|
273
|
+
|
|
274
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
275
|
+
r = c.post("/v1/cards", content=body, headers=headers)
|
|
276
|
+
return self._handle_response(r)
|
|
277
|
+
|
|
278
|
+
def search_agents(
|
|
279
|
+
self,
|
|
280
|
+
capability: Optional[str] = None,
|
|
281
|
+
provider: Optional[str] = None,
|
|
282
|
+
min_reputation: Optional[float] = None,
|
|
283
|
+
limit: int = 20,
|
|
284
|
+
) -> list[dict]:
|
|
285
|
+
"""Search for agents by capability, provider, or minimum reputation."""
|
|
286
|
+
params = {"limit": limit}
|
|
287
|
+
if capability:
|
|
288
|
+
params["capability"] = capability
|
|
289
|
+
if provider:
|
|
290
|
+
params["provider"] = provider
|
|
291
|
+
if min_reputation is not None:
|
|
292
|
+
params["min_reputation"] = min_reputation
|
|
293
|
+
|
|
294
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
295
|
+
r = c.get("/v1/cards", params=params)
|
|
296
|
+
return self._handle_response(r)
|
|
297
|
+
|
|
298
|
+
# === Attestations ===
|
|
299
|
+
|
|
300
|
+
def attest(
|
|
301
|
+
self,
|
|
302
|
+
to_did: str,
|
|
303
|
+
outcome: str = "positive",
|
|
304
|
+
weight: float = 1.0,
|
|
305
|
+
context: Optional[str] = None,
|
|
306
|
+
evidence_hash: Optional[str] = None,
|
|
307
|
+
) -> dict:
|
|
308
|
+
"""
|
|
309
|
+
Submit an attestation about another agent.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
to_did: DID of agent being rated
|
|
313
|
+
outcome: "positive", "negative", or "neutral"
|
|
314
|
+
weight: Confidence weight (0.0 to 1.0)
|
|
315
|
+
context: Interaction type (e.g. "task_completion")
|
|
316
|
+
evidence_hash: SHA256 of interaction log
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Attestation details
|
|
320
|
+
"""
|
|
321
|
+
if outcome not in ("positive", "negative", "neutral"):
|
|
322
|
+
raise AVPValidationError(f"Invalid outcome: {outcome}. Must be positive/negative/neutral")
|
|
323
|
+
if not 0.0 <= weight <= 1.0:
|
|
324
|
+
raise AVPValidationError(f"Weight must be 0.0-1.0, got {weight}")
|
|
325
|
+
|
|
326
|
+
# Build and sign attestation payload
|
|
327
|
+
attest_payload = json.dumps({
|
|
328
|
+
"to": to_did,
|
|
329
|
+
"outcome": outcome,
|
|
330
|
+
"weight": weight,
|
|
331
|
+
"context": context or "",
|
|
332
|
+
"evidence_hash": evidence_hash or "",
|
|
333
|
+
}, sort_keys=True, separators=(",", ":")).encode()
|
|
334
|
+
|
|
335
|
+
signing_key = SigningKey(self._private_key)
|
|
336
|
+
signed = signing_key.sign(attest_payload)
|
|
337
|
+
sig_hex = signed.signature.hex()
|
|
338
|
+
|
|
339
|
+
body_data = {
|
|
340
|
+
"to_agent_did": to_did,
|
|
341
|
+
"outcome": outcome,
|
|
342
|
+
"weight": weight,
|
|
343
|
+
"signature": sig_hex,
|
|
344
|
+
}
|
|
345
|
+
if context:
|
|
346
|
+
body_data["context"] = context
|
|
347
|
+
if evidence_hash:
|
|
348
|
+
body_data["evidence_hash"] = evidence_hash
|
|
349
|
+
|
|
350
|
+
body = json.dumps(body_data).encode()
|
|
351
|
+
headers = self._auth_headers("POST", "/v1/attestations", body)
|
|
352
|
+
|
|
353
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
354
|
+
r = c.post("/v1/attestations", content=body, headers=headers)
|
|
355
|
+
return self._handle_response(r)
|
|
356
|
+
|
|
357
|
+
# === Reputation ===
|
|
358
|
+
|
|
359
|
+
def get_reputation(self, did: Optional[str] = None) -> dict:
|
|
360
|
+
"""
|
|
361
|
+
Get reputation score for an agent.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
did: Agent DID (defaults to self)
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
dict with score, confidence, interpretation
|
|
368
|
+
"""
|
|
369
|
+
target = did or self._did
|
|
370
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
371
|
+
r = c.get(f"/v1/reputation/{target}")
|
|
372
|
+
return self._handle_response(r)
|
|
373
|
+
|
|
374
|
+
def get_agent_info(self, did: Optional[str] = None) -> dict:
|
|
375
|
+
"""Get public info about an agent."""
|
|
376
|
+
target = did or self._did
|
|
377
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
378
|
+
r = c.get(f"/v1/agents/{target}")
|
|
379
|
+
return self._handle_response(r)
|
|
380
|
+
|
|
381
|
+
# === Payments ===
|
|
382
|
+
|
|
383
|
+
def get_balance(self, did: Optional[str] = None) -> dict:
|
|
384
|
+
"""Get agent's balance (internal + Agnet)."""
|
|
385
|
+
target = did or self._did
|
|
386
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
387
|
+
r = c.get(f"/v1/payments/balance/{target}")
|
|
388
|
+
return self._handle_response(r)
|
|
389
|
+
|
|
390
|
+
def transfer(self, to_did: str, amount_nagn: int, memo: Optional[str] = None) -> dict:
|
|
391
|
+
"""
|
|
392
|
+
Transfer funds to another agent.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
to_did: Recipient DID
|
|
396
|
+
amount_nagn: Amount in nAGN (1 AGN = 1,000,000 nAGN)
|
|
397
|
+
memo: Optional memo
|
|
398
|
+
"""
|
|
399
|
+
body_data = {"to_agent_did": to_did, "amount_nagn": amount_nagn}
|
|
400
|
+
if memo:
|
|
401
|
+
body_data["memo"] = memo
|
|
402
|
+
|
|
403
|
+
body = json.dumps(body_data).encode()
|
|
404
|
+
headers = self._auth_headers("POST", "/v1/payments/transfer", body)
|
|
405
|
+
|
|
406
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
407
|
+
r = c.post("/v1/payments/transfer", content=body, headers=headers)
|
|
408
|
+
return self._handle_response(r)
|
|
409
|
+
|
|
410
|
+
# === Escrow ===
|
|
411
|
+
|
|
412
|
+
def create_escrow(
|
|
413
|
+
self,
|
|
414
|
+
payee_did: str,
|
|
415
|
+
amount_nagn: int,
|
|
416
|
+
ttl_hours: int = 24,
|
|
417
|
+
memo: Optional[str] = None,
|
|
418
|
+
) -> dict:
|
|
419
|
+
"""
|
|
420
|
+
Create an escrow — hold funds until interaction completes.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
payee_did: Agent who will receive funds on success
|
|
424
|
+
amount_nagn: Amount to hold
|
|
425
|
+
ttl_hours: Hours before auto-refund (1-168)
|
|
426
|
+
memo: Optional memo
|
|
427
|
+
"""
|
|
428
|
+
body_data = {
|
|
429
|
+
"payee_did": payee_did,
|
|
430
|
+
"amount_nagn": amount_nagn,
|
|
431
|
+
"ttl_hours": ttl_hours,
|
|
432
|
+
}
|
|
433
|
+
if memo:
|
|
434
|
+
body_data["memo"] = memo
|
|
435
|
+
|
|
436
|
+
body = json.dumps(body_data).encode()
|
|
437
|
+
headers = self._auth_headers("POST", "/v1/payments/escrow", body)
|
|
438
|
+
|
|
439
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
440
|
+
r = c.post("/v1/payments/escrow", content=body, headers=headers)
|
|
441
|
+
return self._handle_response(r)
|
|
442
|
+
|
|
443
|
+
def release_escrow(self, escrow_id: str) -> dict:
|
|
444
|
+
"""Release escrow — send funds to payee. Only payee can call this."""
|
|
445
|
+
body_data = {"escrow_id": escrow_id, "action": "release"}
|
|
446
|
+
body = json.dumps(body_data).encode()
|
|
447
|
+
headers = self._auth_headers("POST", "/v1/payments/escrow/resolve", body)
|
|
448
|
+
|
|
449
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
450
|
+
r = c.post("/v1/payments/escrow/resolve", content=body, headers=headers)
|
|
451
|
+
return self._handle_response(r)
|
|
452
|
+
|
|
453
|
+
def refund_escrow(self, escrow_id: str) -> dict:
|
|
454
|
+
"""Refund escrow — return funds to payer. Only payer can call this."""
|
|
455
|
+
body_data = {"escrow_id": escrow_id, "action": "refund"}
|
|
456
|
+
body = json.dumps(body_data).encode()
|
|
457
|
+
headers = self._auth_headers("POST", "/v1/payments/escrow/resolve", body)
|
|
458
|
+
|
|
459
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
460
|
+
r = c.post("/v1/payments/escrow/resolve", content=body, headers=headers)
|
|
461
|
+
return self._handle_response(r)
|
|
462
|
+
|
|
463
|
+
# === Utility ===
|
|
464
|
+
|
|
465
|
+
def health(self) -> dict:
|
|
466
|
+
"""Check AVP server health."""
|
|
467
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as c:
|
|
468
|
+
r = c.get("/v1/health")
|
|
469
|
+
return self._handle_response(r)
|
|
470
|
+
|
|
471
|
+
def __repr__(self) -> str:
|
|
472
|
+
status = "verified" if self._is_verified else ("registered" if self._is_registered else "new")
|
|
473
|
+
return f"AVPAgent(name={self._name}, did={self._did[:35]}..., status={status})"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AVP authentication helpers.
|
|
3
|
+
Builds signed Authorization headers for API requests.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import secrets
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from nacl.signing import SigningKey
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_auth_header(
|
|
14
|
+
private_key: bytes,
|
|
15
|
+
did: str,
|
|
16
|
+
method: str,
|
|
17
|
+
path: str,
|
|
18
|
+
body: bytes = b"",
|
|
19
|
+
) -> dict[str, str]:
|
|
20
|
+
"""
|
|
21
|
+
Build AVP-Sig Authorization header.
|
|
22
|
+
|
|
23
|
+
Signs: {method}:{path}:{timestamp}:{nonce}:{body_sha256}
|
|
24
|
+
Returns dict with Authorization header ready for httpx.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
private_key: Ed25519 private key (32 bytes)
|
|
28
|
+
did: Agent's DID (did:key:z6Mk...)
|
|
29
|
+
method: HTTP method (GET, POST, etc.)
|
|
30
|
+
path: URL path (/v1/attestations)
|
|
31
|
+
body: Request body bytes
|
|
32
|
+
"""
|
|
33
|
+
ts = int(time.time())
|
|
34
|
+
nonce = secrets.token_hex(16)
|
|
35
|
+
body_hash = hashlib.sha256(body).hexdigest()
|
|
36
|
+
|
|
37
|
+
message = f"{method}:{path}:{ts}:{nonce}:{body_hash}"
|
|
38
|
+
signing_key = SigningKey(private_key)
|
|
39
|
+
signed = signing_key.sign(message.encode())
|
|
40
|
+
sig_hex = signed.signature.hex()
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"Authorization": f'AVP-Sig did="{did}",ts="{ts}",nonce="{nonce}",sig="{sig_hex}"',
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""AVP SDK exceptions — clear, actionable error messages."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AVPError(Exception):
|
|
5
|
+
"""Base exception for all AVP SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int = 0, detail: str = ""):
|
|
8
|
+
self.message = message
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
self.detail = detail
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AVPAuthError(AVPError):
|
|
15
|
+
"""Authentication failed — invalid signature, expired timestamp, or nonce reuse."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AVPNotFoundError(AVPError):
|
|
20
|
+
"""Resource not found — agent, card, escrow, etc."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AVPRateLimitError(AVPError):
|
|
25
|
+
"""Rate limit exceeded — wait and retry."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str = "Rate limit exceeded", retry_after: int = 60):
|
|
28
|
+
self.retry_after = retry_after
|
|
29
|
+
super().__init__(message, status_code=429)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AVPInsufficientFundsError(AVPError):
|
|
33
|
+
"""Not enough balance for the operation."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AVPValidationError(AVPError):
|
|
38
|
+
"""Invalid input data."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AVPServerError(AVPError):
|
|
43
|
+
"""Server-side error — retry later."""
|
|
44
|
+
pass
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal @avp_tracked decorator — one line = full AVP integration.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from agentveil import avp_tracked
|
|
6
|
+
|
|
7
|
+
@avp_tracked("https://agentveil.dev", name="my_agent", capabilities=["code_review"])
|
|
8
|
+
def review_code(pr_url: str) -> str:
|
|
9
|
+
# Your logic here
|
|
10
|
+
return analysis
|
|
11
|
+
|
|
12
|
+
# That's it. The decorator handles:
|
|
13
|
+
# - Auto-registration on first call
|
|
14
|
+
# - Positive attestation on success
|
|
15
|
+
# - Negative attestation with evidence on failure
|
|
16
|
+
# - Agent card publishing with capabilities
|
|
17
|
+
|
|
18
|
+
Works with sync and async functions, any framework.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import functools
|
|
23
|
+
import hashlib
|
|
24
|
+
import inspect
|
|
25
|
+
import logging
|
|
26
|
+
import traceback
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
from agentveil.agent import AVPAgent
|
|
30
|
+
from agentveil.exceptions import AVPError
|
|
31
|
+
|
|
32
|
+
log = logging.getLogger("agentveil.tracked")
|
|
33
|
+
|
|
34
|
+
# Cache of initialized agents (by name) to avoid re-registration
|
|
35
|
+
_agent_cache: dict[str, AVPAgent] = {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_or_create_agent(
|
|
39
|
+
base_url: str,
|
|
40
|
+
name: str,
|
|
41
|
+
capabilities: list[str],
|
|
42
|
+
provider: Optional[str],
|
|
43
|
+
) -> AVPAgent:
|
|
44
|
+
"""Get cached agent or create+register a new one."""
|
|
45
|
+
if name in _agent_cache:
|
|
46
|
+
return _agent_cache[name]
|
|
47
|
+
|
|
48
|
+
# Try loading existing agent
|
|
49
|
+
try:
|
|
50
|
+
agent = AVPAgent.load(base_url, name=name)
|
|
51
|
+
if agent.is_verified:
|
|
52
|
+
_agent_cache[name] = agent
|
|
53
|
+
log.info(f"Loaded existing agent: {name}")
|
|
54
|
+
return agent
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
# Create and register new agent
|
|
59
|
+
agent = AVPAgent.create(base_url, name=name, save=True)
|
|
60
|
+
try:
|
|
61
|
+
agent.register(display_name=name)
|
|
62
|
+
log.info(f"Auto-registered agent: {name} ({agent.did[:40]}...)")
|
|
63
|
+
except AVPError as e:
|
|
64
|
+
# Already registered (409) — load state and continue
|
|
65
|
+
if e.status_code == 409:
|
|
66
|
+
log.info(f"Agent already registered: {name}")
|
|
67
|
+
agent._is_registered = True
|
|
68
|
+
agent._is_verified = True
|
|
69
|
+
agent.save()
|
|
70
|
+
else:
|
|
71
|
+
log.warning(f"Registration failed: {e}")
|
|
72
|
+
raise
|
|
73
|
+
|
|
74
|
+
# Publish capabilities card
|
|
75
|
+
if capabilities:
|
|
76
|
+
try:
|
|
77
|
+
agent.publish_card(capabilities=capabilities, provider=provider)
|
|
78
|
+
log.info(f"Published card: {capabilities}")
|
|
79
|
+
except AVPError as e:
|
|
80
|
+
log.warning(f"Card publish failed (non-fatal): {e}")
|
|
81
|
+
|
|
82
|
+
_agent_cache[name] = agent
|
|
83
|
+
return agent
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _make_evidence_hash(exc: Exception) -> str:
|
|
87
|
+
"""Create SHA256 hash of exception traceback for evidence."""
|
|
88
|
+
tb = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
89
|
+
tb_text = "".join(tb)
|
|
90
|
+
return hashlib.sha256(tb_text.encode()).hexdigest()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _derive_context(func_name: str) -> str:
|
|
94
|
+
"""Derive attestation context from function name."""
|
|
95
|
+
# Sanitize: only alphanumeric, underscore, hyphen, dot (AVP context rules)
|
|
96
|
+
clean = "".join(c if c.isalnum() or c in ("_", "-", ".") else "_" for c in func_name)
|
|
97
|
+
return clean[:100]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def avp_tracked(
|
|
101
|
+
base_url: str,
|
|
102
|
+
*,
|
|
103
|
+
name: str = "agent",
|
|
104
|
+
to_did: Optional[str] = None,
|
|
105
|
+
capabilities: Optional[list[str]] = None,
|
|
106
|
+
provider: Optional[str] = None,
|
|
107
|
+
weight: float = 0.8,
|
|
108
|
+
attest_self: bool = False,
|
|
109
|
+
):
|
|
110
|
+
"""
|
|
111
|
+
Decorator that integrates any function with Agent Veil Protocol.
|
|
112
|
+
|
|
113
|
+
On first call: auto-registers agent (if not registered).
|
|
114
|
+
On success: submits positive attestation.
|
|
115
|
+
On exception: submits negative attestation with stack trace hash as evidence.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
base_url: AVP server URL (e.g. "https://agentveil.dev")
|
|
119
|
+
name: Agent name (used for key storage and display)
|
|
120
|
+
to_did: DID of agent to attest (required unless attest_self=True)
|
|
121
|
+
capabilities: Agent capabilities for card (defaults to [function_name])
|
|
122
|
+
provider: LLM provider for card (e.g. "anthropic")
|
|
123
|
+
weight: Attestation weight 0.0-1.0 (default 0.8)
|
|
124
|
+
attest_self: If True and to_did is None, skip attestation (no self-attest)
|
|
125
|
+
|
|
126
|
+
Usage:
|
|
127
|
+
@avp_tracked("https://agentveil.dev", name="reviewer", to_did="did:key:z6Mk...")
|
|
128
|
+
def review_code(code: str) -> str:
|
|
129
|
+
return "LGTM"
|
|
130
|
+
|
|
131
|
+
@avp_tracked("https://agentveil.dev", name="reviewer", capabilities=["code_review"])
|
|
132
|
+
async def review_code(code: str) -> str:
|
|
133
|
+
return "LGTM"
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def decorator(func):
|
|
137
|
+
func_name = func.__name__
|
|
138
|
+
caps = capabilities if capabilities is not None else [func_name]
|
|
139
|
+
context = _derive_context(func_name)
|
|
140
|
+
|
|
141
|
+
if inspect.iscoroutinefunction(func):
|
|
142
|
+
@functools.wraps(func)
|
|
143
|
+
async def async_wrapper(*args, **kwargs):
|
|
144
|
+
agent = _get_or_create_agent(base_url, name, caps, provider)
|
|
145
|
+
target_did = to_did
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
result = await func(*args, **kwargs)
|
|
149
|
+
|
|
150
|
+
# Positive attestation on success
|
|
151
|
+
if target_did:
|
|
152
|
+
try:
|
|
153
|
+
agent.attest(
|
|
154
|
+
to_did=target_did,
|
|
155
|
+
outcome="positive",
|
|
156
|
+
weight=weight,
|
|
157
|
+
context=context,
|
|
158
|
+
)
|
|
159
|
+
except AVPError as e:
|
|
160
|
+
log.warning(f"Positive attestation failed (non-fatal): {e}")
|
|
161
|
+
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
# Negative attestation on failure
|
|
166
|
+
if target_did:
|
|
167
|
+
evidence = _make_evidence_hash(exc)
|
|
168
|
+
try:
|
|
169
|
+
agent.attest(
|
|
170
|
+
to_did=target_did,
|
|
171
|
+
outcome="negative",
|
|
172
|
+
weight=weight,
|
|
173
|
+
context=context,
|
|
174
|
+
evidence_hash=evidence,
|
|
175
|
+
)
|
|
176
|
+
except AVPError as e:
|
|
177
|
+
log.warning(f"Negative attestation failed (non-fatal): {e}")
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
return async_wrapper
|
|
181
|
+
else:
|
|
182
|
+
@functools.wraps(func)
|
|
183
|
+
def sync_wrapper(*args, **kwargs):
|
|
184
|
+
agent = _get_or_create_agent(base_url, name, caps, provider)
|
|
185
|
+
target_did = to_did
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
result = func(*args, **kwargs)
|
|
189
|
+
|
|
190
|
+
# Positive attestation on success
|
|
191
|
+
if target_did:
|
|
192
|
+
try:
|
|
193
|
+
agent.attest(
|
|
194
|
+
to_did=target_did,
|
|
195
|
+
outcome="positive",
|
|
196
|
+
weight=weight,
|
|
197
|
+
context=context,
|
|
198
|
+
)
|
|
199
|
+
except AVPError as e:
|
|
200
|
+
log.warning(f"Positive attestation failed (non-fatal): {e}")
|
|
201
|
+
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
# Negative attestation on failure
|
|
206
|
+
if target_did:
|
|
207
|
+
evidence = _make_evidence_hash(exc)
|
|
208
|
+
try:
|
|
209
|
+
agent.attest(
|
|
210
|
+
to_did=target_did,
|
|
211
|
+
outcome="negative",
|
|
212
|
+
weight=weight,
|
|
213
|
+
context=context,
|
|
214
|
+
evidence_hash=evidence,
|
|
215
|
+
)
|
|
216
|
+
except AVPError as e:
|
|
217
|
+
log.warning(f"Negative attestation failed (non-fatal): {e}")
|
|
218
|
+
raise
|
|
219
|
+
|
|
220
|
+
return sync_wrapper
|
|
221
|
+
|
|
222
|
+
return decorator
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def clear_agent_cache():
|
|
226
|
+
"""Clear the cached agents. Useful for testing."""
|
|
227
|
+
_agent_cache.clear()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentveil
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for Agent Veil Protocol — reputation and identity for AI agents
|
|
5
|
+
Author: Agent Veil Protocol
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://agentveil.dev
|
|
8
|
+
Project-URL: Documentation, https://agentveil.dev/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/creatorrmode-lead/avp
|
|
10
|
+
Keywords: ai,agents,reputation,did,identity,a2a
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: pynacl>=1.5.0
|
|
23
|
+
Requires-Dist: base58>=2.1.0
|
|
24
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
agentveil/__init__.py
|
|
5
|
+
agentveil/agent.py
|
|
6
|
+
agentveil/auth.py
|
|
7
|
+
agentveil/exceptions.py
|
|
8
|
+
agentveil/tracked.py
|
|
9
|
+
agentveil.egg-info/PKG-INFO
|
|
10
|
+
agentveil.egg-info/SOURCES.txt
|
|
11
|
+
agentveil.egg-info/dependency_links.txt
|
|
12
|
+
agentveil.egg-info/requires.txt
|
|
13
|
+
agentveil.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentveil
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agentveil"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Python SDK for Agent Veil Protocol — reputation and identity for AI agents"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
authors = [{name = "Agent Veil Protocol"}]
|
|
8
|
+
keywords = ["ai", "agents", "reputation", "did", "identity", "a2a"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Topic :: Software Development :: Libraries",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"httpx>=0.27.0",
|
|
22
|
+
"pynacl>=1.5.0",
|
|
23
|
+
"base58>=2.1.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://agentveil.dev"
|
|
28
|
+
Documentation = "https://agentveil.dev/docs"
|
|
29
|
+
Repository = "https://github.com/creatorrmode-lead/avp"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["setuptools>=68.0"]
|
|
33
|
+
build-backend = "setuptools.build_meta"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
include = ["agentveil*"]
|