halyn 0.2.0__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.
halyn/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
+ # Licensed under the MIT License. See LICENSE file.
3
+ """Halyn — NRP control plane with domain-scoped authorization."""
4
+
5
+ __version__ = "0.2.0"
6
+ __author__ = "Elmadani SALKA"
7
+ __license__ = "MIT"
halyn/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
+ # Licensed under the MIT License. See LICENSE file.
3
+ from .cli import main
4
+ main()
halyn/audit.py ADDED
@@ -0,0 +1,278 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
+ # Licensed under the MIT License. See LICENSE file.
3
+ """
4
+ Audit — Indestructible record of every action.
5
+
6
+ Append-only audit log with SHA-256 hash chaining.
7
+ Persisted via SQLite WAL. Tamper-detectable in O(n).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import logging
15
+ import os
16
+ import sqlite3
17
+ import threading
18
+ import time
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ log = logging.getLogger("halyn.audit")
24
+
25
+ _SCHEMA = """
26
+ CREATE TABLE IF NOT EXISTS audit_log (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ timestamp REAL NOT NULL,
29
+ tool TEXT NOT NULL,
30
+ node TEXT NOT NULL DEFAULT '',
31
+ args TEXT NOT NULL DEFAULT '{}',
32
+ result TEXT NOT NULL DEFAULT '',
33
+ status TEXT NOT NULL DEFAULT 'ok',
34
+ duration_ms REAL NOT NULL DEFAULT 0,
35
+ user_id TEXT NOT NULL DEFAULT '',
36
+ llm_model TEXT NOT NULL DEFAULT '',
37
+ intent TEXT NOT NULL DEFAULT '',
38
+ domain TEXT NOT NULL DEFAULT '',
39
+ autonomy_level INTEGER NOT NULL DEFAULT -1,
40
+ decision TEXT NOT NULL DEFAULT '',
41
+ hash TEXT NOT NULL,
42
+ prev_hash TEXT NOT NULL
43
+ );
44
+ CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log(timestamp);
45
+ CREATE INDEX IF NOT EXISTS idx_audit_tool ON audit_log(tool);
46
+ CREATE INDEX IF NOT EXISTS idx_audit_node ON audit_log(node);
47
+ CREATE INDEX IF NOT EXISTS idx_audit_status ON audit_log(status);
48
+ CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id);
49
+ """
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class AuditEntry:
54
+ """One audited action. Immutable. Hashable."""
55
+ timestamp: float
56
+ tool: str
57
+ node: str
58
+ args: dict[str, Any]
59
+ result: str
60
+ status: str
61
+ duration_ms: float
62
+ user_id: str
63
+ llm_model: str
64
+ intent: str # Why was this action taken?
65
+ domain: str # Which domain policy applied?
66
+ autonomy_level: int # What level authorized it?
67
+ decision: str # allow / confirm / deny
68
+ hash: str
69
+ prev_hash: str
70
+
71
+ def to_dict(self) -> dict[str, Any]:
72
+ return {
73
+ "timestamp": self.timestamp,
74
+ "tool": self.tool,
75
+ "node": self.node,
76
+ "args": self.args,
77
+ "result": self.result[:500],
78
+ "status": self.status,
79
+ "duration_ms": self.duration_ms,
80
+ "user_id": self.user_id,
81
+ "llm_model": self.llm_model,
82
+ "intent": self.intent,
83
+ "domain": self.domain,
84
+ "autonomy_level": self.autonomy_level,
85
+ "decision": self.decision,
86
+ "hash": self.hash,
87
+ "prev_hash": self.prev_hash,
88
+ }
89
+
90
+
91
+ class AuditStore:
92
+ """
93
+ Persistent, tamper-evident audit trail.
94
+
95
+ Every action is:
96
+ 1. Written to disk BEFORE execution (WAL)
97
+ 2. Hash-chained to the previous entry
98
+ 3. Queryable by time, tool, node, user, status
99
+
100
+ Tampering breaks the chain. Detectable in O(n).
101
+ """
102
+
103
+ def __init__(self, db_path: str = "") -> None:
104
+ if not db_path:
105
+ data_dir = Path.home() / ".halyn"
106
+ data_dir.mkdir(parents=True, exist_ok=True)
107
+ db_path = str(data_dir / "audit.db")
108
+ self._db_path = db_path
109
+ self._lock = threading.Lock()
110
+ self._prev_hash = "GENESIS"
111
+ self._conn = sqlite3.connect(db_path, check_same_thread=False)
112
+ self._conn.execute("PRAGMA journal_mode=WAL")
113
+ self._conn.execute("PRAGMA synchronous=NORMAL")
114
+ self._conn.executescript(_SCHEMA)
115
+ self._conn.commit()
116
+ self._restore_chain()
117
+ log.info("audit.init db=%s chain_tip=%s", db_path, self._prev_hash[:16])
118
+
119
+ def record(
120
+ self,
121
+ tool: str,
122
+ node: str = "",
123
+ args: dict[str, Any] | None = None,
124
+ result: str = "",
125
+ status: str = "ok",
126
+ duration_ms: float = 0.0,
127
+ user_id: str = "",
128
+ llm_model: str = "",
129
+ intent: str = "",
130
+ domain: str = "",
131
+ autonomy_level: int = -1,
132
+ decision: str = "",
133
+ ) -> AuditEntry:
134
+ """Record an action. Returns the entry with its hash."""
135
+ ts = time.time()
136
+ args = args or {}
137
+
138
+ entry_hash = self._compute_hash(
139
+ ts, tool, node, json.dumps(args, default=str, sort_keys=True),
140
+ result[:500], status, self._prev_hash,
141
+ )
142
+
143
+ entry = AuditEntry(
144
+ timestamp=ts, tool=tool, node=node, args=args,
145
+ result=result[:2000], status=status, duration_ms=duration_ms,
146
+ user_id=user_id, llm_model=llm_model, intent=intent,
147
+ domain=domain, autonomy_level=autonomy_level, decision=decision,
148
+ hash=entry_hash, prev_hash=self._prev_hash,
149
+ )
150
+
151
+ with self._lock:
152
+ self._conn.execute(
153
+ "INSERT INTO audit_log "
154
+ "(timestamp, tool, node, args, result, status, duration_ms, "
155
+ "user_id, llm_model, intent, domain, autonomy_level, decision, "
156
+ "hash, prev_hash) "
157
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
158
+ (ts, tool, node, json.dumps(args, default=str),
159
+ result[:2000], status, duration_ms,
160
+ user_id, llm_model, intent, domain, autonomy_level,
161
+ decision, entry_hash, self._prev_hash),
162
+ )
163
+ self._conn.commit()
164
+ self._prev_hash = entry_hash
165
+
166
+ log.debug("audit.record tool=%s hash=%s", tool, entry_hash[:16])
167
+ return entry
168
+
169
+ def query(
170
+ self,
171
+ since: float = 0,
172
+ until: float = 0,
173
+ tool: str = "",
174
+ node: str = "",
175
+ user_id: str = "",
176
+ status: str = "",
177
+ limit: int = 100,
178
+ ) -> list[AuditEntry]:
179
+ """Query the audit trail with filters."""
180
+ conditions: list[str] = []
181
+ params: list[Any] = []
182
+
183
+ if since:
184
+ conditions.append("timestamp >= ?")
185
+ params.append(since)
186
+ if until:
187
+ conditions.append("timestamp <= ?")
188
+ params.append(until)
189
+ if tool:
190
+ conditions.append("tool LIKE ?")
191
+ params.append(f"%{tool}%")
192
+ if node:
193
+ conditions.append("node LIKE ?")
194
+ params.append(f"%{node}%")
195
+ if user_id:
196
+ conditions.append("user_id = ?")
197
+ params.append(user_id)
198
+ if status:
199
+ conditions.append("status = ?")
200
+ params.append(status)
201
+
202
+ where = " AND ".join(conditions) if conditions else "1=1"
203
+ sql = f"SELECT * FROM audit_log WHERE {where} ORDER BY timestamp DESC LIMIT ?"
204
+ params.append(limit)
205
+
206
+ with self._lock:
207
+ rows = self._conn.execute(sql, params).fetchall()
208
+
209
+ return [self._row_to_entry(r) for r in rows]
210
+
211
+ def verify_chain(self) -> tuple[bool, int, str]:
212
+ """
213
+ Verify the entire hash chain.
214
+ Returns: (valid, entries_checked, error_message)
215
+ """
216
+ with self._lock:
217
+ rows = self._conn.execute(
218
+ "SELECT timestamp, tool, node, args, result, status, hash, prev_hash "
219
+ "FROM audit_log ORDER BY id ASC"
220
+ ).fetchall()
221
+
222
+ prev = "GENESIS"
223
+ for i, row in enumerate(rows):
224
+ ts, tool, node, args_str, result, status, stored_hash, stored_prev = row
225
+ if stored_prev != prev:
226
+ return False, i, f"Chain broken at entry {i}: prev mismatch"
227
+ computed = self._compute_hash(ts, tool, node, args_str, result[:500], status, prev)
228
+ if computed != stored_hash:
229
+ return False, i, f"Tamper detected at entry {i}: hash mismatch"
230
+ prev = stored_hash
231
+
232
+ return True, len(rows), "Chain valid"
233
+
234
+ @property
235
+ def count(self) -> int:
236
+ with self._lock:
237
+ r = self._conn.execute("SELECT COUNT(*) FROM audit_log").fetchone()
238
+ return r[0] if r else 0
239
+
240
+ @property
241
+ def chain_tip(self) -> str:
242
+ return self._prev_hash
243
+
244
+ def export_jsonl(self, path: str) -> int:
245
+ """Export full audit trail to JSONL file."""
246
+ entries = self.query(limit=1_000_000)
247
+ with open(path, "w") as f:
248
+ for e in reversed(entries):
249
+ f.write(json.dumps(e.to_dict(), default=str) + "\n")
250
+ return len(entries)
251
+
252
+ def _restore_chain(self) -> None:
253
+ row = self._conn.execute(
254
+ "SELECT hash FROM audit_log ORDER BY id DESC LIMIT 1"
255
+ ).fetchone()
256
+ if row:
257
+ self._prev_hash = row[0]
258
+
259
+ @staticmethod
260
+ def _compute_hash(ts: float, tool: str, node: str, args: str,
261
+ result: str, status: str, prev_hash: str) -> str:
262
+ payload = f"{ts}|{tool}|{node}|{args}|{result}|{status}|{prev_hash}"
263
+ return hashlib.sha256(payload.encode()).hexdigest()
264
+
265
+ def _row_to_entry(self, row: tuple) -> AuditEntry:
266
+ return AuditEntry(
267
+ timestamp=row[1], tool=row[2], node=row[3],
268
+ args=json.loads(row[4]) if row[4] else {},
269
+ result=row[5], status=row[6], duration_ms=row[7],
270
+ user_id=row[8], llm_model=row[9], intent=row[10],
271
+ domain=row[11], autonomy_level=row[12], decision=row[13],
272
+ hash=row[14], prev_hash=row[15],
273
+ )
274
+
275
+ def close(self) -> None:
276
+ with self._lock:
277
+ self._conn.close()
278
+
halyn/auth.py ADDED
@@ -0,0 +1,88 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
+ # Licensed under the MIT License. See LICENSE file.
3
+ """
4
+ Auth — API authentication + rate limiting.
5
+
6
+ Simple, effective, no framework dependency.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import hmac
13
+ import logging
14
+ import os
15
+ import time
16
+ from typing import Any
17
+
18
+ from aiohttp import web
19
+
20
+ log = logging.getLogger("jarvis.auth")
21
+
22
+
23
+ class AuthMiddleware:
24
+ """API key authentication + rate limiting."""
25
+
26
+ def __init__(self, api_key: str = "", rate_limit: int = 60) -> None:
27
+ self.api_key = api_key or os.environ.get("JARVIS_API_KEY", "")
28
+ self.rate_limit = rate_limit # requests per minute
29
+ self._requests: dict[str, list[float]] = {} # ip -> timestamps
30
+ self.enabled = bool(self.api_key)
31
+
32
+ if not self.enabled:
33
+ log.warning("auth.disabled — set JARVIS_API_KEY to enable")
34
+
35
+ def check(self, request: web.Request) -> str | None:
36
+ """Returns error message if denied, None if allowed."""
37
+ # Health endpoint is always public
38
+ if request.path == "/health":
39
+ return None
40
+
41
+ # Auth check
42
+ if self.enabled:
43
+ key = (
44
+ request.headers.get("X-API-Key", "")
45
+ or request.headers.get("Authorization", "").removeprefix("Bearer ")
46
+ )
47
+ if not self._verify_key(key):
48
+ log.warning("auth.denied ip=%s path=%s", request.remote, request.path)
49
+ return "invalid or missing API key"
50
+
51
+ # Rate limit check
52
+ ip = request.remote or "unknown"
53
+ now = time.monotonic()
54
+ timestamps = self._requests.get(ip, [])
55
+ # Clean old entries (older than 60s)
56
+ timestamps = [t for t in timestamps if now - t < 60]
57
+ if len(timestamps) >= self.rate_limit:
58
+ log.warning("auth.rate_limited ip=%s count=%d", ip, len(timestamps))
59
+ return "rate limit exceeded"
60
+ timestamps.append(now)
61
+ self._requests[ip] = timestamps
62
+
63
+ return None
64
+
65
+ def _verify_key(self, provided: str) -> bool:
66
+ """Constant-time comparison to prevent timing attacks."""
67
+ if not provided or not self.api_key:
68
+ return False
69
+ return hmac.compare_digest(provided.encode(), self.api_key.encode())
70
+
71
+
72
+ def create_auth_middleware(api_key: str = "", rate_limit: int = 60):
73
+ """Create aiohttp middleware for auth."""
74
+ auth = AuthMiddleware(api_key, rate_limit)
75
+
76
+ @web.middleware
77
+ async def middleware(request: web.Request, handler: Any) -> web.Response:
78
+ error = auth.check(request)
79
+ if error:
80
+ return web.Response(
81
+ text=f'{{"ok":false,"error":"{error}"}}',
82
+ content_type="application/json",
83
+ status=401 if "API key" in error else 429,
84
+ )
85
+ return await handler(request)
86
+
87
+ return middleware
88
+
halyn/autonomy.py ADDED
@@ -0,0 +1,262 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
+ # Licensed under the MIT License. See LICENSE file.
3
+ """
4
+ Autonomy — The human always controls.
5
+
6
+ Domain-scoped authorization with 5 configurable trust levels.
7
+ Rate limiting, time-of-day restrictions, and per-command policies.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import time
14
+ from dataclasses import dataclass, field
15
+ from enum import IntEnum
16
+ from typing import Any, Callable, Awaitable
17
+
18
+ from .types import Action, ActionStatus, ToolCategory
19
+
20
+ log = logging.getLogger("halyn.autonomy")
21
+
22
+
23
+ class Level(IntEnum):
24
+ """How much freedom the AI has."""
25
+ MANUAL = 0 # AI proposes. Human approves EVERY action.
26
+ SUPERVISED = 1 # AI reads alone. Asks before writing/acting.
27
+ GUIDED = 2 # Safe actions alone. Dangerous = confirm.
28
+ AUTONOMOUS = 3 # Does everything. Human can interrupt.
29
+ FULL_AUTO = 4 # Handles routine. Reports daily.
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class DomainPolicy:
34
+ """Policy for one domain (physical, financial, infra, etc.)."""
35
+ name: str
36
+ level: Level = Level.SUPERVISED
37
+ node_patterns: list[str] = field(default_factory=lambda: ["*"])
38
+ hours: tuple[int, int] | None = None # (start_hour, end_hour) or None=always
39
+ max_actions_per_hour: int = 1000
40
+ blocked_commands: list[str] = field(default_factory=list)
41
+ confirm_commands: list[str] = field(default_factory=list)
42
+
43
+ def matches_node(self, node_name: str) -> bool:
44
+ import fnmatch
45
+ return any(fnmatch.fnmatch(node_name, p) for p in self.node_patterns)
46
+
47
+ def is_active_now(self) -> bool:
48
+ if self.hours is None:
49
+ return True
50
+ hour = time.localtime().tm_hour
51
+ start, end = self.hours
52
+ if start <= end:
53
+ return start <= hour < end
54
+ return hour >= start or hour < end # Overnight range
55
+
56
+
57
+ @dataclass(slots=True)
58
+ class ConfirmationRequest:
59
+ """A pending action waiting for human approval."""
60
+ request_id: str
61
+ action: Action
62
+ reason: str
63
+ domain: str
64
+ created_at: float = field(default_factory=time.time)
65
+ expires_at: float = 0.0 # 0 = no expiry
66
+ status: str = "pending" # pending, approved, denied, expired
67
+
68
+ def __post_init__(self) -> None:
69
+ if self.expires_at == 0.0:
70
+ self.expires_at = self.created_at + 300 # 5 min default
71
+
72
+ @property
73
+ def expired(self) -> bool:
74
+ return time.time() > self.expires_at and self.status == "pending"
75
+
76
+
77
+ class AutonomyController:
78
+ """
79
+ The gatekeeper. Every action passes through here.
80
+
81
+ Decides: execute immediately, ask for confirmation, or deny.
82
+ Based on: domain policy, action category, time of day, rate limits.
83
+ """
84
+
85
+ __slots__ = ("_domains", "_pending", "_action_counts", "_default_level")
86
+
87
+ def __init__(self, default_level: Level = Level.SUPERVISED) -> None:
88
+ self._domains: dict[str, DomainPolicy] = {}
89
+ self._pending: dict[str, ConfirmationRequest] = {}
90
+ self._action_counts: dict[str, list[float]] = {} # domain -> timestamps
91
+ self._default_level = default_level
92
+
93
+ def add_domain(self, policy: DomainPolicy) -> None:
94
+ self._domains[policy.name] = policy
95
+ log.info("autonomy.domain name=%s level=%s nodes=%s",
96
+ policy.name, Level(policy.level).name, policy.node_patterns)
97
+
98
+ def check(self, action: Action, tool_category: ToolCategory,
99
+ tool_dangerous: bool) -> tuple[str, str]:
100
+ """
101
+ Check if an action should proceed.
102
+
103
+ Returns: (decision, reason)
104
+ decision: "allow", "confirm", "deny"
105
+ reason: human-readable explanation
106
+ """
107
+ # Find matching domain
108
+ domain = self._find_domain(action.node)
109
+ if domain is None:
110
+ domain = DomainPolicy(name="default", level=self._default_level)
111
+
112
+ # Check if domain is active
113
+ if not domain.is_active_now():
114
+ return "deny", f"Domain '{domain.name}' not active at this hour"
115
+
116
+ # Check rate limit
117
+ if not self._check_rate(domain):
118
+ return "deny", f"Rate limit exceeded for domain '{domain.name}'"
119
+
120
+ # Check blocked commands
121
+ cmd = action.tool + " " + action.args.get("command", "")
122
+ for blocked in domain.blocked_commands:
123
+ if blocked.lower() in cmd.lower():
124
+ return "deny", f"Command blocked by domain policy: {blocked}"
125
+
126
+ # Check confirm commands
127
+ for confirm_pattern in domain.confirm_commands:
128
+ if confirm_pattern.lower() in cmd.lower():
129
+ return "confirm", f"Requires confirmation: matches '{confirm_pattern}'"
130
+
131
+ # Apply autonomy level
132
+ level = domain.level
133
+
134
+ if level == Level.MANUAL:
135
+ return "confirm", "Level MANUAL: all actions require approval"
136
+
137
+ if level == Level.SUPERVISED:
138
+ if tool_category == ToolCategory.OBSERVER:
139
+ self._record_action(domain.name)
140
+ return "allow", "Level SUPERVISED: observe allowed"
141
+ return "confirm", "Level SUPERVISED: action requires approval"
142
+
143
+ if level == Level.GUIDED:
144
+ if tool_dangerous:
145
+ return "confirm", f"Level GUIDED: dangerous action requires approval"
146
+ self._record_action(domain.name)
147
+ return "allow", "Level GUIDED: safe action allowed"
148
+
149
+ if level == Level.AUTONOMOUS:
150
+ self._record_action(domain.name)
151
+ return "allow", "Level AUTONOMOUS: action allowed (interruptible)"
152
+
153
+ if level == Level.FULL_AUTO:
154
+ self._record_action(domain.name)
155
+ return "allow", "Level FULL_AUTO: autonomous execution"
156
+
157
+ return "confirm", "Unknown level: defaulting to confirm"
158
+
159
+ # ─── Confirmation management ────────────────────
160
+
161
+ def request_confirmation(self, request_id: str, action: Action,
162
+ reason: str, domain: str = "") -> ConfirmationRequest:
163
+ req = ConfirmationRequest(
164
+ request_id=request_id, action=action,
165
+ reason=reason, domain=domain,
166
+ )
167
+ self._pending[request_id] = req
168
+ log.info("autonomy.confirm_requested id=%s tool=%s reason=%s",
169
+ request_id, action.tool, reason)
170
+ return req
171
+
172
+ def approve(self, request_id: str) -> bool:
173
+ req = self._pending.get(request_id)
174
+ if req and req.status == "pending" and not req.expired:
175
+ req.status = "approved"
176
+ log.info("autonomy.approved id=%s", request_id)
177
+ return True
178
+ return False
179
+
180
+ def deny(self, request_id: str) -> bool:
181
+ req = self._pending.get(request_id)
182
+ if req and req.status == "pending":
183
+ req.status = "denied"
184
+ log.info("autonomy.denied id=%s", request_id)
185
+ return True
186
+ return False
187
+
188
+ def get_pending(self) -> list[ConfirmationRequest]:
189
+ self._clean_expired()
190
+ return [r for r in self._pending.values() if r.status == "pending"]
191
+
192
+ def get_request(self, request_id: str) -> ConfirmationRequest | None:
193
+ return self._pending.get(request_id)
194
+
195
+ # ─── Internal ──────────────────────────────────
196
+
197
+ def _find_domain(self, node: str) -> DomainPolicy | None:
198
+ for domain in self._domains.values():
199
+ if domain.matches_node(node):
200
+ return domain
201
+ return None
202
+
203
+ def _check_rate(self, domain: DomainPolicy) -> bool:
204
+ now = time.time()
205
+ timestamps = self._action_counts.get(domain.name, [])
206
+ timestamps = [t for t in timestamps if now - t < 3600]
207
+ self._action_counts[domain.name] = timestamps
208
+ return len(timestamps) < domain.max_actions_per_hour
209
+
210
+ def _record_action(self, domain_name: str) -> None:
211
+ if domain_name not in self._action_counts:
212
+ self._action_counts[domain_name] = []
213
+ self._action_counts[domain_name].append(time.time())
214
+
215
+ def _clean_expired(self) -> None:
216
+ for req_id, req in list(self._pending.items()):
217
+ if req.expired:
218
+ req.status = "expired"
219
+ log.info("autonomy.expired id=%s", req_id)
220
+
221
+
222
+ # ─── Default domain presets ─────────────────────────
223
+
224
+ PRESET_DOMAINS = {
225
+ "physical": DomainPolicy(
226
+ name="physical",
227
+ level=Level.SUPERVISED,
228
+ node_patterns=["robot/*", "drone/*", "arm/*", "vehicle/*"],
229
+ confirm_commands=["emergency_stop", "move", "pick", "deploy"],
230
+ ),
231
+ "financial": DomainPolicy(
232
+ name="financial",
233
+ level=Level.MANUAL,
234
+ node_patterns=["finance/*", "bank/*", "payment/*"],
235
+ blocked_commands=["delete", "drop"],
236
+ ),
237
+ "infrastructure": DomainPolicy(
238
+ name="infrastructure",
239
+ level=Level.GUIDED,
240
+ node_patterns=["server/*", "cloud/*", "docker/*"],
241
+ confirm_commands=["restart", "deploy", "delete"],
242
+ ),
243
+ "monitoring": DomainPolicy(
244
+ name="monitoring",
245
+ level=Level.FULL_AUTO,
246
+ node_patterns=["sensor/*", "monitor/*", "metric/*"],
247
+ max_actions_per_hour=10000,
248
+ ),
249
+ "home": DomainPolicy(
250
+ name="home",
251
+ level=Level.AUTONOMOUS,
252
+ node_patterns=["home/*"],
253
+ blocked_commands=["unlock_door"],
254
+ ),
255
+ "communication": DomainPolicy(
256
+ name="communication",
257
+ level=Level.SUPERVISED,
258
+ node_patterns=["email/*", "slack/*", "telegram/*"],
259
+ confirm_commands=["send", "post", "reply"],
260
+ ),
261
+ }
262
+