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.
- authority_runtime/__init__.py +139 -0
- authority_runtime/approvals.py +267 -0
- authority_runtime/backends/__init__.py +16 -0
- authority_runtime/backends/memory.py +296 -0
- authority_runtime/backends/slos.py +555 -0
- authority_runtime/cli.py +1405 -0
- authority_runtime/compiler.py +719 -0
- authority_runtime/compliance.py +572 -0
- authority_runtime/constraints.py +124 -0
- authority_runtime/crypto.py +212 -0
- authority_runtime/enforce.py +590 -0
- authority_runtime/envelope.py +575 -0
- authority_runtime/keys.py +212 -0
- authority_runtime/langgraph.py +407 -0
- authority_runtime/logging_config.py +71 -0
- authority_runtime/mcp_server.py +1431 -0
- authority_runtime/policy.py +245 -0
- authority_runtime/roles.py +539 -0
- authority_runtime/shell.py +489 -0
- authority_runtime/storage.py +970 -0
- authority_runtime/types.py +281 -0
- authority_runtime/validation.py +428 -0
- authority_runtime/vault_scope.py +175 -0
- authority_runtime-0.3.0.dist-info/METADATA +220 -0
- authority_runtime-0.3.0.dist-info/RECORD +29 -0
- authority_runtime-0.3.0.dist-info/WHEEL +5 -0
- authority_runtime-0.3.0.dist-info/entry_points.txt +2 -0
- authority_runtime-0.3.0.dist-info/licenses/LICENSE +89 -0
- authority_runtime-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|