rememberstack 0.1.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 (186) hide show
  1. rememberstack/__init__.py +9 -0
  2. rememberstack/adapters/__init__.py +42 -0
  3. rememberstack/adapters/codex_writer.py +221 -0
  4. rememberstack/adapters/markitdown_converter.py +42 -0
  5. rememberstack/adapters/openrouter.py +136 -0
  6. rememberstack/adapters/selfhost/__init__.py +54 -0
  7. rememberstack/adapters/selfhost/forget.py +66 -0
  8. rememberstack/adapters/selfhost/git.py +374 -0
  9. rememberstack/adapters/selfhost/lance.py +328 -0
  10. rememberstack/adapters/selfhost/minio.py +279 -0
  11. rememberstack/adapters/selfhost/mounts.py +249 -0
  12. rememberstack/adapters/selfhost/object_store.py +130 -0
  13. rememberstack/adapters/selfhost/projection.py +80 -0
  14. rememberstack/adapters/selfhost/queue.py +137 -0
  15. rememberstack/adapters/selfhost/telemetry.py +45 -0
  16. rememberstack/adapters/selfhost/watcher.py +70 -0
  17. rememberstack/adapters/testing/__init__.py +15 -0
  18. rememberstack/adapters/testing/cost_meter.py +13 -0
  19. rememberstack/adapters/testing/model_provider.py +83 -0
  20. rememberstack/adapters/testing/queue.py +43 -0
  21. rememberstack/adapters/testing/telemetry.py +22 -0
  22. rememberstack/client.py +19 -0
  23. rememberstack/core/__init__.py +127 -0
  24. rememberstack/core/blockizer.py +189 -0
  25. rememberstack/core/chunker.py +216 -0
  26. rememberstack/core/consumption_skill.py +275 -0
  27. rememberstack/core/conversion.py +76 -0
  28. rememberstack/core/core_manifest.py +598 -0
  29. rememberstack/core/extension_packs.py +124 -0
  30. rememberstack/core/forget.py +17 -0
  31. rememberstack/core/knowledge_authored.py +276 -0
  32. rememberstack/core/knowledge_compile.py +215 -0
  33. rememberstack/core/knowledge_fact_sheet.py +210 -0
  34. rememberstack/core/knowledge_hashing.py +68 -0
  35. rememberstack/core/knowledge_planner.py +64 -0
  36. rememberstack/core/knowledge_writer.py +175 -0
  37. rememberstack/core/ranking.py +200 -0
  38. rememberstack/core/recipe_linter.py +149 -0
  39. rememberstack/core/section_snap.py +209 -0
  40. rememberstack/core/storage_routing.py +27 -0
  41. rememberstack/eval/__init__.py +53 -0
  42. rememberstack/eval/consumption.py +141 -0
  43. rememberstack/eval/contradiction.py +184 -0
  44. rememberstack/eval/harness.py +136 -0
  45. rememberstack/eval/lifecycle.py +400 -0
  46. rememberstack/eval/operational_scale.py +49 -0
  47. rememberstack/eval/resolution.py +255 -0
  48. rememberstack/eval/retrieval_spikes.py +50 -0
  49. rememberstack/eval/skeleton.py +231 -0
  50. rememberstack/llm/__init__.py +1 -0
  51. rememberstack/model/__init__.py +589 -0
  52. rememberstack/model/adjudication.py +100 -0
  53. rememberstack/model/auth.py +27 -0
  54. rememberstack/model/blocks.py +30 -0
  55. rememberstack/model/chunks.py +190 -0
  56. rememberstack/model/claims.py +162 -0
  57. rememberstack/model/client.py +98 -0
  58. rememberstack/model/clustering.py +54 -0
  59. rememberstack/model/component_version.py +124 -0
  60. rememberstack/model/consumption.py +88 -0
  61. rememberstack/model/conversion.py +31 -0
  62. rememberstack/model/deployment.py +53 -0
  63. rememberstack/model/documents.py +168 -0
  64. rememberstack/model/envelope.py +513 -0
  65. rememberstack/model/evaluation.py +72 -0
  66. rememberstack/model/forget.py +143 -0
  67. rememberstack/model/git.py +13 -0
  68. rememberstack/model/knowledge.py +840 -0
  69. rememberstack/model/knowledge_authored.py +325 -0
  70. rememberstack/model/knowledge_planner.py +431 -0
  71. rememberstack/model/lifecycle.py +42 -0
  72. rememberstack/model/model_provider.py +78 -0
  73. rememberstack/model/mounts.py +24 -0
  74. rememberstack/model/object_store.py +21 -0
  75. rememberstack/model/operational_scale.py +59 -0
  76. rememberstack/model/operations.py +153 -0
  77. rememberstack/model/processing.py +228 -0
  78. rememberstack/model/queue.py +73 -0
  79. rememberstack/model/recipes.py +83 -0
  80. rememberstack/model/relations.py +79 -0
  81. rememberstack/model/resolution.py +83 -0
  82. rememberstack/model/retrieval_spikes.py +62 -0
  83. rememberstack/model/sections.py +120 -0
  84. rememberstack/model/telemetry.py +30 -0
  85. rememberstack/ports/__init__.py +29 -0
  86. rememberstack/ports/auth.py +16 -0
  87. rememberstack/ports/connector.py +23 -0
  88. rememberstack/ports/cost_meter.py +17 -0
  89. rememberstack/ports/forget.py +20 -0
  90. rememberstack/ports/git.py +20 -0
  91. rememberstack/ports/model_provider.py +28 -0
  92. rememberstack/ports/mounts.py +16 -0
  93. rememberstack/ports/object_store.py +27 -0
  94. rememberstack/ports/p1_index.py +92 -0
  95. rememberstack/ports/purge.py +93 -0
  96. rememberstack/ports/queue.py +23 -0
  97. rememberstack/ports/telemetry.py +21 -0
  98. rememberstack/profiles/__init__.py +22 -0
  99. rememberstack/profiles/selfhost.py +324 -0
  100. rememberstack/profiles/selfhost_forget.py +158 -0
  101. rememberstack/profiles/selfhost_operations.py +95 -0
  102. rememberstack/py.typed +1 -0
  103. rememberstack/spine/__init__.py +93 -0
  104. rememberstack/spine/admission.py +26 -0
  105. rememberstack/spine/backfill.py +168 -0
  106. rememberstack/spine/catalog_contract.py +742 -0
  107. rememberstack/spine/chunk_catalog.py +237 -0
  108. rememberstack/spine/claim_catalog.py +298 -0
  109. rememberstack/spine/clustering.py +740 -0
  110. rememberstack/spine/component_versions.py +208 -0
  111. rememberstack/spine/consumption.py +81 -0
  112. rememberstack/spine/deployment_bootstrap.py +445 -0
  113. rememberstack/spine/document_catalog.py +621 -0
  114. rememberstack/spine/entity_registry.py +205 -0
  115. rememberstack/spine/extension_packs.py +220 -0
  116. rememberstack/spine/fact_catalog.py +571 -0
  117. rememberstack/spine/forget.py +1753 -0
  118. rememberstack/spine/knowledge.py +5467 -0
  119. rememberstack/spine/lifecycle.py +1071 -0
  120. rememberstack/spine/migrations/__init__.py +1 -0
  121. rememberstack/spine/migrations/_helpers.py +153 -0
  122. rememberstack/spine/migrations/env.py +58 -0
  123. rememberstack/spine/migrations/script.py.mako +27 -0
  124. rememberstack/spine/migrations/versions/__init__.py +1 -0
  125. rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
  126. rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
  127. rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
  128. rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
  129. rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
  130. rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
  131. rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
  132. rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
  133. rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
  134. rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
  135. rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
  136. rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
  137. rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
  138. rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
  139. rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
  140. rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
  141. rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
  142. rememberstack/spine/observation_adjudication.py +778 -0
  143. rememberstack/spine/operations.py +298 -0
  144. rememberstack/spine/projection.py +662 -0
  145. rememberstack/spine/recipes.py +276 -0
  146. rememberstack/spine/resolver.py +763 -0
  147. rememberstack/spine/review.py +650 -0
  148. rememberstack/spine/settings.py +22 -0
  149. rememberstack/spine/supersession.py +510 -0
  150. rememberstack/spine/sync.py +128 -0
  151. rememberstack/spine/work_ledger.py +816 -0
  152. rememberstack/surfaces/__init__.py +110 -0
  153. rememberstack/surfaces/cli.py +447 -0
  154. rememberstack/surfaces/consumption_skill.py +87 -0
  155. rememberstack/surfaces/graph_queries.py +698 -0
  156. rememberstack/surfaces/http_api.py +377 -0
  157. rememberstack/surfaces/mcp.py +67 -0
  158. rememberstack/surfaces/query_engine.py +1591 -0
  159. rememberstack/surfaces/recipe_executor.py +185 -0
  160. rememberstack/surfaces/recipe_surface.py +219 -0
  161. rememberstack/surfaces/remote_mcp.py +133 -0
  162. rememberstack/surfaces/sdk.py +324 -0
  163. rememberstack/workers/__init__.py +155 -0
  164. rememberstack/workers/base.py +312 -0
  165. rememberstack/workers/e0.py +577 -0
  166. rememberstack/workers/e1.py +425 -0
  167. rememberstack/workers/e2.py +525 -0
  168. rememberstack/workers/e3.py +434 -0
  169. rememberstack/workers/forget.py +299 -0
  170. rememberstack/workers/knowledge_authored.py +146 -0
  171. rememberstack/workers/knowledge_driver.py +735 -0
  172. rememberstack/workers/knowledge_fact_sheet.py +123 -0
  173. rememberstack/workers/knowledge_planner.py +325 -0
  174. rememberstack/workers/knowledge_writer.py +393 -0
  175. rememberstack/workers/operations.py +42 -0
  176. rememberstack/workers/p1.py +234 -0
  177. rememberstack/workers/p2.py +513 -0
  178. rememberstack/workers/p2_analytics.py +276 -0
  179. rememberstack/workers/p3.py +673 -0
  180. rememberstack/workers/reconcile.py +485 -0
  181. rememberstack/workers/sync.py +168 -0
  182. rememberstack-0.1.0.dist-info/METADATA +213 -0
  183. rememberstack-0.1.0.dist-info/RECORD +186 -0
  184. rememberstack-0.1.0.dist-info/WHEEL +4 -0
  185. rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
  186. rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,377 @@
