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,296 @@
1
+ """
2
+ In-Memory Backend for Authority Runtime
3
+
4
+ Provides a standalone backend that stores documents in Python dicts,
5
+ enabling Carryall to run without any external data store (SLOS, S3, etc.).
6
+
7
+ Use cases:
8
+ - Quick demos and pilots
9
+ - Testing and CI
10
+ - Environments where SLOS is not available
11
+
12
+ Implements the same interface as SlosBackend so it can be used as a drop-in
13
+ replacement via CarryallMCPServer(backend=MemoryBackend(...)).
14
+ """
15
+
16
+ import uuid
17
+ from datetime import datetime, timezone
18
+ from typing import Any, Optional
19
+
20
+ from .slos import Decision, PolicyResult, DocumentMetadata, parse_slos_uri
21
+
22
+
23
+ class MemoryBackend:
24
+ """
25
+ In-memory document store with policy evaluation.
26
+
27
+ Documents are organized by vault (domain). Each document has content,
28
+ metadata, and access control fields (allowed_agents, denied_agents,
29
+ requires_approval) that mirror SLOS document frontmatter.
30
+
31
+ Example:
32
+ ```python
33
+ backend = MemoryBackend(initial_data={
34
+ "finance": {
35
+ "budget-2026": {
36
+ "content": "# Q1 Budget\\nTotal: $500k",
37
+ "title": "Q1 2026 Budget",
38
+ "sensitivity": "confidential",
39
+ "allowed_agents": ["finance-agent"],
40
+ "denied_agents": ["startup-agent"],
41
+ }
42
+ }
43
+ })
44
+
45
+ # List vaults
46
+ vaults = backend.list_vaults("finance-agent")
47
+ # ["finance"]
48
+
49
+ # Check access
50
+ result = backend.check_access(envelope, "read", "slos://vaults/finance/budget-2026")
51
+ # PolicyResult(decision=Decision.ALLOW, ...)
52
+ ```
53
+ """
54
+
55
+ def __init__(self, initial_data: Optional[dict] = None):
56
+ """
57
+ Initialize in-memory backend.
58
+
59
+ Args:
60
+ initial_data: Optional dict of {vault: {doc_id: {content, title, ...}}}
61
+ Each document can have:
62
+ - content (str): Document content (default: "")
63
+ - title (str): Document title (default: doc_id)
64
+ - sensitivity (str): internal|confidential|restricted (default: "internal")
65
+ - allowed_agents (list[str]): Agents with explicit access (default: [])
66
+ - denied_agents (list[str]): Agents explicitly denied (default: [])
67
+ - requires_approval (list[str]): Agents needing approval (default: [])
68
+ - data_type (str): note|profile|budget|etc. (default: "note")
69
+ - subpath (str): Subdirectory within vault (default: "")
70
+ """
71
+ # {vault_name: {doc_id: doc_dict}}
72
+ self._vaults: dict[str, dict[str, dict]] = {}
73
+
74
+ if initial_data:
75
+ for vault, documents in initial_data.items():
76
+ self._vaults[vault] = {}
77
+ for doc_id, doc_data in documents.items():
78
+ self._vaults[vault][doc_id] = {
79
+ "content": doc_data.get("content", ""),
80
+ "title": doc_data.get("title", doc_id),
81
+ "sensitivity": doc_data.get("sensitivity", "internal"),
82
+ "allowed_agents": doc_data.get("allowed_agents", []),
83
+ "denied_agents": doc_data.get("denied_agents", []),
84
+ "requires_approval": doc_data.get("requires_approval", []),
85
+ "data_type": doc_data.get("data_type", "note"),
86
+ "subpath": doc_data.get("subpath", ""),
87
+ "created_at": doc_data.get(
88
+ "created_at",
89
+ datetime.now(timezone.utc).isoformat(),
90
+ ),
91
+ "updated_at": datetime.now(timezone.utc).isoformat(),
92
+ }
93
+
94
+ def list_vaults(self, agent_id: str = "executive-agent", mock: bool = False) -> list[str]:
95
+ """List available vaults."""
96
+ return list(self._vaults.keys())
97
+
98
+ def list_resources(self, vault: str, agent_id: str, mock: bool = False) -> list[dict]:
99
+ """List documents in a vault."""
100
+ if vault not in self._vaults:
101
+ return []
102
+ return [
103
+ {"id": doc_id, "title": doc.get("title", doc_id)}
104
+ for doc_id, doc in self._vaults[vault].items()
105
+ ]
106
+
107
+ def get_metadata(self, uri: str, agent_id: str, mock: bool = False) -> DocumentMetadata:
108
+ """Get document metadata including access policies."""
109
+ vault, doc_id = parse_slos_uri(uri)
110
+
111
+ if vault not in self._vaults or doc_id not in self._vaults[vault]:
112
+ # Return empty metadata for missing documents (same as SLOS behavior)
113
+ return DocumentMetadata(
114
+ uri=uri,
115
+ id=doc_id,
116
+ domain=[vault],
117
+ sensitivity="unknown",
118
+ allowed_agents=[],
119
+ denied_agents=[],
120
+ requires_approval=[],
121
+ )
122
+
123
+ doc = self._vaults[vault][doc_id]
124
+ return DocumentMetadata(
125
+ uri=uri,
126
+ id=doc_id,
127
+ domain=[vault],
128
+ sensitivity=doc.get("sensitivity", "internal"),
129
+ allowed_agents=doc.get("allowed_agents", []),
130
+ denied_agents=doc.get("denied_agents", []),
131
+ requires_approval=doc.get("requires_approval", []),
132
+ )
133
+
134
+ def check_access(
135
+ self,
136
+ envelope: Any,
137
+ action: str,
138
+ uri: str,
139
+ mock: bool = False,
140
+ ) -> PolicyResult:
141
+ """
142
+ Policy evaluation — same priority chain as SlosBackend.
143
+
144
+ 1. Explicit deny (denied_agents)
145
+ 2. Requires approval (requires_approval)
146
+ 3. Explicit allow (allowed_agents)
147
+ 4. Scope-based allow (envelope scopes)
148
+ 5. Default deny
149
+ """
150
+ agent_id = envelope.agent_id
151
+
152
+ metadata = self.get_metadata(uri, agent_id)
153
+
154
+ # 1. Explicit deny
155
+ if agent_id in metadata.denied_agents:
156
+ return PolicyResult(
157
+ decision=Decision.DENY,
158
+ reason=f"Agent '{agent_id}' explicitly denied by document",
159
+ metadata={"uri": uri, "rule": "denied_agents"},
160
+ )
161
+
162
+ # 2. Requires approval
163
+ if agent_id in metadata.requires_approval:
164
+ return PolicyResult(
165
+ decision=Decision.REQUIRE_APPROVAL,
166
+ reason=f"Agent '{agent_id}' requires approval for this document",
167
+ metadata={"uri": uri, "rule": "requires_approval"},
168
+ )
169
+
170
+ # 3. Explicit allow
171
+ if metadata.allowed_agents and agent_id in metadata.allowed_agents:
172
+ return PolicyResult(
173
+ decision=Decision.ALLOW,
174
+ reason=f"Agent '{agent_id}' explicitly allowed by document",
175
+ metadata={"uri": uri, "rule": "allowed_agents"},
176
+ )
177
+
178
+ # 4. Scope-based allow
179
+ vault, _ = parse_slos_uri(uri)
180
+ required_scope = f"vault:{vault}:{action}"
181
+
182
+ if required_scope in envelope.authority.scopes:
183
+ return PolicyResult(
184
+ decision=Decision.ALLOW,
185
+ reason=f"Envelope has scope '{required_scope}'",
186
+ metadata={"uri": uri, "rule": "envelope_scope", "scope": required_scope},
187
+ )
188
+
189
+ # Wildcard scope
190
+ wildcard_scope = f"vault:{vault}:*"
191
+ if wildcard_scope in envelope.authority.scopes:
192
+ return PolicyResult(
193
+ decision=Decision.ALLOW,
194
+ reason=f"Envelope has wildcard scope '{wildcard_scope}'",
195
+ metadata={"uri": uri, "rule": "envelope_scope", "scope": wildcard_scope},
196
+ )
197
+
198
+ # 5. Default deny
199
+ return PolicyResult(
200
+ decision=Decision.DENY,
201
+ reason=f"No permission for {vault}:{action}",
202
+ metadata={"uri": uri, "required_scope": required_scope},
203
+ )
204
+
205
+ def read_document(self, document_id: str, purpose: str, agent_id: str, mock: bool = False) -> dict:
206
+ """Read document content by ID (searches all vaults)."""
207
+ for vault_name, docs in self._vaults.items():
208
+ if document_id in docs:
209
+ doc = docs[document_id]
210
+ return {
211
+ "id": document_id,
212
+ "vault": vault_name,
213
+ "content": doc.get("content", ""),
214
+ "title": doc.get("title", document_id),
215
+ "sensitivity": doc.get("sensitivity", "internal"),
216
+ "purpose": purpose,
217
+ }
218
+
219
+ return {"error": f"Document '{document_id}' not found", "id": document_id}
220
+
221
+ def write_document(
222
+ self,
223
+ domain: str,
224
+ content: str,
225
+ metadata: dict,
226
+ agent_id: str,
227
+ document_id: str = None,
228
+ mock: bool = False,
229
+ ) -> dict:
230
+ """Write/create a document in a vault."""
231
+ if domain not in self._vaults:
232
+ self._vaults[domain] = {}
233
+
234
+ doc_id = document_id or str(uuid.uuid4())
235
+ now = datetime.now(timezone.utc).isoformat()
236
+
237
+ self._vaults[domain][doc_id] = {
238
+ "content": content,
239
+ "title": metadata.get("title", doc_id),
240
+ "sensitivity": metadata.get("sensitivity", "internal"),
241
+ "allowed_agents": metadata.get("allowed_agents", []),
242
+ "denied_agents": metadata.get("denied_agents", []),
243
+ "requires_approval": metadata.get("requires_approval", []),
244
+ "data_type": metadata.get("data_type", "note"),
245
+ "subpath": metadata.get("subpath", ""),
246
+ "created_at": now,
247
+ "updated_at": now,
248
+ }
249
+
250
+ return {
251
+ "id": doc_id,
252
+ "domain": domain,
253
+ "created_at": now,
254
+ "status": "created" if not document_id else "updated",
255
+ }
256
+
257
+ def query_documents(
258
+ self,
259
+ domain: str,
260
+ query: str,
261
+ agent_id: str,
262
+ include_content: bool = False,
263
+ limit: int = 10,
264
+ mock: bool = False,
265
+ ) -> dict:
266
+ """Search documents within a vault domain."""
267
+ if domain not in self._vaults:
268
+ return {"domain": domain, "query": query, "results": [], "total": 0}
269
+
270
+ query_lower = query.lower()
271
+ results = []
272
+
273
+ for doc_id, doc in self._vaults[domain].items():
274
+ # Simple text search across title and content
275
+ title = doc.get("title", "").lower()
276
+ content = doc.get("content", "").lower()
277
+
278
+ if query_lower in title or query_lower in content:
279
+ result = {
280
+ "id": doc_id,
281
+ "title": doc.get("title", doc_id),
282
+ "sensitivity": doc.get("sensitivity", "internal"),
283
+ }
284
+ if include_content:
285
+ result["content"] = doc.get("content", "")
286
+ results.append(result)
287
+
288
+ if len(results) >= limit:
289
+ break
290
+
291
+ return {
292
+ "domain": domain,
293
+ "query": query,
294
+ "results": results,
295
+ "total": len(results),
296
+ }