authority-runtime 0.3.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.
@@ -0,0 +1,139 @@
1
+ """
2
+ Authority Runtime - Python
3
+
4
+ Cross-platform IAM layer for AI agents.
5
+ """
6
+
7
+ from .envelope import (
8
+ create_envelope,
9
+ create_simple_envelope,
10
+ create_child_envelope,
11
+ validate_envelope,
12
+ generate_key_pair,
13
+ verify_signature,
14
+ )
15
+ try:
16
+ from .compiler import LLMCompiler, OpenAICompiler, AnthropicCompiler
17
+ except ImportError:
18
+ LLMCompiler = None
19
+ OpenAICompiler = None
20
+ AnthropicCompiler = None
21
+
22
+ from .types import (
23
+ Skill,
24
+ Authority,
25
+ Context,
26
+ SkillSelection,
27
+ SkillParameters,
28
+ ExecutionConfig,
29
+ AuthorityEnvelope,
30
+ DecisionContext,
31
+ )
32
+ from .enforce import (
33
+ EnforcedTool,
34
+ EnforcedToolkit,
35
+ enforce,
36
+ check_envelope,
37
+ check_context_field,
38
+ PermissionDenied,
39
+ EnvelopeExpired,
40
+ InvalidSignature,
41
+ ConstraintViolation,
42
+ ApprovalRequired,
43
+ AuditEntry,
44
+ export_audit_trail,
45
+ create_audit_entry,
46
+ )
47
+ from .constraints import check_constraints, ConstraintResult
48
+ from .storage import EnvelopeStore
49
+ from .validation import ValidationError
50
+ try:
51
+ from .langgraph import (
52
+ AuthorityState,
53
+ create_authority_node,
54
+ create_authority_graph,
55
+ )
56
+ except ImportError:
57
+ AuthorityState = None
58
+ create_authority_node = None
59
+ create_authority_graph = None
60
+
61
+ from .vault_scope import (
62
+ VaultScope,
63
+ create_vault_envelope,
64
+ check_vault_access,
65
+ enforce_envelope,
66
+ )
67
+ from .keys import AgentKeyStore
68
+ from .backends.slos import (
69
+ SlosBackend,
70
+ Decision,
71
+ PolicyResult,
72
+ DocumentMetadata,
73
+ parse_slos_uri,
74
+ )
75
+ from .backends.memory import MemoryBackend
76
+
77
+ __version__ = "0.3.0"
78
+
79
+ __all__ = [
80
+ # Envelope core
81
+ "create_envelope",
82
+ "create_simple_envelope",
83
+ "create_child_envelope",
84
+ "validate_envelope",
85
+ "generate_key_pair",
86
+ "verify_signature",
87
+ # Compilers
88
+ "LLMCompiler",
89
+ "OpenAICompiler",
90
+ "AnthropicCompiler",
91
+ # Types
92
+ "Skill",
93
+ "Authority",
94
+ "Context",
95
+ "SkillSelection",
96
+ "SkillParameters",
97
+ "ExecutionConfig",
98
+ "AuthorityEnvelope",
99
+ "DecisionContext",
100
+ # Enforcement (the real value)
101
+ "EnforcedTool",
102
+ "EnforcedToolkit",
103
+ "enforce",
104
+ "check_envelope",
105
+ "check_context_field",
106
+ "PermissionDenied",
107
+ "EnvelopeExpired",
108
+ "InvalidSignature",
109
+ "ConstraintViolation",
110
+ "ApprovalRequired",
111
+ "ValidationError",
112
+ # Constraints
113
+ "check_constraints",
114
+ "ConstraintResult",
115
+ # Audit trail (enterprise compliance)
116
+ "AuditEntry",
117
+ "export_audit_trail",
118
+ "create_audit_entry",
119
+ # Persistence (production storage)
120
+ "EnvelopeStore",
121
+ # LangGraph integration
122
+ "AuthorityState",
123
+ "create_authority_node",
124
+ "create_authority_graph",
125
+ # Key management
126
+ "AgentKeyStore",
127
+ # Vault-scoped enforcement (multi-tenant)
128
+ "VaultScope",
129
+ "create_vault_envelope",
130
+ "check_vault_access",
131
+ "enforce_envelope",
132
+ # Backends
133
+ "SlosBackend",
134
+ "MemoryBackend",
135
+ "Decision",
136
+ "PolicyResult",
137
+ "DocumentMetadata",
138
+ "parse_slos_uri",
139
+ ]
@@ -0,0 +1,267 @@
1
+ """
2
+ Carryall Approval Queue — human-in-the-loop authorization for cross-domain access.
3
+
4
+ Manages approval requests as YAML files in the SLOS meta vault approvals directory,
5
+ matching the existing format at ~/slos/vaults/meta/approvals/.
6
+
7
+ When an agent's access requires approval (per OPA policy or document-level metadata),
8
+ the request is queued here. The agent receives a "requires_approval" response with a
9
+ request ID. The request can be approved/denied via Telegram or the Authority Dashboard.
10
+ """
11
+
12
+ import logging
13
+ import uuid
14
+ from datetime import datetime, timezone, timedelta
15
+ from pathlib import Path
16
+ from typing import Optional, Union
17
+
18
+ import yaml
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ApprovalQueue:
24
+ """
25
+ File-backed approval queue using YAML records.
26
+
27
+ Each approval request is stored as a YAML file named {request_id}.yaml
28
+ in the approvals directory (typically ~/slos/vaults/meta/approvals/).
29
+ """
30
+
31
+ def __init__(self, approvals_dir: Union[str, Path]):
32
+ self.approvals_dir = Path(approvals_dir).expanduser()
33
+ self.approvals_dir.mkdir(parents=True, exist_ok=True)
34
+
35
+ def _request_path(self, request_id: str) -> Path:
36
+ return self.approvals_dir / f"{request_id}.yaml"
37
+
38
+ def create_request(
39
+ self,
40
+ agent_id: str,
41
+ action: str,
42
+ resource_uri: str,
43
+ purpose: str,
44
+ target_domain: Optional[str] = None,
45
+ fields_needed: Optional[list[str]] = None,
46
+ ttl_seconds: int = 86400,
47
+ ) -> str:
48
+ """
49
+ Create a pending approval request.
50
+
51
+ Args:
52
+ agent_id: Agent requesting access
53
+ action: Action type (read, write, etc.)
54
+ resource_uri: SLOS URI of the resource
55
+ purpose: Why the agent needs access (audit trail)
56
+ target_domain: Target vault domain
57
+ fields_needed: Specific fields requested (optional)
58
+ ttl_seconds: Time to live before auto-expiry (default 24h)
59
+
60
+ Returns:
61
+ Request ID (UUIDv7-style)
62
+ """
63
+ now = datetime.now(timezone.utc)
64
+ request_id = str(uuid.uuid4())
65
+
66
+ # Extract domain from URI if not provided
67
+ if not target_domain and resource_uri.startswith("slos://vaults/"):
68
+ parts = resource_uri.replace("slos://vaults/", "").split("/", 1)
69
+ target_domain = parts[0] if parts else "unknown"
70
+
71
+ record = {
72
+ "id": request_id,
73
+ "agent_id": agent_id,
74
+ "action": action,
75
+ "resource_uri": resource_uri,
76
+ "target_domain": target_domain or "unknown",
77
+ "purpose": purpose,
78
+ "created_at": now.isoformat(),
79
+ "expires_at": (now + timedelta(seconds=ttl_seconds)).isoformat(),
80
+ "status": "pending",
81
+ }
82
+
83
+ if fields_needed:
84
+ record["fields_needed"] = fields_needed
85
+
86
+ path = self._request_path(request_id)
87
+ with open(path, "w") as f:
88
+ yaml.dump(record, f, default_flow_style=False, sort_keys=False)
89
+
90
+ logger.info(
91
+ f"Approval request {request_id} created: {agent_id} wants to {action} "
92
+ f"{resource_uri} — {purpose}"
93
+ )
94
+ return request_id
95
+
96
+ def decide(
97
+ self,
98
+ request_id: str,
99
+ decision: str,
100
+ decided_by: str,
101
+ reason: str = "",
102
+ ) -> bool:
103
+ """
104
+ Approve or deny a pending request.
105
+
106
+ Args:
107
+ request_id: The approval request ID
108
+ decision: "approved" or "denied"
109
+ decided_by: Who made the decision (e.g., "erik", "telegram")
110
+ reason: Optional reason for the decision
111
+
112
+ Returns:
113
+ True if decision was recorded, False if request not found or already decided
114
+ """
115
+ path = self._request_path(request_id)
116
+ if not path.exists():
117
+ logger.warning(f"Approval request {request_id} not found")
118
+ return False
119
+
120
+ with open(path) as f:
121
+ record = yaml.safe_load(f)
122
+
123
+ if record.get("status") != "pending":
124
+ logger.warning(
125
+ f"Approval request {request_id} already decided: {record.get('status')}"
126
+ )
127
+ return False
128
+
129
+ # Check expiry
130
+ expires_at = datetime.fromisoformat(record["expires_at"])
131
+ if datetime.now(timezone.utc) > expires_at:
132
+ record["status"] = "expired"
133
+ with open(path, "w") as f:
134
+ yaml.dump(record, f, default_flow_style=False, sort_keys=False)
135
+ logger.warning(f"Approval request {request_id} has expired")
136
+ return False
137
+
138
+ now = datetime.now(timezone.utc)
139
+ record["status"] = decision
140
+ record["decided_at"] = now.isoformat()
141
+ record["decided_by"] = decided_by
142
+ record["decision_reason"] = reason
143
+
144
+ with open(path, "w") as f:
145
+ yaml.dump(record, f, default_flow_style=False, sort_keys=False)
146
+
147
+ logger.info(f"Approval request {request_id} {decision} by {decided_by}: {reason}")
148
+ return True
149
+
150
+ def check(self, request_id: str) -> Optional[dict]:
151
+ """
152
+ Check the status of an approval request.
153
+
154
+ Returns:
155
+ Record dict if found, None otherwise. Auto-expires stale requests.
156
+ """
157
+ path = self._request_path(request_id)
158
+ if not path.exists():
159
+ return None
160
+
161
+ with open(path) as f:
162
+ record = yaml.safe_load(f)
163
+
164
+ # Auto-expire
165
+ if record.get("status") == "pending":
166
+ expires_at = datetime.fromisoformat(record["expires_at"])
167
+ if datetime.now(timezone.utc) > expires_at:
168
+ record["status"] = "expired"
169
+ with open(path, "w") as f:
170
+ yaml.dump(record, f, default_flow_style=False, sort_keys=False)
171
+
172
+ return record
173
+
174
+ def is_approved(self, request_id: str) -> bool:
175
+ """Check if a specific request has been approved."""
176
+ record = self.check(request_id)
177
+ return record is not None and record.get("status") == "approved"
178
+
179
+ def find_approved(
180
+ self,
181
+ agent_id: str,
182
+ action: str,
183
+ resource_uri: str,
184
+ ) -> Optional[dict]:
185
+ """
186
+ Find an existing approved request that covers this access.
187
+
188
+ Checks for a non-expired approved request matching the agent, action, and resource.
189
+ This allows agents to retry after approval without needing the original request ID.
190
+
191
+ Returns:
192
+ Approved record dict if found, None otherwise.
193
+ """
194
+ now = datetime.now(timezone.utc)
195
+ for path in self.approvals_dir.glob("*.yaml"):
196
+ try:
197
+ with open(path) as f:
198
+ record = yaml.safe_load(f)
199
+ if (
200
+ record.get("status") == "approved"
201
+ and record.get("agent_id") == agent_id
202
+ and record.get("action") == action
203
+ and record.get("resource_uri") == resource_uri
204
+ ):
205
+ expires_at = datetime.fromisoformat(record["expires_at"])
206
+ if now <= expires_at:
207
+ return record
208
+ except Exception:
209
+ continue
210
+ return None
211
+
212
+ def list_pending(self) -> list[dict]:
213
+ """List all pending (non-expired) approval requests."""
214
+ pending = []
215
+ now = datetime.now(timezone.utc)
216
+ for path in sorted(self.approvals_dir.glob("*.yaml")):
217
+ try:
218
+ with open(path) as f:
219
+ record = yaml.safe_load(f)
220
+ if record.get("status") == "pending":
221
+ expires_at = datetime.fromisoformat(record["expires_at"])
222
+ if now <= expires_at:
223
+ pending.append(record)
224
+ else:
225
+ # Auto-expire
226
+ record["status"] = "expired"
227
+ with open(path, "w") as f:
228
+ yaml.dump(record, f, default_flow_style=False, sort_keys=False)
229
+ except Exception:
230
+ continue
231
+ return pending
232
+
233
+ def list_history(self, limit: int = 50) -> list[dict]:
234
+ """List recent approval decisions (approved, denied, expired)."""
235
+ history = []
236
+ for path in sorted(self.approvals_dir.glob("*.yaml"), reverse=True):
237
+ if len(history) >= limit:
238
+ break
239
+ try:
240
+ with open(path) as f:
241
+ record = yaml.safe_load(f)
242
+ if record.get("status") != "pending":
243
+ history.append(record)
244
+ except Exception:
245
+ continue
246
+ return history
247
+
248
+ def expire_stale(self) -> int:
249
+ """Mark all expired pending requests. Returns count of newly expired."""
250
+ count = 0
251
+ now = datetime.now(timezone.utc)
252
+ for path in self.approvals_dir.glob("*.yaml"):
253
+ try:
254
+ with open(path) as f:
255
+ record = yaml.safe_load(f)
256
+ if record.get("status") == "pending":
257
+ expires_at = datetime.fromisoformat(record["expires_at"])
258
+ if now > expires_at:
259
+ record["status"] = "expired"
260
+ with open(path, "w") as f:
261
+ yaml.dump(record, f, default_flow_style=False, sort_keys=False)
262
+ count += 1
263
+ except Exception:
264
+ continue
265
+ if count:
266
+ logger.info(f"Expired {count} stale approval requests")
267
+ return count
@@ -0,0 +1,16 @@
1
+ """
2
+ Backend adapters for Authority Runtime.
3
+
4
+ Backends handle authentication and policy evaluation for different data stores.
5
+ """
6
+
7
+ from .slos import SlosBackend, Decision, PolicyResult, DocumentMetadata
8
+ from .memory import MemoryBackend
9
+
10
+ __all__ = [
11
+ "SlosBackend",
12
+ "MemoryBackend",
13
+ "Decision",
14
+ "PolicyResult",
15
+ "DocumentMetadata",
16
+ ]