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,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
+ ]