From e006b67d78712698f84bf0fbb340524ef6f359c3 Mon Sep 17 00:00:00 2001 From: Shanu Date: Tue, 25 Aug 2026 21:30:48 +0530 Subject: [PATCH] Carry the backend base URL into the module, so proxied Composio can address it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.8.0 gave the module a way to obtain a session bearer, which was necessary for proxied Composio and not sufficient: `effective_backend_api_url` still answered an empty string, so the request built against an empty base and failed inside the HTTP client with `builder error` — a message that names neither the cause nor the field. Found by routing OpenHuman's Composio sync onto the driver and watching the run get three provider actions in before dying on the transport. The bearer had reached it; the address had not. ## A field, not a seam The bearer is a seam because it is a credential that expires and gets refreshed, so any snapshot of it goes stale. A base URL is the opposite kind of value: routing configuration that changes when an operator points the host at a different backend, which is a restart rather than a mid-session event. Carrying it in `ModuleConfig` is both simpler and more honest about what it is. ## No default is substituted An empty URL stays empty and fails in the HTTP client. Guessing one here would send a user's memory at whichever backend this crate happened to hard-code — including, for a self-hosted operator, a backend they do not control. A bad error message is a much smaller problem than that, so the field is documented as required for proxied mode rather than defaulted into looking optional. `#[serde(default)]`, so a host that predates the field still loads. --- crates/tinymemory-module/src/config.rs | 17 +++++++++++++++++ crates/tinymemory-module/src/provider.rs | 1 + crates/tinymemory-tinycortex/src/engine/mod.rs | 13 ++++++++++++- crates/tinymemory-tinycortex/src/engine/test.rs | 2 ++ .../tests/full_provider_conformance.rs | 1 + 5 files changed, 33 insertions(+), 1 deletion(-) 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(), } }