agent-filetree-memory-mcp 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.
Files changed (50) hide show
  1. agent_filetree_memory/__init__.py +23 -0
  2. agent_filetree_memory/application/__init__.py +5 -0
  3. agent_filetree_memory/application/service.py +291 -0
  4. agent_filetree_memory/auth/__init__.py +5 -0
  5. agent_filetree_memory/auth/capabilities.py +426 -0
  6. agent_filetree_memory/cli.py +266 -0
  7. agent_filetree_memory/control_plane/__init__.py +65 -0
  8. agent_filetree_memory/control_plane/api.py +847 -0
  9. agent_filetree_memory/control_plane/management_store.py +2504 -0
  10. agent_filetree_memory/control_plane/namespace_store.py +1278 -0
  11. agent_filetree_memory/crypto/__init__.py +10 -0
  12. agent_filetree_memory/crypto/envelope.py +197 -0
  13. agent_filetree_memory/crypto/local_keyring.py +90 -0
  14. agent_filetree_memory/domain/__init__.py +5 -0
  15. agent_filetree_memory/domain/errors.py +41 -0
  16. agent_filetree_memory/domain/models.py +115 -0
  17. agent_filetree_memory/domain/paths.py +37 -0
  18. agent_filetree_memory/janitor_cli.py +119 -0
  19. agent_filetree_memory/mcp/__init__.py +9 -0
  20. agent_filetree_memory/mcp/payloads.py +106 -0
  21. agent_filetree_memory/mcp/server.py +449 -0
  22. agent_filetree_memory/mcp_app/__init__.py +5 -0
  23. agent_filetree_memory/mcp_app/app.py +1170 -0
  24. agent_filetree_memory/ports/__init__.py +14 -0
  25. agent_filetree_memory/ports/capabilities.py +22 -0
  26. agent_filetree_memory/ports/crypto.py +51 -0
  27. agent_filetree_memory/ports/store.py +80 -0
  28. agent_filetree_memory/postgres/__init__.py +30 -0
  29. agent_filetree_memory/postgres/janitor.py +263 -0
  30. agent_filetree_memory/postgres/migrations/README.md +16 -0
  31. agent_filetree_memory/postgres/migrations/__init__.py +149 -0
  32. agent_filetree_memory/postgres/migrations/versions/0001_encrypted_filetree.py +284 -0
  33. agent_filetree_memory/postgres/migrations/versions/0002_normalize_constraint_names.py +187 -0
  34. agent_filetree_memory/postgres/migrations/versions/0003_management_control_plane.py +275 -0
  35. agent_filetree_memory/postgres/migrations/versions/0004_workspace_authorization_policies.py +175 -0
  36. agent_filetree_memory/postgres/migrations/versions/__init__.py +1 -0
  37. agent_filetree_memory/postgres/runtime.py +125 -0
  38. agent_filetree_memory/postgres/schema.py +282 -0
  39. agent_filetree_memory/postgres/store.py +1592 -0
  40. agent_filetree_memory/web/__init__.py +15 -0
  41. agent_filetree_memory/web/application.py +65 -0
  42. agent_filetree_memory/web/dist/assets/index-BWOByJXH.js +37 -0
  43. agent_filetree_memory/web/dist/assets/index-Ca-eJNeX.css +1 -0
  44. agent_filetree_memory/web/dist/index.html +14 -0
  45. agent_filetree_memory/web/frontend.py +184 -0
  46. agent_filetree_memory_mcp-0.3.0.dist-info/METADATA +113 -0
  47. agent_filetree_memory_mcp-0.3.0.dist-info/RECORD +50 -0
  48. agent_filetree_memory_mcp-0.3.0.dist-info/WHEEL +4 -0
  49. agent_filetree_memory_mcp-0.3.0.dist-info/entry_points.txt +3 -0
  50. agent_filetree_memory_mcp-0.3.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,23 @@
