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.
@@ -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
+ ]