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,385 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Policy Engine
|
|
3
|
+
|
|
4
|
+
Declarative policy engine with YAML/JSON policies.
|
|
5
|
+
Policy evaluation latency <5ms with 100% deterministic results.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Optional, Literal, Any, Callable
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
import yaml
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PolicyRule(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
A single policy rule.
|
|
19
|
+
|
|
20
|
+
Rules define conditions and actions:
|
|
21
|
+
- condition: Expression that evaluates to true/false
|
|
22
|
+
- action: What to do when condition matches (allow, deny, warn, require_approval)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
name: str = Field(..., description="Rule name")
|
|
26
|
+
description: Optional[str] = Field(None)
|
|
27
|
+
|
|
28
|
+
# Condition
|
|
29
|
+
condition: str = Field(..., description="Condition expression")
|
|
30
|
+
|
|
31
|
+
# Action
|
|
32
|
+
action: Literal["allow", "deny", "warn", "require_approval", "log"] = Field(
|
|
33
|
+
default="deny"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Rate limiting
|
|
37
|
+
limit: Optional[str] = Field(None, description="Rate limit (e.g., '100/hour')")
|
|
38
|
+
|
|
39
|
+
# Approval workflow
|
|
40
|
+
approvers: list[str] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
# Priority (higher = evaluated first)
|
|
43
|
+
priority: int = Field(default=0)
|
|
44
|
+
|
|
45
|
+
# Enabled
|
|
46
|
+
enabled: bool = Field(default=True)
|
|
47
|
+
|
|
48
|
+
def evaluate(self, context: dict) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Evaluate the condition against a context.
|
|
51
|
+
|
|
52
|
+
Supports simple expressions like:
|
|
53
|
+
- action.type == 'export'
|
|
54
|
+
- data.contains_pii
|
|
55
|
+
- user.role in ['admin', 'operator']
|
|
56
|
+
"""
|
|
57
|
+
if not self.enabled:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
# Simple expression evaluation
|
|
62
|
+
# In production, would use a proper expression parser
|
|
63
|
+
return self._eval_expression(self.condition, context)
|
|
64
|
+
except Exception:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
def _eval_expression(self, expr: str, context: dict) -> bool:
|
|
68
|
+
"""Evaluate a simple expression."""
|
|
69
|
+
# Handle common patterns
|
|
70
|
+
|
|
71
|
+
# Equality: action.type == 'export'
|
|
72
|
+
eq_match = re.match(r"(\w+(?:\.\w+)*)\s*==\s*['\"]([^'\"]+)['\"]", expr)
|
|
73
|
+
if eq_match:
|
|
74
|
+
path, value = eq_match.groups()
|
|
75
|
+
actual = self._get_nested(context, path)
|
|
76
|
+
return actual == value
|
|
77
|
+
|
|
78
|
+
# Boolean attribute: data.contains_pii
|
|
79
|
+
bool_match = re.match(r"^(\w+(?:\.\w+)*)$", expr)
|
|
80
|
+
if bool_match:
|
|
81
|
+
path = bool_match.group(1)
|
|
82
|
+
return bool(self._get_nested(context, path))
|
|
83
|
+
|
|
84
|
+
# AND conditions
|
|
85
|
+
if " and " in expr:
|
|
86
|
+
parts = expr.split(" and ")
|
|
87
|
+
return all(self._eval_expression(p.strip(), context) for p in parts)
|
|
88
|
+
|
|
89
|
+
# OR conditions
|
|
90
|
+
if " or " in expr:
|
|
91
|
+
parts = expr.split(" or ")
|
|
92
|
+
return any(self._eval_expression(p.strip(), context) for p in parts)
|
|
93
|
+
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def _get_nested(self, obj: dict, path: str) -> Any:
|
|
97
|
+
"""Get nested value from dict using dot notation."""
|
|
98
|
+
parts = path.split(".")
|
|
99
|
+
current = obj
|
|
100
|
+
for part in parts:
|
|
101
|
+
if isinstance(current, dict):
|
|
102
|
+
current = current.get(part)
|
|
103
|
+
else:
|
|
104
|
+
return None
|
|
105
|
+
return current
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Policy(BaseModel):
|
|
109
|
+
"""
|
|
110
|
+
Complete policy document.
|
|
111
|
+
|
|
112
|
+
Policies are defined in YAML/JSON and loaded at runtime.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
version: str = Field(default="1.0")
|
|
116
|
+
name: str = Field(...)
|
|
117
|
+
description: Optional[str] = Field(None)
|
|
118
|
+
|
|
119
|
+
# Target
|
|
120
|
+
agent: Optional[str] = Field(None, description="Agent this policy applies to")
|
|
121
|
+
agents: list[str] = Field(default_factory=list, description="Multiple agents")
|
|
122
|
+
|
|
123
|
+
# Rules
|
|
124
|
+
rules: list[PolicyRule] = Field(default_factory=list)
|
|
125
|
+
|
|
126
|
+
# Default action
|
|
127
|
+
default_action: Literal["allow", "deny"] = Field(default="deny")
|
|
128
|
+
|
|
129
|
+
# Metadata
|
|
130
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
131
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def from_yaml(cls, yaml_content: str) -> "Policy":
|
|
135
|
+
"""Load policy from YAML."""
|
|
136
|
+
data = yaml.safe_load(yaml_content)
|
|
137
|
+
|
|
138
|
+
# Parse rules
|
|
139
|
+
rules = []
|
|
140
|
+
for rule_data in data.get("rules", []):
|
|
141
|
+
rules.append(PolicyRule(**rule_data))
|
|
142
|
+
data["rules"] = rules
|
|
143
|
+
|
|
144
|
+
return cls(**data)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_json(cls, json_content: str) -> "Policy":
|
|
148
|
+
"""Load policy from JSON."""
|
|
149
|
+
data = json.loads(json_content)
|
|
150
|
+
|
|
151
|
+
rules = []
|
|
152
|
+
for rule_data in data.get("rules", []):
|
|
153
|
+
rules.append(PolicyRule(**rule_data))
|
|
154
|
+
data["rules"] = rules
|
|
155
|
+
|
|
156
|
+
return cls(**data)
|
|
157
|
+
|
|
158
|
+
def applies_to(self, agent_did: str) -> bool:
|
|
159
|
+
"""Check if this policy applies to an agent."""
|
|
160
|
+
if self.agent and self.agent == agent_did:
|
|
161
|
+
return True
|
|
162
|
+
if agent_did in self.agents:
|
|
163
|
+
return True
|
|
164
|
+
if "*" in self.agents:
|
|
165
|
+
return True
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
def to_yaml(self) -> str:
|
|
169
|
+
"""Export policy as YAML."""
|
|
170
|
+
data = self.model_dump(exclude_none=True)
|
|
171
|
+
# Convert rules to dicts
|
|
172
|
+
data["rules"] = [r.model_dump(exclude_none=True) for r in self.rules]
|
|
173
|
+
return yaml.dump(data, default_flow_style=False)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class PolicyDecision(BaseModel):
|
|
177
|
+
"""Result of policy evaluation."""
|
|
178
|
+
|
|
179
|
+
allowed: bool
|
|
180
|
+
action: Literal["allow", "deny", "warn", "require_approval", "log"]
|
|
181
|
+
|
|
182
|
+
# Which rule matched
|
|
183
|
+
matched_rule: Optional[str] = None
|
|
184
|
+
policy_name: Optional[str] = None
|
|
185
|
+
|
|
186
|
+
# Details
|
|
187
|
+
reason: Optional[str] = None
|
|
188
|
+
|
|
189
|
+
# For require_approval
|
|
190
|
+
approvers: list[str] = Field(default_factory=list)
|
|
191
|
+
|
|
192
|
+
# For rate limiting
|
|
193
|
+
rate_limited: bool = False
|
|
194
|
+
rate_limit_reset: Optional[datetime] = None
|
|
195
|
+
|
|
196
|
+
# Timing
|
|
197
|
+
evaluated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
198
|
+
evaluation_ms: Optional[float] = None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class PolicyEngine:
|
|
202
|
+
"""
|
|
203
|
+
Declarative policy engine.
|
|
204
|
+
|
|
205
|
+
Features:
|
|
206
|
+
- YAML/JSON policy definitions
|
|
207
|
+
- <5ms evaluation latency
|
|
208
|
+
- 100% deterministic across runs
|
|
209
|
+
- Rate limiting support
|
|
210
|
+
- Approval workflows
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
MAX_EVAL_MS = 5 # Target: <5ms evaluation
|
|
214
|
+
|
|
215
|
+
def __init__(self):
|
|
216
|
+
self._policies: dict[str, Policy] = {}
|
|
217
|
+
self._rate_limits: dict[str, dict] = {} # rule_name -> {count, reset_at}
|
|
218
|
+
|
|
219
|
+
def load_policy(self, policy: Policy) -> None:
|
|
220
|
+
"""Load a policy into the engine."""
|
|
221
|
+
self._policies[policy.name] = policy
|
|
222
|
+
|
|
223
|
+
def load_yaml(self, yaml_content: str) -> Policy:
|
|
224
|
+
"""Load policy from YAML string."""
|
|
225
|
+
policy = Policy.from_yaml(yaml_content)
|
|
226
|
+
self.load_policy(policy)
|
|
227
|
+
return policy
|
|
228
|
+
|
|
229
|
+
def load_json(self, json_content: str) -> Policy:
|
|
230
|
+
"""Load policy from JSON string."""
|
|
231
|
+
policy = Policy.from_json(json_content)
|
|
232
|
+
self.load_policy(policy)
|
|
233
|
+
return policy
|
|
234
|
+
|
|
235
|
+
def evaluate(
|
|
236
|
+
self,
|
|
237
|
+
agent_did: str,
|
|
238
|
+
context: dict,
|
|
239
|
+
) -> PolicyDecision:
|
|
240
|
+
"""
|
|
241
|
+
Evaluate all applicable policies for an action.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
agent_did: The agent performing the action
|
|
245
|
+
context: Context for evaluation (action, data, etc.)
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
PolicyDecision with allow/deny and details
|
|
249
|
+
"""
|
|
250
|
+
start = datetime.utcnow()
|
|
251
|
+
|
|
252
|
+
# Get applicable policies
|
|
253
|
+
applicable = [p for p in self._policies.values() if p.applies_to(agent_did)]
|
|
254
|
+
|
|
255
|
+
if not applicable:
|
|
256
|
+
# No policies = default allow
|
|
257
|
+
return PolicyDecision(
|
|
258
|
+
allowed=True,
|
|
259
|
+
action="allow",
|
|
260
|
+
reason="No applicable policies",
|
|
261
|
+
evaluated_at=start,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Evaluate rules in priority order
|
|
265
|
+
all_rules = []
|
|
266
|
+
for policy in applicable:
|
|
267
|
+
for rule in policy.rules:
|
|
268
|
+
all_rules.append((policy, rule))
|
|
269
|
+
|
|
270
|
+
all_rules.sort(key=lambda x: x[1].priority, reverse=True)
|
|
271
|
+
|
|
272
|
+
for policy, rule in all_rules:
|
|
273
|
+
if rule.evaluate(context):
|
|
274
|
+
# Rule matched
|
|
275
|
+
decision = self._apply_rule(rule, policy)
|
|
276
|
+
|
|
277
|
+
# Calculate timing
|
|
278
|
+
elapsed = (datetime.utcnow() - start).total_seconds() * 1000
|
|
279
|
+
decision.evaluation_ms = elapsed
|
|
280
|
+
|
|
281
|
+
return decision
|
|
282
|
+
|
|
283
|
+
# No rules matched - use default action
|
|
284
|
+
default = applicable[0].default_action if applicable else "allow"
|
|
285
|
+
elapsed = (datetime.utcnow() - start).total_seconds() * 1000
|
|
286
|
+
|
|
287
|
+
return PolicyDecision(
|
|
288
|
+
allowed=(default == "allow"),
|
|
289
|
+
action=default,
|
|
290
|
+
reason="No matching rules, using default",
|
|
291
|
+
evaluated_at=start,
|
|
292
|
+
evaluation_ms=elapsed,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def _apply_rule(self, rule: PolicyRule, policy: Policy) -> PolicyDecision:
|
|
296
|
+
"""Apply a matched rule."""
|
|
297
|
+
# Check rate limit if applicable
|
|
298
|
+
if rule.limit:
|
|
299
|
+
if self._is_rate_limited(rule):
|
|
300
|
+
return PolicyDecision(
|
|
301
|
+
allowed=False,
|
|
302
|
+
action="deny",
|
|
303
|
+
matched_rule=rule.name,
|
|
304
|
+
policy_name=policy.name,
|
|
305
|
+
reason=f"Rate limit exceeded: {rule.limit}",
|
|
306
|
+
rate_limited=True,
|
|
307
|
+
)
|
|
308
|
+
self._increment_rate_limit(rule)
|
|
309
|
+
|
|
310
|
+
return PolicyDecision(
|
|
311
|
+
allowed=(rule.action == "allow"),
|
|
312
|
+
action=rule.action,
|
|
313
|
+
matched_rule=rule.name,
|
|
314
|
+
policy_name=policy.name,
|
|
315
|
+
reason=rule.description or f"Matched rule: {rule.name}",
|
|
316
|
+
approvers=rule.approvers if rule.action == "require_approval" else [],
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def _is_rate_limited(self, rule: PolicyRule) -> bool:
|
|
320
|
+
"""Check if a rule is rate limited."""
|
|
321
|
+
if not rule.limit:
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
limit_key = rule.name
|
|
325
|
+
limit_data = self._rate_limits.get(limit_key)
|
|
326
|
+
|
|
327
|
+
if not limit_data:
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
# Check if reset time passed
|
|
331
|
+
if datetime.utcnow() > limit_data["reset_at"]:
|
|
332
|
+
self._rate_limits[limit_key] = None
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
# Parse limit (e.g., "100/hour")
|
|
336
|
+
count, period = self._parse_limit(rule.limit)
|
|
337
|
+
|
|
338
|
+
return limit_data["count"] >= count
|
|
339
|
+
|
|
340
|
+
def _increment_rate_limit(self, rule: PolicyRule) -> None:
|
|
341
|
+
"""Increment rate limit counter."""
|
|
342
|
+
if not rule.limit:
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
limit_key = rule.name
|
|
346
|
+
count, period = self._parse_limit(rule.limit)
|
|
347
|
+
|
|
348
|
+
if limit_key not in self._rate_limits or self._rate_limits[limit_key] is None:
|
|
349
|
+
from datetime import timedelta
|
|
350
|
+
self._rate_limits[limit_key] = {
|
|
351
|
+
"count": 0,
|
|
352
|
+
"reset_at": datetime.utcnow() + timedelta(seconds=period),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
self._rate_limits[limit_key]["count"] += 1
|
|
356
|
+
|
|
357
|
+
def _parse_limit(self, limit: str) -> tuple[int, int]:
|
|
358
|
+
"""Parse a limit string like '100/hour'."""
|
|
359
|
+
parts = limit.split("/")
|
|
360
|
+
count = int(parts[0])
|
|
361
|
+
|
|
362
|
+
period_map = {
|
|
363
|
+
"second": 1,
|
|
364
|
+
"minute": 60,
|
|
365
|
+
"hour": 3600,
|
|
366
|
+
"day": 86400,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
period = period_map.get(parts[1], 3600)
|
|
370
|
+
return count, period
|
|
371
|
+
|
|
372
|
+
def get_policy(self, name: str) -> Optional[Policy]:
|
|
373
|
+
"""Get a policy by name."""
|
|
374
|
+
return self._policies.get(name)
|
|
375
|
+
|
|
376
|
+
def list_policies(self) -> list[str]:
|
|
377
|
+
"""List all loaded policy names."""
|
|
378
|
+
return list(self._policies.keys())
|
|
379
|
+
|
|
380
|
+
def remove_policy(self, name: str) -> bool:
|
|
381
|
+
"""Remove a policy."""
|
|
382
|
+
if name in self._policies:
|
|
383
|
+
del self._policies[name]
|
|
384
|
+
return True
|
|
385
|
+
return False
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shadow Mode
|
|
3
|
+
|
|
4
|
+
Simulate agent behavior against real policies without execution.
|
|
5
|
+
Shadow vs. production divergence target: <2% on replay dataset.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Optional, Any
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SimulatedAction:
|
|
17
|
+
"""An action to simulate in shadow mode."""
|
|
18
|
+
|
|
19
|
+
action_id: str
|
|
20
|
+
agent_did: str
|
|
21
|
+
action_type: str
|
|
22
|
+
context: dict
|
|
23
|
+
timestamp: datetime = None
|
|
24
|
+
|
|
25
|
+
def __post_init__(self):
|
|
26
|
+
if self.timestamp is None:
|
|
27
|
+
self.timestamp = datetime.utcnow()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ShadowResult(BaseModel):
|
|
31
|
+
"""Result of shadow mode evaluation."""
|
|
32
|
+
|
|
33
|
+
action_id: str
|
|
34
|
+
|
|
35
|
+
# Shadow evaluation
|
|
36
|
+
shadow_allowed: bool
|
|
37
|
+
shadow_action: str # allow, deny, warn, etc.
|
|
38
|
+
shadow_rule: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
# Production evaluation (if available)
|
|
41
|
+
production_allowed: Optional[bool] = None
|
|
42
|
+
production_action: Optional[str] = None
|
|
43
|
+
production_rule: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
# Divergence
|
|
46
|
+
diverged: bool = False
|
|
47
|
+
divergence_reason: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
# Timing
|
|
50
|
+
evaluated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
51
|
+
shadow_latency_ms: Optional[float] = None
|
|
52
|
+
production_latency_ms: Optional[float] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ShadowSession(BaseModel):
|
|
56
|
+
"""A shadow mode evaluation session."""
|
|
57
|
+
|
|
58
|
+
session_id: str = Field(default_factory=lambda: f"shadow_{uuid.uuid4().hex[:12]}")
|
|
59
|
+
started_at: datetime = Field(default_factory=datetime.utcnow)
|
|
60
|
+
|
|
61
|
+
# Scope
|
|
62
|
+
agent_dids: list[str] = Field(default_factory=list)
|
|
63
|
+
policy_names: list[str] = Field(default_factory=list)
|
|
64
|
+
|
|
65
|
+
# Results
|
|
66
|
+
total_evaluated: int = 0
|
|
67
|
+
total_diverged: int = 0
|
|
68
|
+
divergence_rate: float = 0.0
|
|
69
|
+
|
|
70
|
+
# Detailed results
|
|
71
|
+
results: list[ShadowResult] = Field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
# Status
|
|
74
|
+
active: bool = True
|
|
75
|
+
ended_at: Optional[datetime] = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ShadowMode:
|
|
79
|
+
"""
|
|
80
|
+
Shadow mode for policy testing.
|
|
81
|
+
|
|
82
|
+
Run new policies in shadow mode before production:
|
|
83
|
+
1. Load candidate policies
|
|
84
|
+
2. Replay production traffic or simulate actions
|
|
85
|
+
3. Compare shadow vs production decisions
|
|
86
|
+
4. Report divergence
|
|
87
|
+
|
|
88
|
+
Target: <2% divergence on replay dataset.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
DIVERGENCE_TARGET = 0.02 # 2%
|
|
92
|
+
|
|
93
|
+
def __init__(self, policy_engine):
|
|
94
|
+
"""
|
|
95
|
+
Initialize shadow mode.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
policy_engine: PolicyEngine to test in shadow mode
|
|
99
|
+
"""
|
|
100
|
+
self.policy_engine = policy_engine
|
|
101
|
+
self._sessions: dict[str, ShadowSession] = {}
|
|
102
|
+
self._active_session: Optional[str] = None
|
|
103
|
+
|
|
104
|
+
def start_session(
|
|
105
|
+
self,
|
|
106
|
+
agent_dids: Optional[list[str]] = None,
|
|
107
|
+
policy_names: Optional[list[str]] = None,
|
|
108
|
+
) -> ShadowSession:
|
|
109
|
+
"""Start a new shadow evaluation session."""
|
|
110
|
+
session = ShadowSession(
|
|
111
|
+
agent_dids=agent_dids or [],
|
|
112
|
+
policy_names=policy_names or [],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self._sessions[session.session_id] = session
|
|
116
|
+
self._active_session = session.session_id
|
|
117
|
+
|
|
118
|
+
return session
|
|
119
|
+
|
|
120
|
+
def evaluate(
|
|
121
|
+
self,
|
|
122
|
+
action: SimulatedAction,
|
|
123
|
+
production_decision: Optional[dict] = None,
|
|
124
|
+
) -> ShadowResult:
|
|
125
|
+
"""
|
|
126
|
+
Evaluate an action in shadow mode.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
action: The action to evaluate
|
|
130
|
+
production_decision: Optional actual production decision to compare
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
ShadowResult with comparison
|
|
134
|
+
"""
|
|
135
|
+
start = datetime.utcnow()
|
|
136
|
+
|
|
137
|
+
# Shadow evaluation
|
|
138
|
+
shadow_decision = self.policy_engine.evaluate(
|
|
139
|
+
agent_did=action.agent_did,
|
|
140
|
+
context=action.context,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
shadow_latency = (datetime.utcnow() - start).total_seconds() * 1000
|
|
144
|
+
|
|
145
|
+
# Build result
|
|
146
|
+
result = ShadowResult(
|
|
147
|
+
action_id=action.action_id,
|
|
148
|
+
shadow_allowed=shadow_decision.allowed,
|
|
149
|
+
shadow_action=shadow_decision.action,
|
|
150
|
+
shadow_rule=shadow_decision.matched_rule,
|
|
151
|
+
shadow_latency_ms=shadow_latency,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Compare with production if available
|
|
155
|
+
if production_decision:
|
|
156
|
+
result.production_allowed = production_decision.get("allowed")
|
|
157
|
+
result.production_action = production_decision.get("action")
|
|
158
|
+
result.production_rule = production_decision.get("matched_rule")
|
|
159
|
+
result.production_latency_ms = production_decision.get("latency_ms")
|
|
160
|
+
|
|
161
|
+
# Check for divergence
|
|
162
|
+
if result.shadow_allowed != result.production_allowed:
|
|
163
|
+
result.diverged = True
|
|
164
|
+
result.divergence_reason = (
|
|
165
|
+
f"Shadow={result.shadow_action}, Production={result.production_action}"
|
|
166
|
+
)
|
|
167
|
+
elif result.shadow_action != result.production_action:
|
|
168
|
+
result.diverged = True
|
|
169
|
+
result.divergence_reason = f"Action mismatch: {result.shadow_action} vs {result.production_action}"
|
|
170
|
+
|
|
171
|
+
# Record in session
|
|
172
|
+
if self._active_session:
|
|
173
|
+
session = self._sessions[self._active_session]
|
|
174
|
+
session.results.append(result)
|
|
175
|
+
session.total_evaluated += 1
|
|
176
|
+
if result.diverged:
|
|
177
|
+
session.total_diverged += 1
|
|
178
|
+
session.divergence_rate = (
|
|
179
|
+
session.total_diverged / session.total_evaluated
|
|
180
|
+
if session.total_evaluated > 0 else 0.0
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
def replay_batch(
|
|
186
|
+
self,
|
|
187
|
+
actions: list[SimulatedAction],
|
|
188
|
+
production_decisions: Optional[list[dict]] = None,
|
|
189
|
+
) -> list[ShadowResult]:
|
|
190
|
+
"""
|
|
191
|
+
Replay a batch of actions in shadow mode.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
actions: List of actions to replay
|
|
195
|
+
production_decisions: Optional list of production decisions to compare
|
|
196
|
+
"""
|
|
197
|
+
results = []
|
|
198
|
+
|
|
199
|
+
for i, action in enumerate(actions):
|
|
200
|
+
prod_decision = None
|
|
201
|
+
if production_decisions and i < len(production_decisions):
|
|
202
|
+
prod_decision = production_decisions[i]
|
|
203
|
+
|
|
204
|
+
result = self.evaluate(action, prod_decision)
|
|
205
|
+
results.append(result)
|
|
206
|
+
|
|
207
|
+
return results
|
|
208
|
+
|
|
209
|
+
def end_session(self, session_id: Optional[str] = None) -> ShadowSession:
|
|
210
|
+
"""End a shadow session and return summary."""
|
|
211
|
+
sid = session_id or self._active_session
|
|
212
|
+
if not sid or sid not in self._sessions:
|
|
213
|
+
raise ValueError("No active session")
|
|
214
|
+
|
|
215
|
+
session = self._sessions[sid]
|
|
216
|
+
session.active = False
|
|
217
|
+
session.ended_at = datetime.utcnow()
|
|
218
|
+
|
|
219
|
+
if sid == self._active_session:
|
|
220
|
+
self._active_session = None
|
|
221
|
+
|
|
222
|
+
return session
|
|
223
|
+
|
|
224
|
+
def get_session(self, session_id: str) -> Optional[ShadowSession]:
|
|
225
|
+
"""Get session by ID."""
|
|
226
|
+
return self._sessions.get(session_id)
|
|
227
|
+
|
|
228
|
+
def get_divergence_report(self, session_id: Optional[str] = None) -> dict:
|
|
229
|
+
"""Generate divergence report for a session."""
|
|
230
|
+
sid = session_id or self._active_session
|
|
231
|
+
if not sid or sid not in self._sessions:
|
|
232
|
+
return {"error": "No session found"}
|
|
233
|
+
|
|
234
|
+
session = self._sessions[sid]
|
|
235
|
+
|
|
236
|
+
# Group divergences by reason
|
|
237
|
+
divergence_reasons = {}
|
|
238
|
+
for result in session.results:
|
|
239
|
+
if result.diverged:
|
|
240
|
+
reason = result.divergence_reason or "unknown"
|
|
241
|
+
if reason not in divergence_reasons:
|
|
242
|
+
divergence_reasons[reason] = 0
|
|
243
|
+
divergence_reasons[reason] += 1
|
|
244
|
+
|
|
245
|
+
# Check if within target
|
|
246
|
+
within_target = session.divergence_rate <= self.DIVERGENCE_TARGET
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"session_id": session.session_id,
|
|
250
|
+
"total_evaluated": session.total_evaluated,
|
|
251
|
+
"total_diverged": session.total_diverged,
|
|
252
|
+
"divergence_rate": session.divergence_rate,
|
|
253
|
+
"divergence_rate_pct": f"{session.divergence_rate * 100:.2f}%",
|
|
254
|
+
"target_rate_pct": f"{self.DIVERGENCE_TARGET * 100:.2f}%",
|
|
255
|
+
"within_target": within_target,
|
|
256
|
+
"divergence_breakdown": divergence_reasons,
|
|
257
|
+
"recommendation": (
|
|
258
|
+
"Ready for production" if within_target
|
|
259
|
+
else "Review divergent cases before production"
|
|
260
|
+
),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
def is_ready_for_production(self, session_id: Optional[str] = None) -> bool:
|
|
264
|
+
"""Check if shadow session shows policy is ready for production."""
|
|
265
|
+
report = self.get_divergence_report(session_id)
|
|
266
|
+
return report.get("within_target", False)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Identity & Zero-Trust Core (Layer 1)
|
|
3
|
+
|
|
4
|
+
First-class agent identity with:
|
|
5
|
+
- Cryptographically bound identities
|
|
6
|
+
- Human sponsor accountability
|
|
7
|
+
- Ephemeral credentials (15-min TTL)
|
|
8
|
+
- SPIFFE/SVID workload identity
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .agent_id import AgentIdentity, AgentDID
|
|
12
|
+
from .credentials import Credential, CredentialManager
|
|
13
|
+
from .delegation import DelegationChain, DelegationLink
|
|
14
|
+
from .sponsor import HumanSponsor
|
|
15
|
+
from .risk import RiskScorer, RiskScore
|
|
16
|
+
from .spiffe import SPIFFEIdentity, SVID
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AgentIdentity",
|
|
20
|
+
"AgentDID",
|
|
21
|
+
"Credential",
|
|
22
|
+
"CredentialManager",
|
|
23
|
+
"DelegationChain",
|
|
24
|
+
"DelegationLink",
|
|
25
|
+
"HumanSponsor",
|
|
26
|
+
"RiskScorer",
|
|
27
|
+
"RiskScore",
|
|
28
|
+
"SPIFFEIdentity",
|
|
29
|
+
"SVID",
|
|
30
|
+
]
|