- M7.1-M7.10: Extensible SourceConnector trait, Obsidian/paperless/git/S3 connectors, sync framework, CLI, HTTP endpoints, health monitoring, gate - M3.5.10: Auth integration with Authentik OIDC → Vault token validation - DESIGN.md: Add source connectors architecture, update auth to Authentik/Vault (Kong removed from cluster) - INDEX.md: 75 tasks, 11 gates - Fix all Kong references in M3.5.1 task
5.3 KiB
M7.1 — SourceConnector trait + registry
| Field | Value |
|---|---|
| Phase | M7 — Source connectors |
| Size | M — 1–3 days |
| Status | ⬜ Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | M7.2, M7.3, M7.4, M7.5, M7.6, M7.10 |
| Depends | M0.3, M3.6.1 |
Goal
Define the extensible connector interface so that adding a new document source (paperless-ngx, S3, git repo, etc.) requires implementing one trait and adding one YAML config block — no changes to the ingest pipeline, chunking, embedding, storage, or query layers.
Facts (inlined — no spec read needed)
Memory service is a cluster-wide RAG serving multiple agents. Knowledge lives in many places: an Obsidian vault, paperless-ngx, git repositories, S3 buckets. The connector abstraction makes all of them look the same to the ingest pipeline.
The trait has three methods. list_documents() enumerates what's available
without fetching content. fetch_document() retrieves one document's text.
health_check() reports reachability. The sync framework (M7.6) handles
everything else — change detection, tombstoning, chunking, embedding.
Configuration is YAML-driven. Each connector instance is a block in
connectors.yaml with kind, name, and source-specific config. The registry
maps kind to a factory function that constructs the connector from its config.
Two connector families exist. Session connectors (pi, claude) produce evidence
for the gated loop (L0/L1/L2). Document connectors (obsidian, paperless, git, s3)
produce reference material at Level R, bypassing the gate. The connector's
source_type() method declares which family it belongs to.
Steps
- Define
SourceConnectortrait inmem-ingest/src/connector.rs:#[async_trait] pub trait SourceConnector: Send + Sync { fn kind(&self) -> &str; fn name(&self) -> &str; fn source_type(&self) -> SourceType; // Evidence or Reference async fn list_documents(&self) -> Result<Vec<SourceDocument>>; async fn fetch_document(&self, doc_id: &str) -> Result<DocumentContent>; async fn health_check(&self) -> Result<SourceHealth>; } - Define supporting types:
SourceDocument,DocumentContent,SourceHealth,SourceTypeenum (Evidence,Reference). - Define
ConnectorConfigserde struct for YAML deserialization:connectors: - kind: obsidian name: homelab-vault config: { root: /data/vault, extensions: [md, txt] } - Implement
ConnectorRegistry— mapskindstring to factory function, constructs connectors from config at startup. - Add
connectors.yamlloading inmem-clistartup path. - Provide a
VecConnectortest helper (in-memory documents) for testing downstream consumers without real I/O.
Acceptance
- The trait compiles and is object-safe (
Box<dyn SourceConnector>). ConnectorRegistrycan register and construct connectors by kind string.VecConnectorimplements the trait and passes basic list/fetch assertions.- YAML config deserialization works for known and unknown kinds (unknown = skip with warning, not crash).
source_type()is enforced at the type level — no runtime flag confusion.
Verify
Harness: in-memory VecConnector, YAML config fixtures.
Integration test — tests/it_source_connector.rs:
a1_trait_is_object_safe— construct aBox<dyn SourceConnector>fromVecConnector; call all three methods.a2_registry_constructs_by_kind— register "vec" kind, construct from config, assertkind()andname()match.a3_list_documents_returns_all—VecConnectorwith 3 docs, assertlist_documents()returns 3.a4_fetch_document_by_id— assert content matches what was registered.a5_fetch_unknown_id_errors— assertfetch_document("nonexistent")returns an error, not a panic.a6_health_check_reports_count— asserthealth_check()returnsdocument_count = Some(3).a7_yaml_config_loads— parse a fixtureconnectors.yamlwith two connector blocks; assert both are constructed.a8_unknown_kind_skipped— config withkind: "nonexistent"; assert registry logs a warning and continues without the connector.a9_source_type_evidence_vs_reference— assert session connectors returnEvidence, document connectors returnReference.a10_empty_config_is_valid— noconnectors.yamlor empty file; registry starts with zero connectors, no crash.
Command: cargo test --test it_source_connector
False pass:
- Testing only
VecConnectorand claiming the trait works. The trait is validated by M7.2–M7.5 implementing it against real sources. - Parsing YAML without verifying the constructed connector's methods work.
Traps
- Making the trait not object-safe (generic methods,
Selfin return types). Every consumer storesBox<dyn SourceConnector>, so object safety is load-bearing. - Putting chunking logic inside the connector. Connectors fetch documents; the sync framework (M7.6) chunks them. Mixing concerns means every connector reimplements chunking.
- Hard-coding the list of known kinds. The registry must be extensible — a
register(kind, factory_fn)call, not a match statement.
Background: DESIGN.md — source connectors section