From cd3d5fd8ee51bbda16529792f579d9a4f39a11c9 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 16 Aug 2026 18:04:46 +0300 Subject: [PATCH 1/2] chore(deps): update tinycortex submodule The tinycortex vendored dependency is advanced to commit be7b395, incorporating upstream fixes and improvements. No local changes are required. Auto-committed-on: dragonfly Co-authored-by: Medulla --- vendor/tinycortex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/tinycortex b/vendor/tinycortex index 0a7a0671..be7b3953 160000 --- a/vendor/tinycortex +++ b/vendor/tinycortex @@ -1 +1 @@ -Subproject commit 0a7a06710fce8dba1cdb06b3e4640c351bba800c +Subproject commit be7b395354271082953d2594765aded73975b54c From ad4cd1bdc68c23dc665ad816b812327a7e98949f Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 16 Aug 2026 18:19:45 +0300 Subject: [PATCH 2/2] feat: support Cognee and Supermemory APIs Co-authored-by: Medulla --- README.md | 31 +++++- adapters/remote/examples/conformance.rs | 10 +- adapters/remote/src/cognee.rs | 98 +++++++++++++++---- adapters/remote/src/cognee_test.rs | 88 ++++++++++++++++- adapters/remote/src/common.rs | 6 +- adapters/remote/src/lib.rs | 4 +- adapters/remote/src/supermemory.rs | 52 +++++++++- adapters/remote/src/supermemory_test.rs | 121 +++++++++++++++++++++--- integration/remote-engines/README.md | 17 ++++ vendor/tinycortex | 2 +- 10 files changed, 372 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index d2eb938e..6f911dc5 100644 --- a/README.md +++ b/README.md @@ -72,19 +72,40 @@ that skips enforcement is the entire reason the policy layer exists. ## Remote engines -The `tinymemory-remote` crate supports the self-hosted native APIs of -Supermemory, Mem0, and Cognee. Each adapter stores TinyMemory's key, category, -session, and provenance in backend metadata (or a Cognee raw-data envelope), so -exact CRUD and portability survive the seam while recall remains engine-native. +The `tinymemory-remote` crate supports the managed and self-hosted native APIs +of Supermemory and Cognee, plus self-hosted Mem0. Each adapter stores +TinyMemory's key, category, session, and provenance in backend metadata (or a +Cognee raw-data envelope), so exact CRUD and portability survive the seam while +recall remains engine-native. Provider-facing dataset names, container tags, +and filenames are bounded stable hashes, so every namespace and key accepted by +the TinyMemory contract remains valid on the remote API. ```rust use tinymemory_remote::{SupermemoryMemory, supermemory_provider}; -let memory = SupermemoryMemory::new("http://localhost:6767", Some("sm_..."))?; +let memory = SupermemoryMemory::self_hosted("http://localhost:6767", "sm_...")?; let provider = supermemory_provider(memory); # Ok::<_, anyhow::Error>(provider) ``` +Managed APIs have explicit constructors so their authentication cannot be +confused with a self-hosted token: + +```rust +use tinymemory_remote::{CogneeMemory, SupermemoryMemory}; + +let cognee = CogneeMemory::cloud("cognee-api-key")?; +let supermemory = SupermemoryMemory::cloud("sm_...")?; + +// Cognee also issues tenant-specific API origins. +let tenant = CogneeMemory::api("https://tenant.example.cognee.ai", "api-key")?; +# Ok::<_, anyhow::Error>((cognee, supermemory, tenant)) +``` + +Cognee Cloud uses `X-Api-Key`; authenticated self-hosted Cognee uses a bearer +access token. Supermemory uses bearer API keys for both deployment modes. All +constructors redact credentials from `Debug` output and transport errors. + All three advertise the mandatory Core, Recall, and Portability families. The live Docker harness and conformance command are documented in [`integration/remote-engines/`](integration/remote-engines/README.md). diff --git a/adapters/remote/examples/conformance.rs b/adapters/remote/examples/conformance.rs index bfa0378f..e98c22a4 100644 --- a/adapters/remote/examples/conformance.rs +++ b/adapters/remote/examples/conformance.rs @@ -13,7 +13,9 @@ use tinymemory_remote::{ /// Builds the command-line usage error returned for invalid arguments. fn usage() -> anyhow::Error { - anyhow::anyhow!("usage: conformance [credential]") + anyhow::anyhow!( + "usage: conformance [credential]" + ) } #[tokio::main] @@ -36,6 +38,12 @@ async fn main() -> anyhow::Result<()> { &endpoint, credential.as_deref(), )?)), + "cognee-api" => Arc::new(cognee_provider(CogneeMemory::api( + &endpoint, + credential + .as_deref() + .ok_or_else(|| anyhow::anyhow!("cognee-api requires a credential"))?, + )?)), _ => return Err(usage()), }; diff --git a/adapters/remote/src/cognee.rs b/adapters/remote/src/cognee.rs index 63623259..6b2a60d7 100644 --- a/adapters/remote/src/cognee.rs +++ b/adapters/remote/src/cognee.rs @@ -8,19 +8,22 @@ use tinymemory_api::recall::RecallOpts; use tinymemory_api::traits::Memory; use tinymemory_api::types::MemoryTaint; -use crate::common::{encode, Dialect, HttpClient, RemoteMemory, StoredEntry}; +use crate::common::{stable_id, Dialect, HttpClient, RemoteMemory, StoredEntry}; /// Stable driver id used by configuration and status output. pub use tinymemory::registry::COGNEE_DRIVER_ID; -/// A self-hosted Cognee server exposed through TinyMemory's storage contract. +/// Default base URL for Cognee's managed API. +pub const COGNEE_API_ENDPOINT: &str = "https://api.cognee.ai"; + +/// A Cognee managed or self-hosted service exposed through TinyMemory's contract. #[derive(Debug)] pub struct CogneeMemory { inner: RemoteMemory, } impl CogneeMemory { - /// Connect to a Cognee server. + /// Connect to a self-hosted Cognee server. /// /// `access_token` is sent as a bearer token. Local deployments with /// backend access control disabled may pass `None`. @@ -29,12 +32,53 @@ impl CogneeMemory { /// /// Returns an error when `endpoint` is not an HTTP(S) URL. pub fn new(endpoint: &str, access_token: Option<&str>) -> anyhow::Result { + Self::self_hosted(endpoint, access_token) + } + + /// Connect to a self-hosted Cognee server. + /// + /// `access_token` is sent as a bearer token. Local deployments with + /// authentication disabled may pass `None`. + /// + /// # Errors + /// + /// Returns an error when `endpoint` is not an HTTP(S) URL. + pub fn self_hosted(endpoint: &str, access_token: Option<&str>) -> anyhow::Result { Ok(Self { inner: RemoteMemory::new(CogneeDialect { client: HttpClient::bearer(endpoint, access_token)?, }), }) } + + /// Connect to a Cognee managed API using `X-Api-Key` authentication. + /// + /// This accepts a custom endpoint because Cognee Cloud may issue a + /// tenant-specific base URL. Use [`Self::cloud`] for the shared default. + /// + /// # Errors + /// + /// Returns an error when `endpoint` is invalid or `api_key` is blank. + pub fn api(endpoint: &str, api_key: &str) -> anyhow::Result { + anyhow::ensure!( + !api_key.trim().is_empty(), + "cognee API key must not be empty" + ); + Ok(Self { + inner: RemoteMemory::new(CogneeDialect { + client: HttpClient::api_key(endpoint, Some(api_key))?, + }), + }) + } + + /// Connect to Cognee's shared managed API endpoint. + /// + /// # Errors + /// + /// Returns an error when `api_key` is blank. + pub fn cloud(api_key: &str) -> anyhow::Result { + Self::api(COGNEE_API_ENDPOINT, api_key) + } } #[async_trait] @@ -128,11 +172,11 @@ struct Dataset { impl CogneeDialect { /// Encodes a TinyMemory namespace as a collision-free Cognee dataset name. fn dataset_name(namespace: &str) -> String { - format!("tinymemory__{}", encode(namespace)) + format!("tinymemory__{}", stable_id("dataset", namespace)) } /// Encodes a TinyMemory key as the uploaded envelope's filename. fn filename(key: &str) -> String { - format!("{}.tinymemory.json", encode(key)) + format!("{}.tinymemory.json", stable_id("key", key)) } /// Discovers only datasets owned by the TinyMemory adapter. @@ -239,33 +283,45 @@ impl Dialect for CogneeDialect { /// Replaces an existing envelope and uploads the new exact record. async fn upsert(&self, entry: StoredEntry) -> anyhow::Result<()> { - if let Some(existing) = self + let existing = self .entries() .await? .into_iter() - .find(|item| item.namespace == entry.namespace && item.key == entry.key) - { - self.delete_entry(&existing).await?; - } + .find(|item| item.namespace == entry.namespace && item.key == entry.key); let body = serde_json::to_vec(&entry)?; - let form = multipart::Form::new() - .text("datasetName", Self::dataset_name(&entry.namespace)) - .text("run_in_background", "false") - .part( - "data", - multipart::Part::bytes(body) - .file_name(Self::filename(&entry.key)) - .mime_str("application/json")?, - ); + let form = multipart::Form::new().part( + "data", + multipart::Part::bytes(body) + .file_name(Self::filename(&entry.key)) + .mime_str("application/json")?, + ); + let (method, path, form) = if let Some(existing) = existing { + let (dataset_id, data_id) = existing + .remote_id + .split_once(':') + .ok_or_else(|| anyhow!("Cognee record has no dataset id"))?; + ( + Method::PATCH, + format!("api/v1/update?data_id={data_id}&dataset_id={dataset_id}"), + form, + ) + } else { + ( + Method::POST, + "api/v1/remember".to_owned(), + form.text("datasetName", Self::dataset_name(&entry.namespace)) + .text("run_in_background", "false"), + ) + }; let response = self .client - .multipart("api/v1/remember")? + .multipart(method, &path)? .multipart(form) .send() .await?; if !response.status().is_success() { return Err(anyhow!( - "memory API api/v1/remember returned HTTP {}", + "memory API {path} returned HTTP {}", response.status() )); } diff --git a/adapters/remote/src/cognee_test.rs b/adapters/remote/src/cognee_test.rs index b424047a..64afe247 100644 --- a/adapters/remote/src/cognee_test.rs +++ b/adapters/remote/src/cognee_test.rs @@ -6,15 +6,16 @@ use std::sync::{Arc, Mutex}; use axum::{ extract::{Multipart, State}, - http::StatusCode, + http::{HeaderMap, StatusCode}, response::IntoResponse, - routing::{delete, get, post}, + routing::{delete, get, patch, post}, Json, Router, }; use serde_json::{json, Value}; use tinymemory_api::{ provider::{MemoryCore, MemoryProvider, MemoryRecall}, recall::OwnedRecallOpts, + traits::Memory, types::{MemoryCategory, MemoryTaint}, }; @@ -23,7 +24,10 @@ struct AppState(Arc>>>); async fn datasets(State(state): State) -> Json { let values = if state.0.lock().expect("state lock").is_some() { - vec![json!({"id": "dataset-1", "name": "tinymemory__70726f6a656374"})] + vec![json!({ + "id": "dataset-1", + "name": super::CogneeDialect::dataset_name("project") + })] } else { vec![] }; @@ -70,6 +74,67 @@ async fn recall(State(state): State) -> Json { Json(Value::Array(records)) } +async fn capture_auth(State(state): State>>, headers: HeaderMap) -> StatusCode { + *state.lock().expect("state lock") = json!({ + "authorization": headers + .get("authorization") + .and_then(|value| value.to_str().ok()), + "api_key": headers + .get("x-api-key") + .and_then(|value| value.to_str().ok()), + }); + StatusCode::OK +} + +#[tokio::test] +async fn cognee_supports_cloud_api_keys_and_self_hosted_bearer_tokens() { + let captured = Arc::new(Mutex::new(Value::Null)); + let app = Router::new() + .route("/health", get(capture_auth)) + .with_state(captured.clone()); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind"); + let endpoint = format!("http://{}", listener.local_addr().expect("address")); + tokio::spawn(async move { + axum::serve(listener, app).await.expect("serve"); + }); + + let api = super::CogneeMemory::api(&endpoint, "cloud-secret").expect("api client"); + assert!(api.health_check().await); + let api_headers = captured.lock().expect("state lock").clone(); + assert_eq!(api_headers["api_key"], "cloud-secret"); + assert!(api_headers["authorization"].is_null()); + + let hosted = super::CogneeMemory::self_hosted(&endpoint, Some("local-secret")) + .expect("self-hosted client"); + assert!(hosted.health_check().await); + let hosted_headers = captured.lock().expect("state lock").clone(); + assert_eq!(hosted_headers["authorization"], "Bearer local-secret"); + assert!(hosted_headers["api_key"].is_null()); + + let debug = format!("{api:?}"); + assert!(!debug.contains("cloud-secret")); + assert!(super::CogneeMemory::api(&endpoint, " ").is_err()); +} + +#[test] +fn cognee_remote_names_are_bounded_and_safe_for_arbitrary_contract_keys() { + let unusual = format!("tenant / 🧠 / {}", "x".repeat(500)); + let dataset = super::CogneeDialect::dataset_name(&unusual); + let filename = super::CogneeDialect::filename(&unusual); + + assert!(dataset.starts_with("tinymemory__tm_")); + assert!(dataset.len() < 100); + assert!(dataset + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')); + assert!(filename.starts_with("tm_")); + assert!(filename.ends_with(".tinymemory.json")); + assert!(filename.len() < 100); + assert_eq!(dataset, super::CogneeDialect::dataset_name(&unusual)); +} + #[tokio::test] async fn native_cognee_round_trips_the_tinymemory_contract() { let state = AppState::default(); @@ -79,6 +144,7 @@ async fn native_cognee_round_trips_the_tinymemory_contract() { .route("/api/v1/datasets/{dataset}/data/{data}/raw", get(raw)) .route("/api/v1/datasets/{dataset}/data/{data}", delete(remove)) .route("/api/v1/remember", post(remember)) + .route("/api/v1/update", patch(remember)) .route("/api/v1/recall", post(recall)) .route("/health", get(|| async { StatusCode::OK })) .with_state(state); @@ -90,7 +156,8 @@ async fn native_cognee_round_trips_the_tinymemory_contract() { axum::serve(listener, app).await.expect("serve"); }); - let driver = crate::cognee_provider(super::CogneeMemory::new(&endpoint, None).expect("client")); + let driver = + crate::cognee_provider(super::CogneeMemory::self_hosted(&endpoint, None).expect("client")); tinymemory_api::provider::audit_provider(&driver).expect("honest capabilities"); driver .store( @@ -103,12 +170,23 @@ async fn native_cognee_round_trips_the_tinymemory_contract() { ) .await .expect("store"); + driver + .store( + "project", + "key", + "updated knowledge graph", + MemoryCategory::Conversation, + Some("session"), + MemoryTaint::ExternalSync, + ) + .await + .expect("upsert"); let entry = driver .get("project", "key") .await .expect("get") .expect("entry"); - assert_eq!(entry.content, "knowledge graph"); + assert_eq!(entry.content, "updated knowledge graph"); assert_eq!(entry.taint, MemoryTaint::ExternalSync); assert_eq!( driver diff --git a/adapters/remote/src/common.rs b/adapters/remote/src/common.rs index 6fe3a920..341ce133 100644 --- a/adapters/remote/src/common.rs +++ b/adapters/remote/src/common.rs @@ -144,9 +144,9 @@ impl HttpClient { Ok(status) } - /// Starts an authenticated multipart POST request. - pub(crate) fn multipart(&self, path: &str) -> anyhow::Result { - self.request(Method::POST, path) + /// Starts an authenticated multipart request. + pub(crate) fn multipart(&self, method: Method, path: &str) -> anyhow::Result { + self.request(method, path) } /// Reports whether a GET endpoint responds successfully. diff --git a/adapters/remote/src/lib.rs b/adapters/remote/src/lib.rs index c513c98c..d649e5e7 100644 --- a/adapters/remote/src/lib.rs +++ b/adapters/remote/src/lib.rs @@ -13,9 +13,9 @@ mod common; pub mod mem0; pub mod supermemory; -pub use cognee::{CogneeMemory, COGNEE_DRIVER_ID}; +pub use cognee::{CogneeMemory, COGNEE_API_ENDPOINT, COGNEE_DRIVER_ID}; pub use mem0::{Mem0Memory, MEM0_DRIVER_ID}; -pub use supermemory::{SupermemoryMemory, SUPERMEMORY_DRIVER_ID}; +pub use supermemory::{SupermemoryMemory, SUPERMEMORY_API_ENDPOINT, SUPERMEMORY_DRIVER_ID}; use std::sync::Arc; diff --git a/adapters/remote/src/supermemory.rs b/adapters/remote/src/supermemory.rs index cc8df376..d69c599b 100644 --- a/adapters/remote/src/supermemory.rs +++ b/adapters/remote/src/supermemory.rs @@ -7,12 +7,15 @@ use tinymemory_api::recall::RecallOpts; use tinymemory_api::traits::Memory; use tinymemory_api::types::MemoryTaint; -use crate::common::{category, Dialect, HttpClient, RemoteMemory, StoredEntry}; +use crate::common::{category, stable_id, Dialect, HttpClient, RemoteMemory, StoredEntry}; /// Stable driver id used by configuration and status output. pub use tinymemory::registry::SUPERMEMORY_DRIVER_ID; -/// A self-hosted Supermemory server exposed through TinyMemory's storage contract. +/// Default base URL for Supermemory's managed API. +pub const SUPERMEMORY_API_ENDPOINT: &str = "https://api.supermemory.ai"; + +/// A Supermemory managed or self-hosted service exposed through TinyMemory's contract. #[derive(Debug)] pub struct SupermemoryMemory { inner: RemoteMemory, @@ -31,6 +34,40 @@ impl SupermemoryMemory { }), }) } + + /// Connect to a provided Supermemory API using bearer authentication. + /// + /// # Errors + /// + /// Returns an error when `endpoint` is invalid or `api_key` is blank. + pub fn api(endpoint: &str, api_key: &str) -> anyhow::Result { + anyhow::ensure!( + !api_key.trim().is_empty(), + "supermemory API key must not be empty" + ); + Self::new(endpoint, Some(api_key)) + } + + /// Connect to a self-hosted Supermemory server. + /// + /// Self-hosted Supermemory generates a bearer API key on first boot and + /// exposes the same API as the managed service. + /// + /// # Errors + /// + /// Returns an error when `endpoint` is invalid or `api_key` is blank. + pub fn self_hosted(endpoint: &str, api_key: &str) -> anyhow::Result { + Self::api(endpoint, api_key) + } + + /// Connect to Supermemory's managed API endpoint. + /// + /// # Errors + /// + /// Returns an error when `api_key` is blank. + pub fn cloud(api_key: &str) -> anyhow::Result { + Self::api(SUPERMEMORY_API_ENDPOINT, api_key) + } } #[async_trait] @@ -115,6 +152,11 @@ struct SupermemoryDialect { } impl SupermemoryDialect { + /// Maps an arbitrary TinyMemory namespace into Supermemory's bounded tag grammar. + fn container_tag(namespace: &str) -> String { + format!("tinymemory:{}", stable_id("container", namespace)) + } + /// Encodes TinyMemory identity, classification, session, and provenance. fn metadata(entry: &StoredEntry) -> Value { let mut metadata = serde_json::Map::from_iter([ @@ -271,7 +313,7 @@ impl Dialect for SupermemoryDialect { "isStatic": false, "metadata": metadata }], - "containerTag": entry.namespace, + "containerTag": Self::container_tag(&entry.namespace), })), ) .await?; @@ -298,7 +340,7 @@ impl Dialect for SupermemoryDialect { }); if let Some(object) = body.as_object_mut() { if let Some(namespace) = opts.namespace { - object.insert("containerTag".into(), json!(namespace)); + object.insert("containerTag".into(), json!(Self::container_tag(namespace))); } if let Some(minimum) = opts.min_score { object.insert("threshold".into(), json!(minimum)); @@ -333,7 +375,7 @@ impl Dialect for SupermemoryDialect { "v4/memories", Some(&json!({ "id": entry.remote_id, - "containerTag": namespace, + "containerTag": Self::container_tag(namespace), "reason": "deleted through TinyMemory" })), ) diff --git a/adapters/remote/src/supermemory_test.rs b/adapters/remote/src/supermemory_test.rs index 28d5075c..d3d37ac6 100644 --- a/adapters/remote/src/supermemory_test.rs +++ b/adapters/remote/src/supermemory_test.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex}; use axum::{ extract::State, - http::StatusCode, + http::{HeaderMap, StatusCode}, routing::{get, post}, Json, Router, }; @@ -14,24 +14,47 @@ use serde_json::{json, Value}; use tinymemory_api::{ provider::{MemoryCore, MemoryProvider, MemoryRecall}, recall::OwnedRecallOpts, + traits::Memory, types::{MemoryCategory, MemoryTaint}, }; +#[derive(Default)] +struct Fixture { + records: Vec, + container_tags: Vec, + last_search_tag: Option, +} + #[derive(Clone, Default)] -struct AppState(Arc>>); +struct AppState(Arc>); -async fn tags() -> Json { - Json(json!([{"containerTag": "project"}])) +async fn tags(State(state): State) -> Json { + let fixture = state.0.lock().expect("state lock"); + Json(Value::Array( + fixture + .container_tags + .iter() + .map(|tag| json!({"containerTag": tag})) + .collect(), + )) } async fn list(State(state): State) -> Json { - let records = state.0.lock().expect("state lock"); - Json(json!({"memoryEntries": records.clone(), "pagination": {"totalPages": 1}})) + let fixture = state.0.lock().expect("state lock"); + Json(json!({"memoryEntries": fixture.records, "pagination": {"totalPages": 1}})) } async fn add(State(state): State, Json(body): Json) -> Json { - let mut records = state.0.lock().expect("state lock"); - let id = format!("doc-{}", records.len() + 1); - records.push(json!({ + let mut fixture = state.0.lock().expect("state lock"); + let id = format!("doc-{}", fixture.records.len() + 1); + let container_tag = body["containerTag"].as_str().expect("container tag"); + if !fixture + .container_tags + .iter() + .any(|tag| tag == container_tag) + { + fixture.container_tags.push(container_tag.to_owned()); + } + fixture.records.push(json!({ "id": id, "memory": body["memories"][0]["content"], "metadata": body["memories"][0]["metadata"], @@ -47,6 +70,7 @@ async fn update(State(state): State, Json(body): Json) -> Statu .0 .lock() .expect("state lock") + .records .iter_mut() .find(|r| r["id"] == id) { @@ -61,14 +85,66 @@ async fn remove(State(state): State, Json(body): Json) -> Statu .0 .lock() .expect("state lock") + .records .retain(|r| r["id"] != id); StatusCode::OK } -async fn search(State(state): State) -> Json { - let results = state.0.lock().expect("state lock").iter().map(|r| json!({"id": r["id"], "memory": r["memory"], "metadata": r["metadata"], "similarity": 0.95})).collect::>(); +async fn search(State(state): State, Json(body): Json) -> Json { + let mut fixture = state.0.lock().expect("state lock"); + fixture.last_search_tag = body["containerTag"].as_str().map(str::to_owned); + let results = fixture.records.iter().map(|r| json!({"id": r["id"], "memory": r["memory"], "metadata": r["metadata"], "similarity": 0.95})).collect::>(); Json(json!({"results": results})) } +async fn capture_auth(State(state): State>>, headers: HeaderMap) -> StatusCode { + *state.lock().expect("state lock") = json!({ + "authorization": headers + .get("authorization") + .and_then(|value| value.to_str().ok()), + }); + StatusCode::OK +} + +#[tokio::test] +async fn supermemory_supports_provided_and_self_hosted_apis() { + let captured = Arc::new(Mutex::new(Value::Null)); + let app = Router::new() + .route("/", get(capture_auth)) + .with_state(captured.clone()); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind"); + let endpoint = format!("http://{}", listener.local_addr().expect("address")); + tokio::spawn(async move { + axum::serve(listener, app).await.expect("serve"); + }); + + for client in [ + super::SupermemoryMemory::api(&endpoint, "provided-secret").expect("api client"), + super::SupermemoryMemory::self_hosted(&endpoint, "provided-secret") + .expect("self-hosted client"), + ] { + assert!(client.health_check().await); + let headers = captured.lock().expect("state lock").clone(); + assert_eq!(headers["authorization"], "Bearer provided-secret"); + assert!(!format!("{client:?}").contains("provided-secret")); + } + assert!(super::SupermemoryMemory::api(&endpoint, "").is_err()); +} + +#[test] +fn supermemory_container_tags_cover_arbitrary_contract_namespaces() { + let unusual = format!("tenant / 🧠 / {}", "x".repeat(500)); + let tag = super::SupermemoryDialect::container_tag(&unusual); + + assert!(tag.starts_with("tinymemory:tm_")); + assert!(tag.len() <= 100); + assert!(tag + .bytes() + .all(|byte| { byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'-') })); + assert_eq!(tag, super::SupermemoryDialect::container_tag(&unusual)); +} + #[tokio::test] async fn native_supermemory_round_trips_the_tinymemory_contract() { let state = AppState::default(); @@ -78,7 +154,7 @@ async fn native_supermemory_round_trips_the_tinymemory_contract() { .route("/v4/memories", post(add).patch(update).delete(remove)) .route("/v4/search", post(search)) .route("/", get(|| async { StatusCode::OK })) - .with_state(state); + .with_state(state.clone()); let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("bind"); @@ -88,7 +164,7 @@ async fn native_supermemory_round_trips_the_tinymemory_contract() { }); let driver = crate::supermemory_provider( - super::SupermemoryMemory::new(&endpoint, Some("secret")).expect("client"), + super::SupermemoryMemory::self_hosted(&endpoint, "secret").expect("client"), ); tinymemory_api::provider::audit_provider(&driver).expect("honest capabilities"); driver @@ -120,14 +196,31 @@ async fn native_supermemory_round_trips_the_tinymemory_contract() { .expect("entry"); assert_eq!(entry.content, "use Rust 2024"); assert_eq!(entry.taint, MemoryTaint::ExternalSync); + let expected_tag = super::SupermemoryDialect::container_tag("project"); + assert_eq!( + state.0.lock().expect("state lock").container_tags, + vec![expected_tag.clone()] + ); assert_eq!( driver - .recall("Rust", 1, &OwnedRecallOpts::default(), None) + .recall( + "Rust", + 1, + &OwnedRecallOpts { + namespace: Some("project".into()), + ..OwnedRecallOpts::default() + }, + None, + ) .await .expect("recall") .len(), 1 ); + assert_eq!( + state.0.lock().expect("state lock").last_search_tag, + Some(expected_tag) + ); assert!(driver.forget("project", "decision").await.expect("forget")); assert!(driver.health().await.is_usable()); } diff --git a/integration/remote-engines/README.md b/integration/remote-engines/README.md index 4eb2daec..c7600001 100644 --- a/integration/remote-engines/README.md +++ b/integration/remote-engines/README.md @@ -24,6 +24,23 @@ docker compose -f integration/remote-engines/docker-compose.yml \ cargo run -p tinymemory-remote --example conformance -- cognee http://localhost:8001 ``` +The same conformance command can target managed services. Supermemory uses the +same bearer authentication in both modes, while Cognee Cloud uses its distinct +API-key header: + +```sh +cargo run -p tinymemory-remote --example conformance -- \ + supermemory https://api.supermemory.ai "$SUPERMEMORY_API_KEY" + +cargo run -p tinymemory-remote --example conformance -- \ + cognee-api https://api.cognee.ai "$COGNEE_API_KEY" +``` + +For tenant-specific Cognee deployments, replace the shared endpoint with the +tenant URL issued by Cognee. The command writes a unique conformance namespace, +verifies Core, Recall, and Portability, and deletes its test record before +exiting. + Mem0 and Cognee require an inference provider for their native semantic pipelines. By default the harness starts a deterministic OpenAI-compatible test service, which proves HTTP, persistence, embeddings, and adapter translation diff --git a/vendor/tinycortex b/vendor/tinycortex index be7b3953..0a7a0671 160000 --- a/vendor/tinycortex +++ b/vendor/tinycortex @@ -1 +1 @@ -Subproject commit be7b395354271082953d2594765aded73975b54c +Subproject commit 0a7a06710fce8dba1cdb06b3e4640c351bba800c