1
+ """Encrypted, capability-scoped file-tree memory for agents."""
2
+
3
+ from .domain.models import (
4
+ DeleteResult,
5
+ DocumentSnapshot,
6
+ MemoryAction,
7
+ MemoryEntry,
8
+ Scope,
9
+ VerifiedInvocation,
10
+ WriteResult,
11
+ )
12
+
13
+ __all__ = [
14
+ "DeleteResult",
15
+ "DocumentSnapshot",
16
+ "MemoryAction",
17
+ "MemoryEntry",
18
+ "Scope",
19
+ "VerifiedInvocation",
20
+ "WriteResult",
21
+ ]
22
+
23
+ __version__ = "0.3.0"
@@ -0,0 +1,5 @@
1
+ """Authorization-first application operations."""
2
+
3
+ from .service import MemoryService
4
+
5
+ __all__ = ["MemoryService"]
@@ -0,0 +1,291 @@
1
+ """Authorization-first use cases for the memory tree."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import hashlib
7
+ import re
8
+ from collections.abc import Mapping
9
+ from typing import Sequence
10
+
11
+ from ..domain.errors import AuthorizationDenied
12
+ from ..domain.models import (
13
+ DeleteResult,
14
+ DocumentSnapshot,
15
+ MemoryAction,
16
+ MemoryEntry,
17
+ VerifiedInvocation,
18
+ WriteResult,
19
+ )
20
+ from ..domain.paths import normalize_memory_path
21
+ from ..ports.store import MemoryStore
22
+
23
+ _DEFAULT_MAX_CONTENT_BYTES = 1024 * 1024
24
+ _DEFAULT_MAX_APPEND_BYTES = 256 * 1024
25
+ _DEFAULT_MAX_IMPORT_DOCUMENTS = 1_000
26
+ _IDEMPOTENCY_KEY = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._~:-]{0,254}$")
27
+
28
+
29
+ class MemoryService:
30
+ """Perform memory operations within one already-verified invocation.
31
+
32
+ The invocation is authorized before request validation and, critically,
33
+ before the persistence port is called. A transport adapter must never
34
+ accept scope fields from a model or UI and construct an invocation itself.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ store: MemoryStore,
40
+ *,
41
+ max_content_bytes: int = _DEFAULT_MAX_CONTENT_BYTES,
42
+ max_append_bytes: int = _DEFAULT_MAX_APPEND_BYTES,
43
+ max_import_documents: int = _DEFAULT_MAX_IMPORT_DOCUMENTS,
44
+ logger: logging.Logger | None = None,
45
+ ) -> None:
46
+ if not isinstance(max_content_bytes, int) or isinstance(
47
+ max_content_bytes, bool
48
+ ):
49
+ raise ValueError("max_content_bytes must be a positive integer")
50
+ if not isinstance(max_append_bytes, int) or isinstance(max_append_bytes, bool):
51
+ raise ValueError("max_append_bytes must be a positive integer")
52
+ if max_content_bytes <= 0 or max_append_bytes <= 0:
53
+ raise ValueError("content limits must be positive")
54
+ if max_append_bytes > max_content_bytes:
55
+ raise ValueError("append limit cannot exceed the document limit")
56
+ if (
57
+ not isinstance(max_import_documents, int)
58
+ or isinstance(max_import_documents, bool)
59
+ or max_import_documents <= 0
60
+ ):
61
+ raise ValueError("max_import_documents must be a positive integer")
62
+ self._store = store
63
+ self._max_content_bytes = max_content_bytes
64
+ self._max_append_bytes = max_append_bytes
65
+ self._max_import_documents = max_import_documents
66
+ self._logger = logger or logging.getLogger(__name__)
67
+
68
+ async def list(
69
+ self, invocation: VerifiedInvocation, path: str = "/"
70
+ ) -> Sequence[MemoryEntry]:
71
+ self._authorize(invocation, MemoryAction.LIST)
72
+ normalized = normalize_memory_path(path)
73
+ result = await self._store.list(
74
+ invocation.scope,
75
+ normalized,
76
+ invocation_id=invocation.invocation_id,
77
+ principal_id=invocation.principal_id,
78
+ )
79
+ self._completed(MemoryAction.LIST)
80
+ return result
81
+
82
+ async def read(
83
+ self, invocation: VerifiedInvocation, path: str
84
+ ) -> DocumentSnapshot:
85
+ self._authorize(invocation, MemoryAction.READ)
86
+ normalized = normalize_memory_path(path, allow_root=False)
87
+ result = await self._store.read(
88
+ invocation.scope,
89
+ normalized,
90
+ invocation_id=invocation.invocation_id,
91
+ principal_id=invocation.principal_id,
92
+ )
93
+ self._completed(MemoryAction.READ)
94
+ return result
95
+
96
+ async def write(
97
+ self,
98
+ invocation: VerifiedInvocation,
99
+ path: str,
100
+ content: str,
101
+ *,
102
+ expected_version: int | None = None,
103
+ idempotency_key: str,
104
+ ) -> WriteResult:
105
+ self._authorize(invocation, MemoryAction.WRITE)
106
+ normalized = normalize_memory_path(path, allow_root=False)
107
+ validated_content = self._validate_content(
108
+ content, max_bytes=self._max_content_bytes, allow_empty=True
109
+ )
110
+ self._validate_expected_version(expected_version, allow_none=True)
111
+ validated_key = self._validate_idempotency_key(idempotency_key)
112
+ result = await self._store.write(
113
+ invocation.scope,
114
+ normalized,
115
+ validated_content,
116
+ expected_version=expected_version,
117
+ idempotency_key=validated_key,
118
+ invocation_id=invocation.invocation_id,
119
+ principal_id=invocation.principal_id,
120
+ )
121
+ self._completed(MemoryAction.WRITE)
122
+ return result
123
+
124
+ async def append(
125
+ self,
126
+ invocation: VerifiedInvocation,
127
+ path: str,
128
+ content: str,
129
+ *,
130
+ expected_version: int,
131
+ idempotency_key: str,
132
+ ) -> WriteResult:
133
+ self._authorize(invocation, MemoryAction.APPEND)
134
+ normalized = normalize_memory_path(path, allow_root=False)
135
+ validated_content = self._validate_content(
136
+ content, max_bytes=self._max_append_bytes, allow_empty=False
137
+ )
138
+ self._validate_expected_version(expected_version, allow_none=False)
139
+ validated_key = self._validate_idempotency_key(idempotency_key)
140
+ result = await self._store.append(
141
+ invocation.scope,
142
+ normalized,
143
+ validated_content,
144
+ expected_version=expected_version,
145
+ idempotency_key=validated_key,
146
+ invocation_id=invocation.invocation_id,
147
+ principal_id=invocation.principal_id,
148
+ )
149
+ self._completed(MemoryAction.APPEND)
150
+ return result
151
+
152
+ async def delete(
153
+ self,
154
+ invocation: VerifiedInvocation,
155
+ path: str,
156
+ *,
157
+ expected_version: int,
158
+ idempotency_key: str,
159
+ ) -> DeleteResult:
160
+ self._authorize(invocation, MemoryAction.DELETE)
161
+ normalized = normalize_memory_path(path, allow_root=False)
162
+ self._validate_expected_version(expected_version, allow_none=False)
163
+ validated_key = self._validate_idempotency_key(idempotency_key)
164
+ result = await self._store.delete(
165
+ invocation.scope,
166
+ normalized,
167
+ expected_version=expected_version,
168
+ idempotency_key=validated_key,
169
+ invocation_id=invocation.invocation_id,
170
+ principal_id=invocation.principal_id,
171
+ )
172
+ self._completed(MemoryAction.DELETE)
173
+ return result
174
+
175
+ async def export(
176
+ self, invocation: VerifiedInvocation, path: str = "/"
177
+ ) -> Sequence[DocumentSnapshot]:
178
+ self._authorize(invocation, MemoryAction.EXPORT)
179
+ normalized = normalize_memory_path(path)
180
+ result = await self._store.export_markdown_tree(
181
+ invocation.scope,
182
+ normalized,
183
+ invocation_id=invocation.invocation_id,
184
+ principal_id=invocation.principal_id,
185
+ )
186
+ self._completed(MemoryAction.EXPORT)
187
+ return result
188
+
189
+ async def import_markdown_tree(
190
+ self,
191
+ invocation: VerifiedInvocation,
192
+ documents: Mapping[str, str],
193
+ *,
194
+ idempotency_namespace: str,
195
+ ) -> Sequence[WriteResult]:
196
+ """Create a portable Markdown tree through retry-safe package writes.
197
+
198
+ The import is intentionally create-only. Every path/content pair is
199
+ normalized and validated before the first database call. Each document
200
+ then receives a stable, opaque idempotency key derived from the caller's
201
+ namespace and normalized path, so an interrupted import can be retried
202
+ safely. Hosts that need an all-or-nothing bulk transaction should build
203
+ that policy above the persistence port.
204
+ """
205
+
206
+ self._authorize(invocation, MemoryAction.IMPORT)
207
+ namespace = self._validate_idempotency_key(idempotency_namespace)
208
+ if not isinstance(documents, Mapping) or not documents:
209
+ raise ValueError("documents must be a non-empty path-to-content mapping")
210
+ if len(documents) > self._max_import_documents:
211
+ raise ValueError("documents exceed the configured import limit")
212
+
213
+ validated: dict[str, str] = {}
214
+ for path, content in documents.items():
215
+ normalized = normalize_memory_path(path, allow_root=False)
216
+ if normalized in validated:
217
+ raise ValueError("documents contain duplicate normalized paths")
218
+ validated[normalized] = self._validate_content(
219
+ content,
220
+ max_bytes=self._max_content_bytes,
221
+ allow_empty=True,
222
+ )
223
+
224
+ results: list[WriteResult] = []
225
+ for path in sorted(validated):
226
+ digest = hashlib.sha256(
227
+ b"agent-filetree-memory-import-v1\x00"
228
+ + namespace.encode("ascii")
229
+ + b"\x00"
230
+ + path.encode("utf-8")
231
+ ).hexdigest()
232
+ results.append(
233
+ await self._store.write(
234
+ invocation.scope,
235
+ path,
236
+ validated[path],
237
+ expected_version=None,
238
+ idempotency_key=digest,
239
+ invocation_id=invocation.invocation_id,
240
+ principal_id=invocation.principal_id,
241
+ )
242
+ )
243
+ self._completed(MemoryAction.IMPORT)
244
+ return tuple(results)
245
+
246
+ @staticmethod
247
+ def _authorize(
248
+ invocation: VerifiedInvocation, required_action: MemoryAction
249
+ ) -> None:
250
+ if not isinstance(invocation, VerifiedInvocation) or not isinstance(
251
+ invocation.allowed_actions, frozenset
252
+ ):
253
+ raise AuthorizationDenied("memory operation is not authorized")
254
+ invocation.require(required_action)
255
+
256
+ @staticmethod
257
+ def _validate_content(
258
+ content: str, *, max_bytes: int, allow_empty: bool
259
+ ) -> str:
260
+ if not isinstance(content, str) or "\x00" in content:
261
+ raise ValueError("content must be valid text within configured limits")
262
+ try:
263
+ encoded = content.encode("utf-8")
264
+ except UnicodeEncodeError:
265
+ raise ValueError(
266
+ "content must be valid text within configured limits"
267
+ ) from None
268
+ if (not allow_empty and not encoded) or len(encoded) > max_bytes:
269
+ raise ValueError("content must be valid text within configured limits")
270
+ return content
271
+
272
+ @staticmethod
273
+ def _validate_expected_version(
274
+ version: int | None, *, allow_none: bool
275
+ ) -> None:
276
+ if version is None and allow_none:
277
+ return
278
+ if not isinstance(version, int) or isinstance(version, bool) or version < 1:
279
+ raise ValueError("expected_version must be a positive integer")
280
+
281
+ @staticmethod
282
+ def _validate_idempotency_key(value: str) -> str:
283
+ if not isinstance(value, str) or not _IDEMPOTENCY_KEY.fullmatch(value):
284
+ raise ValueError("idempotency_key must be an opaque identifier")
285
+ return value
286
+
287
+ def _completed(self, action: MemoryAction) -> None:
288
+ # Deliberately omit paths, scopes, idempotency keys, and document data.
289
+ self._logger.info(
290
+ "memory operation completed", extra={"memory_action": action.value}
291
+ )
@@ -0,0 +1,5 @@
1
+ """Asymmetric capability issuance and verification."""
2
+
3
+ from .capabilities import AsymmetricCapabilityVerifier, LocalTestCapabilityIssuer
4
+
5
+ __all__ = ["AsymmetricCapabilityVerifier", "LocalTestCapabilityIssuer"]