agentmesh-platform 1.0.0a1__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.
- agentmesh/__init__.py +119 -0
- agentmesh/cli/__init__.py +10 -0
- agentmesh/cli/main.py +405 -0
- agentmesh/governance/__init__.py +26 -0
- agentmesh/governance/audit.py +381 -0
- agentmesh/governance/compliance.py +447 -0
- agentmesh/governance/policy.py +385 -0
- agentmesh/governance/shadow.py +266 -0
- agentmesh/identity/__init__.py +30 -0
- agentmesh/identity/agent_id.py +319 -0
- agentmesh/identity/credentials.py +323 -0
- agentmesh/identity/delegation.py +281 -0
- agentmesh/identity/risk.py +279 -0
- agentmesh/identity/spiffe.py +230 -0
- agentmesh/identity/sponsor.py +178 -0
- agentmesh/reward/__init__.py +19 -0
- agentmesh/reward/engine.py +454 -0
- agentmesh/reward/learning.py +287 -0
- agentmesh/reward/scoring.py +203 -0
- agentmesh/trust/__init__.py +19 -0
- agentmesh/trust/bridge.py +386 -0
- agentmesh/trust/capability.py +293 -0
- agentmesh/trust/handshake.py +334 -0
- agentmesh_platform-1.0.0a1.dist-info/METADATA +332 -0
- agentmesh_platform-1.0.0a1.dist-info/RECORD +28 -0
- agentmesh_platform-1.0.0a1.dist-info/WHEEL +4 -0
- agentmesh_platform-1.0.0a1.dist-info/entry_points.txt +2 -0
- agentmesh_platform-1.0.0a1.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPIFFE/SVID Integration
|
|
3
|
+
|
|
4
|
+
Workload identity using SPIFFE (Secure Production Identity Framework
|
|
5
|
+
for Everyone) and SVID (SPIFFE Verifiable Identity Documents).
|
|
6
|
+
|
|
7
|
+
SPIFFE/SVID provides:
|
|
8
|
+
- Mutual TLS for all agent transport
|
|
9
|
+
- Zero cleartext traffic between agents
|
|
10
|
+
- Standard workload identity
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from typing import Optional, Literal
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
import hashlib
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SVID(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
SPIFFE Verifiable Identity Document.
|
|
22
|
+
|
|
23
|
+
An SVID is the document that carries the SPIFFE ID and can be
|
|
24
|
+
validated by a third party. AgentMesh uses X.509-SVID format.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
spiffe_id: str = Field(..., description="SPIFFE ID (spiffe://trust-domain/path)")
|
|
28
|
+
svid_type: Literal["x509", "jwt"] = Field(default="x509")
|
|
29
|
+
|
|
30
|
+
# Certificate data (X.509-SVID)
|
|
31
|
+
certificate_chain: Optional[list[str]] = Field(None, description="PEM-encoded cert chain")
|
|
32
|
+
private_key_type: Optional[str] = Field(None, description="Key type (e.g., 'EC P-256')")
|
|
33
|
+
|
|
34
|
+
# JWT-SVID fields
|
|
35
|
+
jwt_token: Optional[str] = Field(None, description="JWT-SVID token")
|
|
36
|
+
|
|
37
|
+
# Metadata
|
|
38
|
+
trust_domain: str = Field(..., description="SPIFFE trust domain")
|
|
39
|
+
issued_at: datetime = Field(default_factory=datetime.utcnow)
|
|
40
|
+
expires_at: datetime = Field(...)
|
|
41
|
+
|
|
42
|
+
# Agent binding
|
|
43
|
+
agent_did: str = Field(..., description="AgentMesh DID this SVID belongs to")
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def parse_spiffe_id(cls, spiffe_id: str) -> tuple[str, str]:
|
|
47
|
+
"""
|
|
48
|
+
Parse a SPIFFE ID into trust domain and path.
|
|
49
|
+
|
|
50
|
+
Format: spiffe://trust-domain/path
|
|
51
|
+
"""
|
|
52
|
+
if not spiffe_id.startswith("spiffe://"):
|
|
53
|
+
raise ValueError(f"Invalid SPIFFE ID: {spiffe_id}")
|
|
54
|
+
|
|
55
|
+
parts = spiffe_id[9:].split("/", 1) # Remove "spiffe://"
|
|
56
|
+
trust_domain = parts[0]
|
|
57
|
+
path = "/" + parts[1] if len(parts) > 1 else "/"
|
|
58
|
+
|
|
59
|
+
return trust_domain, path
|
|
60
|
+
|
|
61
|
+
def is_valid(self) -> bool:
|
|
62
|
+
"""Check if SVID is currently valid."""
|
|
63
|
+
now = datetime.utcnow()
|
|
64
|
+
return self.issued_at <= now < self.expires_at
|
|
65
|
+
|
|
66
|
+
def time_remaining(self) -> timedelta:
|
|
67
|
+
"""Get time remaining until expiration."""
|
|
68
|
+
return max(timedelta(0), self.expires_at - datetime.utcnow())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SPIFFEIdentity(BaseModel):
|
|
72
|
+
"""
|
|
73
|
+
SPIFFE Identity for an agent.
|
|
74
|
+
|
|
75
|
+
Maps AgentMesh identity to SPIFFE workload identity,
|
|
76
|
+
enabling mTLS with other SPIFFE-aware workloads.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
# AgentMesh identity
|
|
80
|
+
agent_did: str = Field(..., description="AgentMesh DID")
|
|
81
|
+
agent_name: str = Field(...)
|
|
82
|
+
|
|
83
|
+
# SPIFFE identity
|
|
84
|
+
spiffe_id: str = Field(..., description="Full SPIFFE ID")
|
|
85
|
+
trust_domain: str = Field(...)
|
|
86
|
+
workload_path: str = Field(...)
|
|
87
|
+
|
|
88
|
+
# Current SVID
|
|
89
|
+
current_svid: Optional[SVID] = Field(None)
|
|
90
|
+
|
|
91
|
+
# Metadata
|
|
92
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def create(
|
|
96
|
+
cls,
|
|
97
|
+
agent_did: str,
|
|
98
|
+
agent_name: str,
|
|
99
|
+
trust_domain: str = "agentmesh.local",
|
|
100
|
+
organization: Optional[str] = None,
|
|
101
|
+
) -> "SPIFFEIdentity":
|
|
102
|
+
"""
|
|
103
|
+
Create SPIFFE identity for an agent.
|
|
104
|
+
|
|
105
|
+
SPIFFE ID format: spiffe://trust-domain/agentmesh/org/agent-name
|
|
106
|
+
"""
|
|
107
|
+
# Build workload path
|
|
108
|
+
org_part = f"/{organization}" if organization else ""
|
|
109
|
+
workload_path = f"/agentmesh{org_part}/{agent_name}"
|
|
110
|
+
|
|
111
|
+
spiffe_id = f"spiffe://{trust_domain}{workload_path}"
|
|
112
|
+
|
|
113
|
+
return cls(
|
|
114
|
+
agent_did=agent_did,
|
|
115
|
+
agent_name=agent_name,
|
|
116
|
+
spiffe_id=spiffe_id,
|
|
117
|
+
trust_domain=trust_domain,
|
|
118
|
+
workload_path=workload_path,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def issue_svid(
|
|
122
|
+
self,
|
|
123
|
+
ttl_hours: int = 1,
|
|
124
|
+
svid_type: Literal["x509", "jwt"] = "x509",
|
|
125
|
+
) -> SVID:
|
|
126
|
+
"""
|
|
127
|
+
Issue a new SVID for this identity.
|
|
128
|
+
|
|
129
|
+
In production, this would request an SVID from the SPIRE server.
|
|
130
|
+
"""
|
|
131
|
+
now = datetime.utcnow()
|
|
132
|
+
|
|
133
|
+
svid = SVID(
|
|
134
|
+
spiffe_id=self.spiffe_id,
|
|
135
|
+
svid_type=svid_type,
|
|
136
|
+
trust_domain=self.trust_domain,
|
|
137
|
+
issued_at=now,
|
|
138
|
+
expires_at=now + timedelta(hours=ttl_hours),
|
|
139
|
+
agent_did=self.agent_did,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
self.current_svid = svid
|
|
143
|
+
return svid
|
|
144
|
+
|
|
145
|
+
def get_valid_svid(self) -> Optional[SVID]:
|
|
146
|
+
"""Get current SVID if valid, None otherwise."""
|
|
147
|
+
if self.current_svid and self.current_svid.is_valid():
|
|
148
|
+
return self.current_svid
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def needs_rotation(self, threshold_minutes: int = 10) -> bool:
|
|
152
|
+
"""Check if SVID needs rotation."""
|
|
153
|
+
if not self.current_svid:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
remaining = self.current_svid.time_remaining()
|
|
157
|
+
return remaining < timedelta(minutes=threshold_minutes)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SPIFFERegistry:
|
|
161
|
+
"""
|
|
162
|
+
Registry mapping AgentMesh identities to SPIFFE identities.
|
|
163
|
+
|
|
164
|
+
In production, this would integrate with SPIRE.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
DEFAULT_TRUST_DOMAIN = "agentmesh.local"
|
|
168
|
+
|
|
169
|
+
def __init__(self, trust_domain: Optional[str] = None):
|
|
170
|
+
self.trust_domain = trust_domain or self.DEFAULT_TRUST_DOMAIN
|
|
171
|
+
self._identities: dict[str, SPIFFEIdentity] = {} # agent_did -> SPIFFEIdentity
|
|
172
|
+
|
|
173
|
+
def register(
|
|
174
|
+
self,
|
|
175
|
+
agent_did: str,
|
|
176
|
+
agent_name: str,
|
|
177
|
+
organization: Optional[str] = None,
|
|
178
|
+
) -> SPIFFEIdentity:
|
|
179
|
+
"""Register an agent and create SPIFFE identity."""
|
|
180
|
+
if agent_did in self._identities:
|
|
181
|
+
return self._identities[agent_did]
|
|
182
|
+
|
|
183
|
+
identity = SPIFFEIdentity.create(
|
|
184
|
+
agent_did=agent_did,
|
|
185
|
+
agent_name=agent_name,
|
|
186
|
+
trust_domain=self.trust_domain,
|
|
187
|
+
organization=organization,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
self._identities[agent_did] = identity
|
|
191
|
+
return identity
|
|
192
|
+
|
|
193
|
+
def get(self, agent_did: str) -> Optional[SPIFFEIdentity]:
|
|
194
|
+
"""Get SPIFFE identity for an agent."""
|
|
195
|
+
return self._identities.get(agent_did)
|
|
196
|
+
|
|
197
|
+
def get_by_spiffe_id(self, spiffe_id: str) -> Optional[SPIFFEIdentity]:
|
|
198
|
+
"""Get identity by SPIFFE ID."""
|
|
199
|
+
for identity in self._identities.values():
|
|
200
|
+
if identity.spiffe_id == spiffe_id:
|
|
201
|
+
return identity
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
def issue_svid(self, agent_did: str) -> Optional[SVID]:
|
|
205
|
+
"""Issue an SVID for an agent."""
|
|
206
|
+
identity = self.get(agent_did)
|
|
207
|
+
if not identity:
|
|
208
|
+
return None
|
|
209
|
+
return identity.issue_svid()
|
|
210
|
+
|
|
211
|
+
def validate_svid(self, svid: SVID) -> bool:
|
|
212
|
+
"""
|
|
213
|
+
Validate an SVID.
|
|
214
|
+
|
|
215
|
+
In production, would verify against SPIRE bundle.
|
|
216
|
+
"""
|
|
217
|
+
# Check basic validity
|
|
218
|
+
if not svid.is_valid():
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
# Verify trust domain matches
|
|
222
|
+
if svid.trust_domain != self.trust_domain:
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
# Verify agent is registered
|
|
226
|
+
identity = self.get(svid.agent_did)
|
|
227
|
+
if not identity:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
return True
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Human Sponsor
|
|
3
|
+
|
|
4
|
+
Every agent identity is linked to a human sponsor who is accountable.
|
|
5
|
+
The sponsor's credentials are cryptographically linked to the delegation chain.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from pydantic import BaseModel, Field, EmailStr
|
|
11
|
+
import hashlib
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HumanSponsor(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Human sponsor responsible for an agent's actions.
|
|
18
|
+
|
|
19
|
+
The sponsor is the accountability anchor - when an agent causes
|
|
20
|
+
damage, the sponsor is responsible.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
sponsor_id: str = Field(..., description="Unique sponsor identifier")
|
|
24
|
+
|
|
25
|
+
# Identity
|
|
26
|
+
email: EmailStr = Field(..., description="Verified email address")
|
|
27
|
+
name: Optional[str] = Field(None, description="Human name")
|
|
28
|
+
|
|
29
|
+
# Organization
|
|
30
|
+
organization_id: Optional[str] = Field(None)
|
|
31
|
+
organization_name: Optional[str] = Field(None)
|
|
32
|
+
department: Optional[str] = Field(None)
|
|
33
|
+
|
|
34
|
+
# Verification
|
|
35
|
+
verified: bool = Field(default=False)
|
|
36
|
+
verified_at: Optional[datetime] = Field(None)
|
|
37
|
+
verification_method: Optional[str] = Field(None) # "email", "sso", "manual"
|
|
38
|
+
|
|
39
|
+
# Permissions
|
|
40
|
+
max_agents: int = Field(default=10, description="Max agents this sponsor can create")
|
|
41
|
+
max_delegation_depth: int = Field(default=3, description="Max delegation chain depth")
|
|
42
|
+
allowed_capabilities: list[str] = Field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
# Status
|
|
45
|
+
status: str = Field(default="active") # active, suspended, revoked
|
|
46
|
+
|
|
47
|
+
# Agents
|
|
48
|
+
agent_dids: list[str] = Field(default_factory=list, description="DIDs of sponsored agents")
|
|
49
|
+
|
|
50
|
+
# Timestamps
|
|
51
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
52
|
+
last_activity_at: Optional[datetime] = Field(None)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def create(
|
|
56
|
+
cls,
|
|
57
|
+
email: str,
|
|
58
|
+
name: Optional[str] = None,
|
|
59
|
+
organization: Optional[str] = None,
|
|
60
|
+
allowed_capabilities: Optional[list[str]] = None,
|
|
61
|
+
) -> "HumanSponsor":
|
|
62
|
+
"""Create a new human sponsor."""
|
|
63
|
+
sponsor_id = f"sponsor_{uuid.uuid4().hex[:16]}"
|
|
64
|
+
|
|
65
|
+
return cls(
|
|
66
|
+
sponsor_id=sponsor_id,
|
|
67
|
+
email=email,
|
|
68
|
+
name=name,
|
|
69
|
+
organization_name=organization,
|
|
70
|
+
allowed_capabilities=allowed_capabilities or ["*"], # Default: all capabilities
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def verify(self, method: str = "email") -> None:
|
|
74
|
+
"""Mark sponsor as verified."""
|
|
75
|
+
self.verified = True
|
|
76
|
+
self.verified_at = datetime.utcnow()
|
|
77
|
+
self.verification_method = method
|
|
78
|
+
|
|
79
|
+
def can_sponsor_agent(self) -> bool:
|
|
80
|
+
"""Check if sponsor can create more agents."""
|
|
81
|
+
if self.status != "active":
|
|
82
|
+
return False
|
|
83
|
+
if not self.verified:
|
|
84
|
+
return False
|
|
85
|
+
return len(self.agent_dids) < self.max_agents
|
|
86
|
+
|
|
87
|
+
def can_grant_capability(self, capability: str) -> bool:
|
|
88
|
+
"""Check if sponsor can grant a specific capability."""
|
|
89
|
+
if "*" in self.allowed_capabilities:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
if capability in self.allowed_capabilities:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
# Check prefix matching
|
|
96
|
+
for allowed in self.allowed_capabilities:
|
|
97
|
+
if allowed.endswith(":*"):
|
|
98
|
+
prefix = allowed[:-2]
|
|
99
|
+
if capability.startswith(prefix + ":"):
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def add_agent(self, agent_did: str) -> None:
|
|
105
|
+
"""Track a new agent sponsored by this human."""
|
|
106
|
+
if agent_did not in self.agent_dids:
|
|
107
|
+
self.agent_dids.append(agent_did)
|
|
108
|
+
self.last_activity_at = datetime.utcnow()
|
|
109
|
+
|
|
110
|
+
def remove_agent(self, agent_did: str) -> None:
|
|
111
|
+
"""Remove an agent from sponsorship."""
|
|
112
|
+
if agent_did in self.agent_dids:
|
|
113
|
+
self.agent_dids.remove(agent_did)
|
|
114
|
+
|
|
115
|
+
def suspend(self, reason: Optional[str] = None) -> None:
|
|
116
|
+
"""Suspend this sponsor (and all their agents should be suspended too)."""
|
|
117
|
+
self.status = "suspended"
|
|
118
|
+
|
|
119
|
+
def reactivate(self) -> None:
|
|
120
|
+
"""Reactivate a suspended sponsor."""
|
|
121
|
+
if self.status == "revoked":
|
|
122
|
+
raise ValueError("Cannot reactivate a revoked sponsor")
|
|
123
|
+
self.status = "active"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SponsorRegistry:
|
|
127
|
+
"""
|
|
128
|
+
Registry for human sponsors.
|
|
129
|
+
|
|
130
|
+
Tracks who is accountable for which agents.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self):
|
|
134
|
+
self._sponsors: dict[str, HumanSponsor] = {}
|
|
135
|
+
self._by_email: dict[str, str] = {} # email -> sponsor_id
|
|
136
|
+
|
|
137
|
+
def register(self, sponsor: HumanSponsor) -> None:
|
|
138
|
+
"""Register a new sponsor."""
|
|
139
|
+
if sponsor.email in self._by_email:
|
|
140
|
+
raise ValueError(f"Sponsor already registered: {sponsor.email}")
|
|
141
|
+
|
|
142
|
+
self._sponsors[sponsor.sponsor_id] = sponsor
|
|
143
|
+
self._by_email[sponsor.email] = sponsor.sponsor_id
|
|
144
|
+
|
|
145
|
+
def get(self, sponsor_id: str) -> Optional[HumanSponsor]:
|
|
146
|
+
"""Get sponsor by ID."""
|
|
147
|
+
return self._sponsors.get(sponsor_id)
|
|
148
|
+
|
|
149
|
+
def get_by_email(self, email: str) -> Optional[HumanSponsor]:
|
|
150
|
+
"""Get sponsor by email."""
|
|
151
|
+
sponsor_id = self._by_email.get(email)
|
|
152
|
+
if sponsor_id:
|
|
153
|
+
return self._sponsors.get(sponsor_id)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def get_or_create(
|
|
157
|
+
self,
|
|
158
|
+
email: str,
|
|
159
|
+
name: Optional[str] = None,
|
|
160
|
+
organization: Optional[str] = None,
|
|
161
|
+
) -> HumanSponsor:
|
|
162
|
+
"""Get existing sponsor or create new one."""
|
|
163
|
+
existing = self.get_by_email(email)
|
|
164
|
+
if existing:
|
|
165
|
+
return existing
|
|
166
|
+
|
|
167
|
+
sponsor = HumanSponsor.create(email, name, organization)
|
|
168
|
+
self.register(sponsor)
|
|
169
|
+
return sponsor
|
|
170
|
+
|
|
171
|
+
def suspend_all_for_org(self, organization_id: str) -> int:
|
|
172
|
+
"""Suspend all sponsors in an organization."""
|
|
173
|
+
count = 0
|
|
174
|
+
for sponsor in self._sponsors.values():
|
|
175
|
+
if sponsor.organization_id == organization_id:
|
|
176
|
+
sponsor.suspend()
|
|
177
|
+
count += 1
|
|
178
|
+
return count
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reward & Learning Engine (Layer 4)
|
|
3
|
+
|
|
4
|
+
Continuous behavioral feedback loop that scores every agent action
|
|
5
|
+
against a multi-dimensional governance rubric.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .engine import RewardEngine
|
|
9
|
+
from .scoring import TrustScore, RewardDimension, RewardSignal
|
|
10
|
+
from .learning import AdaptiveLearner, WeightOptimizer
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"RewardEngine",
|
|
14
|
+
"TrustScore",
|
|
15
|
+
"RewardDimension",
|
|
16
|
+
"RewardSignal",
|
|
17
|
+
"AdaptiveLearner",
|
|
18
|
+
"WeightOptimizer",
|
|
19
|
+
]
|