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.
- rememberstack/__init__.py +9 -0
- rememberstack/adapters/__init__.py +42 -0
- rememberstack/adapters/codex_writer.py +221 -0
- rememberstack/adapters/markitdown_converter.py +42 -0
- rememberstack/adapters/openrouter.py +136 -0
- rememberstack/adapters/selfhost/__init__.py +54 -0
- rememberstack/adapters/selfhost/forget.py +66 -0
- rememberstack/adapters/selfhost/git.py +374 -0
- rememberstack/adapters/selfhost/lance.py +328 -0
- rememberstack/adapters/selfhost/minio.py +279 -0
- rememberstack/adapters/selfhost/mounts.py +249 -0
- rememberstack/adapters/selfhost/object_store.py +130 -0
- rememberstack/adapters/selfhost/projection.py +80 -0
- rememberstack/adapters/selfhost/queue.py +137 -0
- rememberstack/adapters/selfhost/telemetry.py +45 -0
- rememberstack/adapters/selfhost/watcher.py +70 -0
- rememberstack/adapters/testing/__init__.py +15 -0
- rememberstack/adapters/testing/cost_meter.py +13 -0
- rememberstack/adapters/testing/model_provider.py +83 -0
- rememberstack/adapters/testing/queue.py +43 -0
- rememberstack/adapters/testing/telemetry.py +22 -0
- rememberstack/client.py +19 -0
- rememberstack/core/__init__.py +127 -0
- rememberstack/core/blockizer.py +189 -0
- rememberstack/core/chunker.py +216 -0
- rememberstack/core/consumption_skill.py +275 -0
- rememberstack/core/conversion.py +76 -0
- rememberstack/core/core_manifest.py +598 -0
- rememberstack/core/extension_packs.py +124 -0
- rememberstack/core/forget.py +17 -0
- rememberstack/core/knowledge_authored.py +276 -0
- rememberstack/core/knowledge_compile.py +215 -0
- rememberstack/core/knowledge_fact_sheet.py +210 -0
- rememberstack/core/knowledge_hashing.py +68 -0
- rememberstack/core/knowledge_planner.py +64 -0
- rememberstack/core/knowledge_writer.py +175 -0
- rememberstack/core/ranking.py +200 -0
- rememberstack/core/recipe_linter.py +149 -0
- rememberstack/core/section_snap.py +209 -0
- rememberstack/core/storage_routing.py +27 -0
- rememberstack/eval/__init__.py +53 -0
- rememberstack/eval/consumption.py +141 -0
- rememberstack/eval/contradiction.py +184 -0
- rememberstack/eval/harness.py +136 -0
- rememberstack/eval/lifecycle.py +400 -0
- rememberstack/eval/operational_scale.py +49 -0
- rememberstack/eval/resolution.py +255 -0
- rememberstack/eval/retrieval_spikes.py +50 -0
- rememberstack/eval/skeleton.py +231 -0
- rememberstack/llm/__init__.py +1 -0
- rememberstack/model/__init__.py +589 -0
- rememberstack/model/adjudication.py +100 -0
- rememberstack/model/auth.py +27 -0
- rememberstack/model/blocks.py +30 -0
- rememberstack/model/chunks.py +190 -0
- rememberstack/model/claims.py +162 -0
- rememberstack/model/client.py +98 -0
- rememberstack/model/clustering.py +54 -0
- rememberstack/model/component_version.py +124 -0
- rememberstack/model/consumption.py +88 -0
- rememberstack/model/conversion.py +31 -0
- rememberstack/model/deployment.py +53 -0
- rememberstack/model/documents.py +168 -0
- rememberstack/model/envelope.py +513 -0
- rememberstack/model/evaluation.py +72 -0
- rememberstack/model/forget.py +143 -0
- rememberstack/model/git.py +13 -0
- rememberstack/model/knowledge.py +840 -0
- rememberstack/model/knowledge_authored.py +325 -0
- rememberstack/model/knowledge_planner.py +431 -0
- rememberstack/model/lifecycle.py +42 -0
- rememberstack/model/model_provider.py +78 -0
- rememberstack/model/mounts.py +24 -0
- rememberstack/model/object_store.py +21 -0
- rememberstack/model/operational_scale.py +59 -0
- rememberstack/model/operations.py +153 -0
- rememberstack/model/processing.py +228 -0
- rememberstack/model/queue.py +73 -0
- rememberstack/model/recipes.py +83 -0
- rememberstack/model/relations.py +79 -0
- rememberstack/model/resolution.py +83 -0
- rememberstack/model/retrieval_spikes.py +62 -0
- rememberstack/model/sections.py +120 -0
- rememberstack/model/telemetry.py +30 -0
- rememberstack/ports/__init__.py +29 -0
- rememberstack/ports/auth.py +16 -0
- rememberstack/ports/connector.py +23 -0
- rememberstack/ports/cost_meter.py +17 -0
- rememberstack/ports/forget.py +20 -0
- rememberstack/ports/git.py +20 -0
- rememberstack/ports/model_provider.py +28 -0
- rememberstack/ports/mounts.py +16 -0
- rememberstack/ports/object_store.py +27 -0
- rememberstack/ports/p1_index.py +92 -0
- rememberstack/ports/purge.py +93 -0
- rememberstack/ports/queue.py +23 -0
- rememberstack/ports/telemetry.py +21 -0
- rememberstack/profiles/__init__.py +22 -0
- rememberstack/profiles/selfhost.py +324 -0
- rememberstack/profiles/selfhost_forget.py +158 -0
- rememberstack/profiles/selfhost_operations.py +95 -0
- rememberstack/py.typed +1 -0
- rememberstack/spine/__init__.py +93 -0
- rememberstack/spine/admission.py +26 -0
- rememberstack/spine/backfill.py +168 -0
- rememberstack/spine/catalog_contract.py +742 -0
- rememberstack/spine/chunk_catalog.py +237 -0
- rememberstack/spine/claim_catalog.py +298 -0
- rememberstack/spine/clustering.py +740 -0
- rememberstack/spine/component_versions.py +208 -0
- rememberstack/spine/consumption.py +81 -0
- rememberstack/spine/deployment_bootstrap.py +445 -0
- rememberstack/spine/document_catalog.py +621 -0
- rememberstack/spine/entity_registry.py +205 -0
- rememberstack/spine/extension_packs.py +220 -0
- rememberstack/spine/fact_catalog.py +571 -0
- rememberstack/spine/forget.py +1753 -0
- rememberstack/spine/knowledge.py +5467 -0
- rememberstack/spine/lifecycle.py +1071 -0
- rememberstack/spine/migrations/__init__.py +1 -0
- rememberstack/spine/migrations/_helpers.py +153 -0
- rememberstack/spine/migrations/env.py +58 -0
- rememberstack/spine/migrations/script.py.mako +27 -0
- rememberstack/spine/migrations/versions/__init__.py +1 -0
- rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
- rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
- rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
- rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
- rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
- rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
- rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
- rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
- rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
- rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
- rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
- rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
- rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
- rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
- rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
- rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
- rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
- rememberstack/spine/observation_adjudication.py +778 -0
- rememberstack/spine/operations.py +298 -0
- rememberstack/spine/projection.py +662 -0
- rememberstack/spine/recipes.py +276 -0
- rememberstack/spine/resolver.py +763 -0
- rememberstack/spine/review.py +650 -0
- rememberstack/spine/settings.py +22 -0
- rememberstack/spine/supersession.py +510 -0
- rememberstack/spine/sync.py +128 -0
- rememberstack/spine/work_ledger.py +816 -0
- rememberstack/surfaces/__init__.py +110 -0
- rememberstack/surfaces/cli.py +447 -0
- rememberstack/surfaces/consumption_skill.py +87 -0
- rememberstack/surfaces/graph_queries.py +698 -0
- rememberstack/surfaces/http_api.py +377 -0
- rememberstack/surfaces/mcp.py +67 -0
- rememberstack/surfaces/query_engine.py +1591 -0
- rememberstack/surfaces/recipe_executor.py +185 -0
- rememberstack/surfaces/recipe_surface.py +219 -0
- rememberstack/surfaces/remote_mcp.py +133 -0
- rememberstack/surfaces/sdk.py +324 -0
- rememberstack/workers/__init__.py +155 -0
- rememberstack/workers/base.py +312 -0
- rememberstack/workers/e0.py +577 -0
- rememberstack/workers/e1.py +425 -0
- rememberstack/workers/e2.py +525 -0
- rememberstack/workers/e3.py +434 -0
- rememberstack/workers/forget.py +299 -0
- rememberstack/workers/knowledge_authored.py +146 -0
- rememberstack/workers/knowledge_driver.py +735 -0
- rememberstack/workers/knowledge_fact_sheet.py +123 -0
- rememberstack/workers/knowledge_planner.py +325 -0
- rememberstack/workers/knowledge_writer.py +393 -0
- rememberstack/workers/operations.py +42 -0
- rememberstack/workers/p1.py +234 -0
- rememberstack/workers/p2.py +513 -0
- rememberstack/workers/p2_analytics.py +276 -0
- rememberstack/workers/p3.py +673 -0
- rememberstack/workers/reconcile.py +485 -0
- rememberstack/workers/sync.py +168 -0
- rememberstack-0.1.0.dist-info/METADATA +213 -0
- rememberstack-0.1.0.dist-info/RECORD +186 -0
- rememberstack-0.1.0.dist-info/WHEEL +4 -0
- rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
}
|