From 7128b51fed36298d694a4b97a6c592da25b2048d Mon Sep 17 00:00:00 2001 From: Philippe Branchu Date: Sun, 31 May 2026 19:09:05 +0000 Subject: [PATCH 1/2] kernel: thread channel callback context through agent loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous design proposal stored a per-agent ChannelCallbackContext in a global DashMap on the kernel and read it back from tools. That structure caused cross-user callback bleed whenever the same agent was messaged by two channels concurrently — DashMap::insert is atomic but offers no isolation between the write and the later read inside the agent loop. The proper fix is to never store the context globally: pass it as an owned parameter from the bridge dispatch site, down through the agent loop, to the tool runner. Each concurrent invocation owns its own context value, so isolation is enforced by the type system rather than by ad-hoc locks. - Add `openfang_types::ChannelCallbackContext` (channel, recipient, display name, thread_id, agent_id). - Add `ChannelBridgeHandle::send_message_with_context` with a default impl that ignores the context and falls back to `send_message`. - `KernelBridgeAdapter` overrides it to call the new `OpenFangKernel::send_message_with_context` → `send_message_with_handle_and_blocks_and_context` → `execute_llm_agent` plumbing path. - `run_agent_loop` / `run_agent_loop_streaming` gain a `callback_context: Option` parameter that is forwarded to `tool_runner::execute_tool` as `Option<&...>`. - `execute_tool` accepts the context as a new (currently-unused) param; the async-dispatch PR will wire it into `tool_a2a_send_async`. - `dispatch_message` (channels bridge) builds the context from the incoming message and calls `send_message_with_context`. - `KernelHandle::inject_async_callback` declared here with a default Err — production implementation lands with the async-dispatch PR. - New test `test_concurrent_send_message_with_context_no_crossbleed`: two concurrent same-agent dispatches with distinct contexts each receive their own context back, confirming ownership-based isolation. Co-Authored-By: Claude Sonnet 4.6 --- crates/openfang-api/src/channel_bridge.rs | 18 ++ crates/openfang-api/src/routes.rs | 1 + crates/openfang-channels/src/bridge.rs | 194 ++++++++++++++++++- crates/openfang-kernel/src/kernel.rs | 59 ++++++ crates/openfang-runtime/src/agent_loop.rs | 20 ++ crates/openfang-runtime/src/kernel_handle.rs | 20 ++ crates/openfang-runtime/src/tool_runner.rs | 21 ++ crates/openfang-types/src/lib.rs | 20 ++ 8 files changed, 350 insertions(+), 3 deletions(-) diff --git a/crates/openfang-api/src/channel_bridge.rs b/crates/openfang-api/src/channel_bridge.rs index 37ad72921f..665f885804 100644 --- a/crates/openfang-api/src/channel_bridge.rs +++ b/crates/openfang-api/src/channel_bridge.rs @@ -84,6 +84,24 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { Ok(result.response) } + async fn send_message_with_context( + &self, + agent_id: AgentId, + message: &str, + callback_context: Option, + ) -> Result { + let result = self + .kernel + .send_message_with_context(agent_id, message, callback_context) + .await + .map_err(|e| format!("{e}"))?; + // Silent/NO_REPLY responses should not be forwarded to channels + if result.silent { + return Ok(String::new()); + } + Ok(result.response) + } + async fn send_message_with_blocks( &self, agent_id: AgentId, diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index cebb1f599a..4c66df003a 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -7093,6 +7093,7 @@ pub async fn mcp_http( None }, Some(&*state.kernel.process_manager), + None, // callback_context — MCP HTTP endpoint has no channel ) .await; diff --git a/crates/openfang-channels/src/bridge.rs b/crates/openfang-channels/src/bridge.rs index 2043aeaa76..eb14293110 100644 --- a/crates/openfang-channels/src/bridge.rs +++ b/crates/openfang-channels/src/bridge.rs @@ -102,6 +102,23 @@ pub trait ChannelBridgeHandle: Send + Sync { /// Send a message to an agent and get the text response. async fn send_message(&self, agent_id: AgentId, message: &str) -> Result; + /// Send a message to an agent, threading an optional channel callback context + /// through the agent loop so tools that need to deliver async results (e.g. + /// `a2a_send_async`) can capture the originating channel/user without reading + /// from a global per-agent map. + /// + /// Default implementation discards the context and falls back to `send_message`; + /// real bridge adapters override this to propagate the context. + async fn send_message_with_context( + &self, + agent_id: AgentId, + message: &str, + callback_context: Option, + ) -> Result { + let _ = callback_context; + self.send_message(agent_id, message).await + } + /// Send a message with structured content blocks (text + images) to an agent. /// /// Default implementation extracts text from blocks and falls back to `send_message()`. @@ -1330,8 +1347,31 @@ async fn dispatch_message( text.clone() }; - // Send to agent and relay response - let result = handle.send_message(agent_id, &prefixed_text).await; + // Build the channel callback context — passed by value into the agent loop + // (no global per-agent map) so concurrent dispatches for the same agent never + // see one another's context. Tools that need to deliver async results capture + // this from the per-invocation parameter handed to them by `execute_tool`. + // + // `thread_id` is the *smart-thread ID* — `effective_thread_id` resolved above: + // - Discord: thread channel ID from the incoming event + // - Slack: `thread_ts` timestamp + // - auto-threaded adapters: new thread ID from `adapter.create_thread()` + // - non-threaded adapters (Telegram DM, email): `None` + let callback_context = openfang_types::ChannelCallbackContext { + channel_type: ct_str.to_string(), + reply_to_platform_id: message.sender.platform_id.clone(), + reply_to_display_name: message.sender.display_name.clone(), + thread_id: thread_id.map(|t| t.to_string()), + agent_id: agent_id.to_string(), + }; + + // Send to agent and relay response. Clone so the retry path below + // (re-resolution) can pass the same context — otherwise the new agent + // would run without callback context and async tools would silently + // fail to fire. + let result = handle + .send_message_with_context(agent_id, &prefixed_text, Some(callback_context.clone())) + .await; // Stop the typing refresh now that we have a response typing_task.abort(); @@ -1359,7 +1399,22 @@ async fn dispatch_message( // Try re-resolution before reporting error if let Some(new_id) = try_reresolution(&e, &channel_key, handle, router).await { let typing_task2 = spawn_typing_loop(adapter_arc.clone(), message.sender.clone()); - let retry = handle.send_message(new_id, &text).await; + // Re-send WITH context: the freshly resolved agent needs the + // same callback context as the original attempt, otherwise its + // async tools (which rely on the channel callback) silently + // never fire. + // + // CRITICAL: rebuild the context's `agent_id` so it points at the + // newly resolved agent. The original `callback_context` was built + // from the pre-resolution `agent_id` (a UUID that is now dead / + // replaced); downstream consumers — notably the async-dispatch + // tools that read `context.agent_id` to call `send_to_agent(...)` + // — would otherwise deliver async callbacks to the stale agent. + let mut retry_context = callback_context.clone(); + retry_context.agent_id = new_id.to_string(); + let retry = handle + .send_message_with_context(new_id, &text, Some(retry_context)) + .await; typing_task2.abort(); match retry { Ok(response) => { @@ -2810,4 +2865,137 @@ mod tests { "image/jpeg" ); } + + // ------------------------------------------------------------------- + // Per-invocation context isolation test + // ------------------------------------------------------------------- + + /// Stub adapter that captures every call to `send_message_with_context`, + /// echoing the supplied context back in the response so the test can verify + /// no cross-call contamination. + struct CapturingContextHandle { + /// Records (agent_id, message, callback_context) in call order. + calls: tokio::sync::Mutex< + Vec<( + AgentId, + String, + Option, + )>, + >, + } + + #[async_trait] + impl ChannelBridgeHandle for CapturingContextHandle { + async fn send_message(&self, _agent_id: AgentId, message: &str) -> Result { + // Default impl (no context). Tests should use send_message_with_context. + Ok(format!("Echo-no-ctx: {message}")) + } + + async fn send_message_with_context( + &self, + agent_id: AgentId, + message: &str, + callback_context: Option, + ) -> Result { + // Simulate a non-trivial agent loop with an await point so the two + // concurrent calls actually overlap. + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + let recipient = callback_context + .as_ref() + .map(|c| c.reply_to_platform_id.clone()) + .unwrap_or_else(|| "no-context".to_string()); + self.calls + .lock() + .await + .push((agent_id, message.to_string(), callback_context)); + Ok(format!("for:{recipient}|msg:{message}")) + } + + async fn find_agent_by_name(&self, _name: &str) -> Result, String> { + Ok(None) + } + async fn list_agents(&self) -> Result, String> { + Ok(vec![]) + } + async fn spawn_agent_by_name(&self, _: &str) -> Result { + Err("unused".into()) + } + } + + /// Two concurrent `send_message_with_context` calls to the same agent from + /// different channel contexts must each receive a response derived from + /// their own context — no cross-bleed. + /// + /// This is the regression test for the global-channel_contexts-DashMap race + /// that prompted the kernel-context-threading refactor. Because context is + /// now an owned parameter on the call (not a shared map), the test cannot + /// physically observe contamination — the type system enforces isolation. + #[tokio::test] + async fn test_concurrent_send_message_with_context_no_crossbleed() { + let agent_id = AgentId::new(); + let stub = Arc::new(CapturingContextHandle { + calls: tokio::sync::Mutex::new(Vec::new()), + }); + let handle: Arc = stub.clone(); + + let ctx_alice = openfang_types::ChannelCallbackContext { + channel_type: "slack".to_string(), + reply_to_platform_id: "U-ALICE".to_string(), + reply_to_display_name: "Alice".to_string(), + thread_id: Some("ts-1".to_string()), + agent_id: agent_id.to_string(), + }; + let ctx_bob = openfang_types::ChannelCallbackContext { + channel_type: "telegram".to_string(), + reply_to_platform_id: "U-BOB".to_string(), + reply_to_display_name: "Bob".to_string(), + thread_id: None, + agent_id: agent_id.to_string(), + }; + + let h1 = handle.clone(); + let h2 = handle.clone(); + let alice_ctx = ctx_alice.clone(); + let bob_ctx = ctx_bob.clone(); + + let alice = tokio::spawn(async move { + h1.send_message_with_context(agent_id, "alice msg", Some(alice_ctx)) + .await + .unwrap() + }); + let bob = tokio::spawn(async move { + h2.send_message_with_context(agent_id, "bob msg", Some(bob_ctx)) + .await + .unwrap() + }); + + let alice_resp = alice.await.unwrap(); + let bob_resp = bob.await.unwrap(); + + // Each response must carry its own recipient — no cross-talk. + assert!( + alice_resp.contains("U-ALICE") && alice_resp.contains("alice msg"), + "Alice should see her own context, got: {alice_resp}" + ); + assert!( + bob_resp.contains("U-BOB") && bob_resp.contains("bob msg"), + "Bob should see his own context, got: {bob_resp}" + ); + + // And the recorded calls hold the original contexts byte-for-byte. + let calls = stub.calls.lock().await; + assert_eq!(calls.len(), 2); + for (_, msg, ctx) in calls.iter() { + let ctx = ctx.as_ref().expect("context must be present"); + if msg == "alice msg" { + assert_eq!(ctx.reply_to_platform_id, "U-ALICE"); + assert_eq!(ctx.channel_type, "slack"); + } else if msg == "bob msg" { + assert_eq!(ctx.reply_to_platform_id, "U-BOB"); + assert_eq!(ctx.channel_type, "telegram"); + } else { + panic!("unexpected message: {msg}"); + } + } + } } diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 8f59414c97..0efa6c6565 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -1831,6 +1831,35 @@ impl OpenFangKernel { .await } + /// Send a message with a channel callback context attached. + /// + /// The context is threaded into the agent loop and made available to tools + /// that need to deliver async results back to the originating channel. + /// Passed explicitly rather than stored in a global per-agent map so that + /// concurrent dispatches for the same agent never interfere. + pub async fn send_message_with_context( + &self, + agent_id: AgentId, + message: &str, + callback_context: Option, + ) -> KernelResult { + let handle: Option> = self + .self_handle + .get() + .and_then(|w| w.upgrade()) + .map(|arc| arc as Arc); + self.send_message_with_handle_and_blocks_and_context( + agent_id, + message, + handle, + None, + None, + None, + callback_context, + ) + .await + } + /// Send a multimodal message (text + images) to an agent and get a response. /// /// Used by channel bridges when a user sends a photo — the image is downloaded, @@ -1894,6 +1923,32 @@ impl OpenFangKernel { content_blocks: Option>, sender_id: Option, sender_name: Option, + ) -> KernelResult { + self.send_message_with_handle_and_blocks_and_context( + agent_id, + message, + kernel_handle, + content_blocks, + sender_id, + sender_name, + None, + ) + .await + } + + /// Variant of `send_message_with_handle_and_blocks` that also accepts an optional + /// channel callback context (threaded to the agent loop for use by tools that + /// deliver async results back to a channel). + #[allow(clippy::too_many_arguments)] + pub async fn send_message_with_handle_and_blocks_and_context( + &self, + agent_id: AgentId, + message: &str, + kernel_handle: Option>, + content_blocks: Option>, + sender_id: Option, + sender_name: Option, + callback_context: Option, ) -> KernelResult { // Acquire per-agent lock to serialize concurrent messages for the same agent. // This prevents session corruption when multiple messages arrive in quick @@ -1931,6 +1986,7 @@ impl OpenFangKernel { content_blocks, sender_id, sender_name, + callback_context, ) .await }; @@ -2378,6 +2434,7 @@ impl OpenFangKernel { ctx_window, Some(&kernel_clone.process_manager), content_blocks, + None, // callback_context (streaming path is API/CLI driven, not channel) ) .await; @@ -2637,6 +2694,7 @@ impl OpenFangKernel { content_blocks: Option>, sender_id: Option, sender_name: Option, + callback_context: Option, ) -> KernelResult { // Check metering quota before starting self.metering @@ -2969,6 +3027,7 @@ impl OpenFangKernel { ctx_window, Some(&self.process_manager), content_blocks, + callback_context, ) .await .map_err(KernelError::OpenFang)?; diff --git a/crates/openfang-runtime/src/agent_loop.rs b/crates/openfang-runtime/src/agent_loop.rs index 615fbfdb73..c4f9647217 100644 --- a/crates/openfang-runtime/src/agent_loop.rs +++ b/crates/openfang-runtime/src/agent_loop.rs @@ -312,6 +312,10 @@ pub async fn run_agent_loop( context_window_tokens: Option, process_manager: Option<&crate::process_manager::ProcessManager>, user_content_blocks: Option>, + // Per-invocation channel callback context — handed by the caller, not read + // from any global. Threaded into `execute_tool` for tools that deliver async + // results to channels. + callback_context: Option, ) -> OpenFangResult { info!(agent = %manifest.name, "Starting agent loop"); @@ -948,6 +952,7 @@ pub async fn run_agent_loop( tts_engine, docker_config, process_manager, + callback_context.as_ref(), ); let result = match timeout_opt { Some(timeout) => { @@ -1540,6 +1545,8 @@ pub async fn run_agent_loop_streaming( context_window_tokens: Option, process_manager: Option<&crate::process_manager::ProcessManager>, user_content_blocks: Option>, + // See `run_agent_loop`. + callback_context: Option, ) -> OpenFangResult { info!(agent = %manifest.name, "Starting streaming agent loop"); @@ -2157,6 +2164,7 @@ pub async fn run_agent_loop_streaming( tts_engine, docker_config, process_manager, + callback_context.as_ref(), ); let result = match timeout_opt { Some(timeout) => { @@ -3821,6 +3829,7 @@ mod tests { None, // context_window_tokens None, // process_manager None, // user_content_blocks + None, // callback_context ) .await .expect("Loop should complete without error"); @@ -3874,6 +3883,7 @@ mod tests { None, // context_window_tokens None, // process_manager None, // user_content_blocks + None, // callback_context ) .await .expect("Loop should complete without error"); @@ -3929,6 +3939,7 @@ mod tests { None, // context_window_tokens None, // process_manager None, // user_content_blocks + None, // callback_context ) .await .expect("Loop should complete without error"); @@ -3982,6 +3993,7 @@ mod tests { None, // context_window_tokens None, // process_manager None, // user_content_blocks + None, // callback_context ) .await .expect("Loop should complete without error"); @@ -4028,6 +4040,7 @@ mod tests { None, // context_window_tokens None, // process_manager None, // user_content_blocks + None, // callback_context ) .await .expect("Streaming loop should complete without error"); @@ -4152,6 +4165,7 @@ mod tests { None, // context_window_tokens None, // process_manager None, // user_content_blocks + None, // callback_context ) .await .expect("Loop should recover via retry"); @@ -4199,6 +4213,7 @@ mod tests { None, // context_window_tokens None, // process_manager None, // user_content_blocks + None, // callback_context ) .await .expect("Loop should complete with fallback"); @@ -4254,6 +4269,7 @@ mod tests { None, // context_window_tokens None, // process_manager None, // user_content_blocks + None, // callback_context ) .await .expect("Streaming loop should complete without error"); @@ -5230,6 +5246,7 @@ mod tests { None, // context_window_tokens None, // process_manager None, // user_content_blocks + None, // callback_context ) .await .expect("Agent loop should complete"); @@ -5300,6 +5317,7 @@ mod tests { None, None, None, + None, ) .await .expect("Agent loop should recover nested XML tool calls"); @@ -5372,6 +5390,7 @@ mod tests { None, None, None, // user_content_blocks + None, // callback_context ) .await .expect("Normal loop should complete"); @@ -5435,6 +5454,7 @@ mod tests { None, // context_window_tokens None, // process_manager None, // user_content_blocks + None, // callback_context ) .await .expect("Streaming loop should complete"); diff --git a/crates/openfang-runtime/src/kernel_handle.rs b/crates/openfang-runtime/src/kernel_handle.rs index ec57efe1f9..42ba282ac0 100644 --- a/crates/openfang-runtime/src/kernel_handle.rs +++ b/crates/openfang-runtime/src/kernel_handle.rs @@ -212,6 +212,26 @@ pub trait KernelHandle: Send + Sync { Err("Channel send not available".to_string()) } + /// Inject an async-tool result back into the originating channel. + /// + /// Used by `tool_a2a_send_async` (in the async-dispatch PR) when a background + /// task completes and its result must be delivered to the channel/user that + /// initiated the request. The `context` is captured at spawn time by the + /// tool (handed in via the per-invocation `callback_context` parameter), so + /// no global lookup happens here and no cross-user bleed is possible. + /// + /// The implementation should tag `result_text` as untrusted external content + /// before passing it through the agent loop (prompt-injection mitigation). + async fn inject_async_callback( + &self, + context: openfang_types::ChannelCallbackContext, + agent_name: &str, + result_text: &str, + ) -> Result<(), String> { + let _ = (context, agent_name, result_text); + Err("Async callback injection not available".to_string()) + } + /// Send media content (image/file) to a user on a named channel adapter. /// `media_type` is "image" or "file", `media_url` is the URL, `caption` is optional text. /// When `thread_id` is provided, the media is sent as a thread reply. diff --git a/crates/openfang-runtime/src/tool_runner.rs b/crates/openfang-runtime/src/tool_runner.rs index 16695616ee..953b4f6d8b 100644 --- a/crates/openfang-runtime/src/tool_runner.rs +++ b/crates/openfang-runtime/src/tool_runner.rs @@ -124,6 +124,12 @@ pub async fn execute_tool( tts_engine: Option<&crate::tts::TtsEngine>, docker_config: Option<&openfang_types::config::DockerSandboxConfig>, process_manager: Option<&crate::process_manager::ProcessManager>, + // Per-invocation channel callback context. Passed explicitly rather than + // stored on the kernel so concurrent dispatches for the same agent cannot + // see one another's context. Tools that deliver async results (e.g. + // `a2a_send_async` in the async-dispatch PR) capture this value at spawn + // time and own it for the lifetime of the background task. + _callback_context: Option<&openfang_types::ChannelCallbackContext>, ) -> ToolResult { // Normalize the tool name through compat mappings so LLM-hallucinated aliases // (e.g. "fs-write" → "file_write") resolve to the canonical OpenFang name. @@ -3925,6 +3931,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!( @@ -3954,6 +3961,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!(result.is_error); @@ -3980,6 +3988,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!(result.is_error); @@ -4006,6 +4015,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!(result.is_error); @@ -4068,6 +4078,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!( @@ -4098,6 +4109,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!(result.is_error); @@ -4124,6 +4136,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; // web_search now attempts a real fetch; may succeed or fail depending on network @@ -4150,6 +4163,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!(result.is_error); @@ -4176,6 +4190,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!(result.is_error); @@ -4203,6 +4218,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!(result.is_error); @@ -4234,6 +4250,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; // Should fail for file-not-found, NOT for permission denied @@ -4279,6 +4296,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; // Should NOT be the capability-enforcement "Permission denied" — it should @@ -4314,6 +4332,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!(result.is_error); @@ -4483,6 +4502,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!(result.is_error); @@ -4528,6 +4548,7 @@ mod tests { None, // tts_engine None, // docker_config None, // process_manager + None, // callback_context ) .await; assert!(result.is_error); diff --git a/crates/openfang-types/src/lib.rs b/crates/openfang-types/src/lib.rs index de9df5975d..a0a177be9b 100644 --- a/crates/openfang-types/src/lib.rs +++ b/crates/openfang-types/src/lib.rs @@ -23,6 +23,26 @@ pub mod tool; pub mod tool_compat; pub mod webhook; +/// Context for delivering async agent results back to the originating channel. +/// +/// Threaded as an optional parameter through `send_message` rather than stored +/// globally — see the `kernel-context-threading` PR description for why the +/// global-map approach was rejected. +#[derive(Debug, Clone)] +pub struct ChannelCallbackContext { + /// Channel adapter name (e.g. "slack", "telegram", "email"). + pub channel_type: String, + /// Platform-specific recipient ID (e.g. Slack user ID, Telegram chat ID). + pub reply_to_platform_id: String, + /// Human-readable sender name (used for prompt context). + pub reply_to_display_name: String, + /// Smart-thread ID for threaded channels (Discord thread, Slack thread_ts, etc.). + pub thread_id: Option, + /// UUID of the agent that received the message — used by callbacks to + /// re-enter the agent loop. + pub agent_id: String, +} + /// Safely truncate a string to at most `max_bytes`, never splitting a UTF-8 char. pub fn truncate_str(s: &str, max_bytes: usize) -> &str { if s.len() <= max_bytes { From 624c493fb3b31c3ba26b1ec92cf966d9bc7ed237 Mon Sep 17 00:00:00 2001 From: Philippe Branchu Date: Sun, 31 May 2026 21:43:15 +0000 Subject: [PATCH 2/2] fix(deps): upgrade lettre to 0.11.22 (RUSTSEC-2026-0141) Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e880570247..f30243e281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,7 +139,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -150,7 +150,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -893,7 +893,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1555,7 +1555,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1805,7 +1805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2743,7 +2743,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3255,9 +3255,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lettre" -version = "0.11.21" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7" +checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" dependencies = [ "async-trait", "base64 0.22.1", @@ -3739,7 +3739,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5105,7 +5105,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -5143,7 +5143,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -5709,7 +5709,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5768,7 +5768,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6314,7 +6314,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7050,7 +7050,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7622,7 +7622,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8495,7 +8495,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]]