1
+ """The HTTP API surface (WP-1.6/WP-5.4): primitives and recipes over FastAPI.
2
+
3
+ A thin, typed veneer: every endpoint delegates to one QueryEngine primitive or
4
+ runs one recipe through the shared `RecipeSurface`, and returns the D49
5
+ envelope verbatim. The recipe endpoints render from the registry — `/recipes`
6
+ IS the registry's active rows — so the CLI and MCP surfaces stay in lockstep
7
+ by construction (they render the same surface).
8
+
9
+ The API is the one place authorization is enforced for query-engine reads
10
+ (retrieval §9): a deployment that passes an `AuthPerimeterPort` gets every
11
+ endpoint gated on a valid perimeter credential for THIS deployment — a single
12
+ trust domain, never per-request tenancy. With no port, the perimeter is
13
+ infrastructure's job and the app is open (the self-host default). The surface
14
+ itself never touches adapters.
15
+ """
16
+
17
+ from datetime import datetime
18
+ from datetime import timedelta
19
+ from typing import Annotated
20
+ from typing import Literal
21
+ from typing import Protocol
22
+ from uuid import UUID
23
+
24
+ from fastapi import Body
25
+ from fastapi import Depends
26
+ from fastapi import FastAPI
27
+ from fastapi import Header
28
+ from fastapi import HTTPException
29
+ from fastapi import Query
30
+ from pydantic import SecretBytes
31
+
32
+ from rememberstack.model import AuthenticatedContext
33
+ from rememberstack.model import ConnectorCreate
34
+ from rememberstack.model import ConnectorDescriptor
35
+ from rememberstack.model import ConnectorNotFoundError
36
+ from rememberstack.model import DocumentUpload
37
+ from rememberstack.model import Envelope
38
+ from rememberstack.model import ForgetInProgressError
39
+ from rememberstack.model import IngestedVersion
40
+ from rememberstack.model import PerimeterCredential
41
+ from rememberstack.model import ToolDescriptor
42
+ from rememberstack.ports.auth import AuthPerimeterPort
43
+ from rememberstack.surfaces.query_engine import QueryEngine
44
+ from rememberstack.surfaces.query_engine import RESOLVE_CONTEXT_LIMIT
45
+ from rememberstack.surfaces.recipe_surface import InvalidArgumentError
46
+ from rememberstack.surfaces.recipe_surface import MissingArgumentError
47
+ from rememberstack.surfaces.recipe_surface import RecipeSurface
48
+ from rememberstack.surfaces.recipe_surface import UnknownRecipeError
49
+
50
+
51
+ class IngestPort(Protocol):
52
+ """The E0 ingest operations the HTTP surface may expose."""
53
+
54
+ def ingest(
55
+ self, *, deployment_id: UUID, upload: DocumentUpload
56
+ ) -> IngestedVersion: ...
57
+
58
+ def ingest_observed(
59
+ self,
60
+ *,
61
+ deployment_id: UUID,
62
+ source_kind: str,
63
+ source_ref: str,
64
+ upload: DocumentUpload,
65
+ versioning_mode: str,
66
+ source_modified_at: datetime | None,
67
+ source_version_ref: str | None,
68
+ sync_cycle_id: UUID | None,
69
+ ) -> IngestedVersion: ...
70
+
71
+
72
+ class ConnectorManagementPort(Protocol):
73
+ """Manage deployment-side connector configuration, never run it client-side."""
74
+
75
+ def connectors(self, *, deployment_id: UUID) -> tuple[ConnectorDescriptor, ...]: ...
76
+
77
+ def add(
78
+ self, *, deployment_id: UUID, connector: ConnectorCreate
79
+ ) -> ConnectorDescriptor: ...
80
+
81
+ def pause(
82
+ self, *, deployment_id: UUID, connector_id: UUID
83
+ ) -> ConnectorDescriptor: ...
84
+
85
+ def status(
86
+ self, *, deployment_id: UUID, connector_id: UUID
87
+ ) -> ConnectorDescriptor: ...
88
+
89
+
90
+ class AdmissionPort(Protocol):
91
+ """The deployment-wide fail-closed check applied before public traffic."""
92
+
93
+ def assert_available(self, *, deployment_id: UUID) -> None:
94
+ """Raise ``ForgetInProgressError`` while D74 admission is closed."""
95
+ ...
96
+
97
+
98
+ class ReadinessPort(Protocol):
99
+ """The mandatory restore replay completed before an API begins serving."""
100
+
101
+ def ensure_ready(self, *, deployment_id: UUID) -> tuple[UUID, ...]:
102
+ """Re-honor every portable forget manifest or raise fail-closed."""
103
+ ...
104
+
105
+
106
+ def build_api(
107
+ *,
108
+ engine: QueryEngine,
109
+ deployment_id: UUID,
110
+ admission: AdmissionPort,
111
+ readiness: ReadinessPort,
112
+ surface: RecipeSurface | None = None,
113
+ auth: AuthPerimeterPort | None = None,
114
+ ingest: IngestPort | None = None,
115
+ connectors: ConnectorManagementPort | None = None,
116
+ ) -> FastAPI:
117
+ """Build one deployment's query API over a composed engine.
118
+
119
+ `surface` adds registry-rendered recipes; `ingest` exposes the E0 write
120
+ gate; `connectors` manages deployment-side connector configuration; and
121
+ `auth` gates every endpoint on one perimeter credential. Each capability
122
+ is explicitly composed; absent services do not pretend to exist.
123
+ """
124
+ if surface is not None and surface.deployment_id != deployment_id:
125
+ raise ValueError(
126
+ "the recipe surface and the API serve different deployments —"
127
+ " one deployment is one trust domain (D50)"
128
+ )
129
+ readiness.ensure_ready(deployment_id=deployment_id)
130
+ dependencies = [
131
+ *(
132
+ [Depends(_perimeter(auth=auth, deployment_id=deployment_id))]
133
+ if auth is not None
134
+ else []
135
+ ),
136
+ Depends(_admission(admission=admission, deployment_id=deployment_id)),
137
+ ]
138
+ app = FastAPI(
139
+ title="RememberStack query API",
140
+ docs_url=None,
141
+ redoc_url=None,
142
+ openapi_url=None, # a machine API; the schema endpoint is not gated, so off
143
+ dependencies=dependencies,
144
+ )
145
+
146
+ @app.get("/resolve", response_model=Envelope)
147
+ def resolve(
148
+ name: str,
149
+ entity_type: str | None = None,
150
+ context_entity_ids: Annotated[
151
+ list[UUID] | None, Query(max_length=RESOLVE_CONTEXT_LIMIT)
152
+ ] = None,
153
+ ) -> Envelope:
154
+ """Resolve current entities, optionally ranked by focal context (S51)."""
155
+ return engine.resolve(
156
+ deployment_id=deployment_id,
157
+ name=name,
158
+ entity_type=entity_type,
159
+ context_entity_ids=tuple(context_entity_ids or ()),
160
+ )
161
+
162
+ @app.get("/lookup/relations", response_model=Envelope)
163
+ def lookup_relations(
164
+ subject_entity_id: UUID | None = None,
165
+ predicate: str | None = None,
166
+ object_entity_id: UUID | None = None,
167
+ valid_at: datetime | None = None,
168
+ ) -> Envelope:
169
+ """Relations matching an (s, p, o) pattern — current, or as-of (S9)."""
170
+ return engine.lookup_relations(
171
+ deployment_id=deployment_id,
172
+ subject_entity_id=subject_entity_id,
173
+ predicate=predicate,
174
+ object_entity_id=object_entity_id,
175
+ valid_at=valid_at,
176
+ )
177
+
178
+ @app.get("/transcript/relation/{relation_id}", response_model=Envelope)
179
+ def transcript_relation(relation_id: UUID) -> Envelope:
180
+ """The S8 audit query: why the system believes what it believes."""
181
+ return engine.transcript_relation(
182
+ deployment_id=deployment_id, relation_id=relation_id
183
+ )
184
+
185
+ @app.get("/lookup/observations", response_model=Envelope)
186
+ def lookup_observations(
187
+ entity_id: UUID, property_query: str | None = None, k: int = 10
188
+ ) -> Envelope:
189
+ """Live observations on one entity, semantic over statements (S2)."""
190
+ return engine.lookup_observations(
191
+ deployment_id=deployment_id,
192
+ entity_id=entity_id,
193
+ property_query=property_query,
194
+ k=k,
195
+ )
196
+
197
+ @app.get("/search/claims", response_model=Envelope)
198
+ def search_claims(query: str, k: int = 10) -> Envelope:
199
+ """Semantic claim search — evidence grain, never current-fact truth."""
200
+ return engine.search_claims(deployment_id=deployment_id, query=query, k=k)
201
+
202
+ @app.get("/hydrate/relation/{relation_id}", response_model=Envelope)
203
+ def hydrate_relation(relation_id: UUID) -> Envelope:
204
+ """The S5 chain: relation → evidence claims → source documents."""
205
+ return engine.hydrate_relation(
206
+ deployment_id=deployment_id, relation_id=relation_id
207
+ )
208
+
209
+ if surface is not None:
210
+ _mount_recipes(app=app, surface=surface)
211
+ if ingest is not None:
212
+ _mount_ingest(app=app, ingest=ingest, deployment_id=deployment_id)
213
+ if connectors is not None:
214
+ _mount_connectors(app=app, connectors=connectors, deployment_id=deployment_id)
215
+
216
+ return app
217
+
218
+
219
+ def _mount_recipes(*, app: FastAPI, surface: RecipeSurface) -> None:
220
+ """Add the registry-rendered recipe endpoints to the app (D50)."""
221
+
222
+ @app.get("/recipes", response_model=list[ToolDescriptor])
223
+ def list_recipes() -> list[ToolDescriptor]:
224
+ """The recipe tool list — this deployment's active registry rows."""
225
+ return list(surface.descriptors())
226
+
227
+ @app.post("/recipe/{name}", response_model=Envelope)
228
+ def run_recipe(
229
+ name: str, arguments: Annotated[dict[str, object], Body(default_factory=dict)]
230
+ ) -> Envelope:
231
+ """Run one recipe by name over JSON arguments (the D50 executor)."""
232
+ try:
233
+ return surface.run(name=name, arguments=arguments)
234
+ except UnknownRecipeError as error:
235
+ raise HTTPException(status_code=404, detail=str(error)) from error
236
+ except (MissingArgumentError, InvalidArgumentError) as error:
237
+ raise HTTPException(status_code=422, detail=str(error)) from error
238
+
239
+
240
+ def _mount_ingest(*, app: FastAPI, ingest: IngestPort, deployment_id: UUID) -> None:
241
+ """Add the D62 lineage-aware push surface over the E0 ingest gate."""
242
+
243
+ @app.post("/ingest", response_model=IngestedVersion)
244
+ def ingest_document(
245
+ content: Annotated[bytes, Body(media_type="application/octet-stream")],
246
+ filename: Annotated[str, Query(min_length=1)],
247
+ mime: Annotated[str, Query(min_length=1)],
248
+ title: str | None = None,
249
+ source_kind: Annotated[str | None, Query(min_length=1)] = None,
250
+ source_ref: Annotated[str | None, Query(min_length=1)] = None,
251
+ source_modified_at: datetime | None = None,
252
+ versioning_mode: Literal["snapshot", "living"] = "snapshot",
253
+ source_version_ref: str | None = None,
254
+ ) -> IngestedVersion:
255
+ """Push one file through E0, optionally as a stable lineage version."""
256
+ if (source_kind is None) != (source_ref is None):
257
+ raise HTTPException(
258
+ status_code=422,
259
+ detail="source_kind and source_ref must be supplied together",
260
+ )
261
+ if source_modified_at is not None and (
262
+ source_modified_at.tzinfo is None
263
+ or source_modified_at.utcoffset() != timedelta(0)
264
+ ):
265
+ raise HTTPException(
266
+ status_code=422, detail="source_modified_at must be timezone-aware UTC"
267
+ )
268
+ upload = DocumentUpload(
269
+ filename=filename, mime=mime, content=content, title=title
270
+ )
271
+ if source_kind is None or source_ref is None:
272
+ if (
273
+ source_modified_at is not None
274
+ or source_version_ref is not None
275
+ or versioning_mode != "snapshot"
276
+ ):
277
+ raise HTTPException(
278
+ status_code=422,
279
+ detail=(
280
+ "source timestamps, revisions, and living mode require"
281
+ " source_kind/source_ref"
282
+ ),
283
+ )
284
+ return ingest.ingest(deployment_id=deployment_id, upload=upload)
285
+ return ingest.ingest_observed(
286
+ deployment_id=deployment_id,
287
+ source_kind=source_kind,
288
+ source_ref=source_ref,
289
+ upload=upload,
290
+ versioning_mode=versioning_mode,
291
+ source_modified_at=source_modified_at,
292
+ source_version_ref=source_version_ref,
293
+ sync_cycle_id=None,
294
+ )
295
+
296
+
297
+ def _mount_connectors(
298
+ *, app: FastAPI, connectors: ConnectorManagementPort, deployment_id: UUID
299
+ ) -> None:
300
+ """Add remote connector-management endpoints; execution stays server-side."""
301
+
302
+ @app.get("/connectors", response_model=list[ConnectorDescriptor])
303
+ def list_connectors() -> list[ConnectorDescriptor]:
304
+ return list(connectors.connectors(deployment_id=deployment_id))
305
+
306
+ @app.post("/connectors", response_model=ConnectorDescriptor)
307
+ def add_connector(connector: ConnectorCreate) -> ConnectorDescriptor:
308
+ return connectors.add(deployment_id=deployment_id, connector=connector)
309
+
310
+ @app.post("/connectors/{connector_id}/pause", response_model=ConnectorDescriptor)
311
+ def pause_connector(connector_id: UUID) -> ConnectorDescriptor:
312
+ try:
313
+ return connectors.pause(
314
+ deployment_id=deployment_id, connector_id=connector_id
315
+ )
316
+ except ConnectorNotFoundError as error:
317
+ raise HTTPException(status_code=404, detail=str(error)) from error
318
+
319
+ @app.get("/connectors/{connector_id}", response_model=ConnectorDescriptor)
320
+ def connector_status(connector_id: UUID) -> ConnectorDescriptor:
321
+ try:
322
+ return connectors.status(
323
+ deployment_id=deployment_id, connector_id=connector_id
324
+ )
325
+ except ConnectorNotFoundError as error:
326
+ raise HTTPException(status_code=404, detail=str(error)) from error
327
+
328
+
329
+ def _perimeter(*, auth: AuthPerimeterPort, deployment_id: UUID): # noqa: ANN202
330
+ """A FastAPI dependency that authenticates the perimeter credential.
331
+
332
+ The `Authorization: <scheme> <value>` header is handed to the configured
333
+ port; a failure, a missing header, or a credential for another deployment
334
+ is a 401/403 before any read runs. This is the single enforcement point
335
+ (retrieval §9) — inside, it is one trust domain.
336
+ """
337
+
338
+ def dependency(
339
+ authorization: str | None = Header(default=None),
340
+ ) -> AuthenticatedContext:
341
+ if not authorization:
342
+ raise HTTPException(
343
+ status_code=401, detail="a perimeter credential is required"
344
+ )
345
+ scheme, _, value = authorization.partition(" ")
346
+ try:
347
+ context = auth.authenticate(
348
+ credential=PerimeterCredential(
349
+ scheme=scheme, value=SecretBytes(value.encode("utf-8"))
350
+ )
351
+ )
352
+ except Exception as error: # any auth failure is an opaque 401
353
+ raise HTTPException(
354
+ status_code=401, detail="perimeter authentication failed"
355
+ ) from error
356
+ if context.deployment_id != deployment_id:
357
+ raise HTTPException(
358
+ status_code=403, detail="credential is for another deployment"
359
+ )
360
+ return context
361
+
362
+ return dependency
363
+
364
+
365
+ def _admission(*, admission: AdmissionPort, deployment_id: UUID): # noqa: ANN202
366
+ """Return the deployment-wide D74 traffic dependency."""
367
+
368
+ def dependency() -> None:
369
+ """Map a closed fail-safe barrier to one stable HTTP negative."""
370
+ try:
371
+ admission.assert_available(deployment_id=deployment_id)
372
+ except ForgetInProgressError as error:
373
+ raise HTTPException(
374
+ status_code=503, detail={"code": "forget_in_progress"}
375
+ ) from error
376
+
377
+ return dependency
@@ -0,0 +1,67 @@
1
+ """The MCP surface (retrieval §7, D50): the tool list IS the recipe registry.
2
+
3
+ An MCP client discovers tools with `tools/list` and invokes one with
4
+ `tools/call`. Here both render straight off the recipe registry through the
5
+ shared `RecipeSurface`: every active recipe is a tool, and calling it runs the
6
+ recipe's chain. Adding a query pattern is adding a registry row — the tool
7
+ list updates with no code change, exactly as the design requires.
8
+
9
+ This class is the protocol *logic* — the two method results, shaped as MCP
10
+ expects. A deployment profile wraps it in a stdio (or SSE) JSON-RPC loop; the
11
+ transport is plumbing, and keeping it out of here lets the rendering and
12
+ dispatch be tested directly against the registry.
13
+ """
14
+
15
+ from rememberstack.surfaces.recipe_surface import InvalidArgumentError
16
+ from rememberstack.surfaces.recipe_surface import MissingArgumentError
17
+ from rememberstack.surfaces.recipe_surface import RecipeSurface
18
+ from rememberstack.surfaces.recipe_surface import UnknownRecipeError
19
+
20
+
21
+ class RecipeMcpServer:
22
+ """Render and dispatch the recipe registry as MCP tools."""
23
+
24
+ def __init__(self, *, surface: RecipeSurface) -> None:
25
+ """Bind the MCP server to the shared recipe surface."""
26
+ self._surface = surface
27
+
28
+ def list_tools(self) -> dict[str, object]:
29
+ """The `tools/list` result: one MCP tool per active recipe.
30
+
31
+ Each tool carries its name, description, and JSON-Schema
32
+ `inputSchema` — the recipe registry rendered verbatim, so the tool
33
+ list an agent sees is always exactly the registry's active rows.
34
+ """
35
+ return {
36
+ "tools": [
37
+ {
38
+ "name": descriptor.name,
39
+ "description": descriptor.description,
40
+ "inputSchema": descriptor.input_schema,
41
+ }
42
+ for descriptor in self._surface.descriptors()
43
+ ]
44
+ }
45
+
46
+ def call_tool(
47
+ self, *, name: str, arguments: dict[str, object]
48
+ ) -> dict[str, object]:
49
+ """The `tools/call` result: run the recipe, return its envelope as text.
50
+
51
+ The answer is the D49 envelope serialized to JSON in one text content
52
+ block. An unknown tool or a missing required argument is a protocol
53
+ error result (`isError`), not an exception across the wire — the
54
+ client re-plans against a stated failure.
55
+ """
56
+ try:
57
+ envelope = self._surface.run(name=name, arguments=arguments)
58
+ except (
59
+ UnknownRecipeError,
60
+ MissingArgumentError,
61
+ InvalidArgumentError,
62
+ ) as error:
63
+ return {"content": [{"type": "text", "text": str(error)}], "isError": True}
64
+ return {
65
+ "content": [{"type": "text", "text": envelope.model_dump_json()}],
66
+ "isError": False,
67
+ }