diff --git a/crates/tinymemory-module/src/config.rs b/crates/tinymemory-module/src/config.rs index 0dd8b54..0009836 100644 --- a/crates/tinymemory-module/src/config.rs +++ b/crates/tinymemory-module/src/config.rs @@ -164,6 +164,22 @@ pub struct ModuleConfig { /// a no-sync nobody can, this picks the one that can be noticed. pub memory_sync_interval_secs: Option, + /// Base URL of the OpenHuman backend, for proxied ("backend") Composio. + /// + /// A field rather than a seam member, unlike the session bearer beside it, + /// and the difference is what each thing is. A bearer is a credential that + /// expires and gets refreshed, so a snapshot of it goes stale and has to be + /// asked for per call. A base URL is routing configuration: it changes when + /// an operator points the host at a different backend, which is a restart, + /// not a mid-session event. + /// + /// Empty means the host named none. The proxied branch of `composio_config` + /// then builds its request against an empty base and fails inside the HTTP + /// client with a builder error that names no cause — so a host that intends + /// proxied mode must send this. + #[serde(default)] + pub backend_api_url: String, + /// How the host routes Composio calls: `backend` or `direct`. /// /// Empty means the host stated no mode — an older host, or one with no @@ -204,6 +220,7 @@ impl Default for ModuleConfig { local_ai: LocalAiConfig::default(), embeddings_provider: None, memory_provider: None, + backend_api_url: String::new(), default_model: None, default_temperature: 0.0, output_language: None, diff --git a/crates/tinymemory-module/src/provider.rs b/crates/tinymemory-module/src/provider.rs index a48734b..4aeb437 100644 --- a/crates/tinymemory-module/src/provider.rs +++ b/crates/tinymemory-module/src/provider.rs @@ -33,6 +33,7 @@ impl From<&ModuleConfig> for EngineRuntimeConfig { // mode, and both of those skip work rather than fail it. memory_sync_interval_secs: config.memory_sync_interval_secs, composio_mode: config.composio_mode.clone(), + backend_api_url: config.backend_api_url.clone(), composio_entity_id: config.composio_entity_id.clone(), } } diff --git a/crates/tinymemory-tinycortex/src/engine/mod.rs b/crates/tinymemory-tinycortex/src/engine/mod.rs index dd3c92d..7f9a55a 100644 --- a/crates/tinymemory-tinycortex/src/engine/mod.rs +++ b/crates/tinymemory-tinycortex/src/engine/mod.rs @@ -118,6 +118,10 @@ pub struct EngineRuntimeConfig { /// answer backend mode gets, which is what an unset Composio integration /// should look like. pub composio_mode: String, + /// Base URL of the host's backend, for proxied Composio. Empty when the + /// host named none — see `effective_backend_api_url` below for why that is + /// a refusal rather than a default. + pub backend_api_url: String, /// The Composio entity the host authenticates as. /// /// An identifier, not a credential: it selects whose connected accounts a @@ -200,8 +204,15 @@ impl MemoryHostConfig for EngineRuntimeConfig { fn api_url(&self) -> Option<&str> { None } + /// The host's backend base URL, verbatim. + /// + /// No default is substituted for an empty one. Guessing a URL here would + /// send a user's memory at whichever backend this crate happened to hard-code + /// — including, for a self-hosted operator, one they do not control. An + /// empty string fails inside the HTTP client instead, which is a bad error + /// message and the right outcome. fn effective_backend_api_url(&self) -> String { - String::new() + self.backend_api_url.clone() } /// Always the named refusal, never `Ok(None)`. /// diff --git a/crates/tinymemory-tinycortex/src/engine/test.rs b/crates/tinymemory-tinycortex/src/engine/test.rs index 1caeb18..a2aa311 100644 --- a/crates/tinymemory-tinycortex/src/engine/test.rs +++ b/crates/tinymemory-tinycortex/src/engine/test.rs @@ -55,6 +55,7 @@ fn runtime_config() -> EngineRuntimeConfig { EngineRuntimeConfig { workspace_dir: "/workspace".into(), config_path: "/workspace/config.toml".into(), + backend_api_url: String::new(), memory: Default::default(), memory_tree: Default::default(), scheduler_gate: Default::default(), @@ -253,6 +254,7 @@ fn the_sync_cadence_is_answered_from_the_host_and_not_from_a_constant() { fn an_unstated_composio_mode_is_not_direct() { let config = EngineRuntimeConfig { composio_mode: String::new(), + backend_api_url: String::new(), composio_entity_id: String::new(), ..runtime_config() }; diff --git a/crates/tinymemory-tinycortex/tests/full_provider_conformance.rs b/crates/tinymemory-tinycortex/tests/full_provider_conformance.rs index 95b487c..e977a9e 100644 --- a/crates/tinymemory-tinycortex/tests/full_provider_conformance.rs +++ b/crates/tinymemory-tinycortex/tests/full_provider_conformance.rs @@ -118,6 +118,7 @@ fn provider_config( // sends. memory_sync_interval_secs: None, composio_mode: String::new(), + backend_api_url: String::new(), composio_entity_id: String::new(), } }