Skip to content

Commit 6f36926

Browse files
authored
Merge pull request #119 from PyRo1121/fix/security-hardening-phase2
fix: harden 13 security vulnerabilities (phase 2)
2 parents 329b3a6 + 4e7c341 commit 6f36926

14 files changed

Lines changed: 405 additions & 63 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ fastembed = "4"
3333

3434
# Encoding
3535
base64 = "0.22"
36+
hex = "0.4"
3637

3738
# Logging and tracing
3839
tracing = "0.1"

src/agent/cortex_chat.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,16 @@ pub enum CortexChatEvent {
5252
/// Prompt hook that forwards tool events to an mpsc channel for SSE streaming.
5353
#[derive(Clone)]
5454
struct CortexChatHook {
55-
event_tx: mpsc::UnboundedSender<CortexChatEvent>,
55+
event_tx: mpsc::Sender<CortexChatEvent>,
5656
}
5757

5858
impl CortexChatHook {
59-
fn new(event_tx: mpsc::UnboundedSender<CortexChatEvent>) -> Self {
59+
fn new(event_tx: mpsc::Sender<CortexChatEvent>) -> Self {
6060
Self { event_tx }
6161
}
6262

63-
fn send(&self, event: CortexChatEvent) {
64-
let _ = self.event_tx.send(event);
63+
async fn send(&self, event: CortexChatEvent) {
64+
let _ = self.event_tx.send(event).await;
6565
}
6666
}
6767

@@ -75,7 +75,7 @@ impl<M: CompletionModel> PromptHook<M> for CortexChatHook {
7575
) -> ToolCallHookAction {
7676
self.send(CortexChatEvent::ToolStarted {
7777
tool: tool_name.to_string(),
78-
});
78+
}).await;
7979
ToolCallHookAction::Continue
8080
}
8181

@@ -95,7 +95,7 @@ impl<M: CompletionModel> PromptHook<M> for CortexChatHook {
9595
self.send(CortexChatEvent::ToolCompleted {
9696
tool: tool_name.to_string(),
9797
result_preview: preview,
98-
});
98+
}).await;
9999
HookAction::Continue
100100
}
101101

@@ -232,7 +232,7 @@ impl CortexChatSession {
232232
thread_id: &str,
233233
user_text: &str,
234234
channel_context_id: Option<&str>,
235-
) -> Result<mpsc::UnboundedReceiver<CortexChatEvent>, anyhow::Error> {
235+
) -> Result<mpsc::Receiver<CortexChatEvent>, anyhow::Error> {
236236
let _guard = self.send_lock.lock().await;
237237

238238
// Save the user message
@@ -271,7 +271,7 @@ impl CortexChatSession {
271271
.tool_server_handle(self.tool_server.clone())
272272
.build();
273273

274-
let (event_tx, event_rx) = mpsc::unbounded_channel();
274+
let (event_tx, event_rx) = mpsc::channel(256);
275275
let hook = CortexChatHook::new(event_tx.clone());
276276

277277
// Clone what the spawned task needs
@@ -296,7 +296,7 @@ impl CortexChatSession {
296296
.await;
297297
let _ = event_tx.send(CortexChatEvent::Done {
298298
full_text: response,
299-
});
299+
}).await;
300300
}
301301
Err(error) => {
302302
let error_text = format!("Cortex chat error: {error}");
@@ -305,7 +305,7 @@ impl CortexChatSession {
305305
.await;
306306
let _ = event_tx.send(CortexChatEvent::Error {
307307
message: error_text,
308-
});
308+
}).await;
309309
}
310310
}
311311
});

src/api/server.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ use super::{
88

99
use axum::Json;
1010
use axum::Router;
11-
use axum::extract::{Request, State};
11+
use axum::extract::{DefaultBodyLimit, Request, State};
1212
use axum::http::{StatusCode, Uri, header};
1313
use axum::middleware::{self, Next};
1414
use axum::response::{Html, IntoResponse, Response};
1515
use axum::routing::{delete, get, post, put};
1616
use rust_embed::Embed;
17+
use tower_http::cors::CorsLayer;
1718
use serde_json::json;
18-
use tower_http::cors::{Any, CorsLayer};
1919

2020
use std::net::SocketAddr;
2121
use std::sync::Arc;
@@ -36,9 +36,19 @@ pub async fn start_http_server(
3636
shutdown_rx: tokio::sync::watch::Receiver<bool>,
3737
) -> anyhow::Result<tokio::task::JoinHandle<()>> {
3838
let cors = CorsLayer::new()
39-
.allow_origin(Any)
40-
.allow_methods(Any)
41-
.allow_headers(Any);
39+
.allow_origin(tower_http::cors::AllowOrigin::mirror_request())
40+
.allow_methods([
41+
axum::http::Method::GET,
42+
axum::http::Method::POST,
43+
axum::http::Method::PUT,
44+
axum::http::Method::DELETE,
45+
axum::http::Method::OPTIONS,
46+
])
47+
.allow_headers([
48+
header::CONTENT_TYPE,
49+
header::AUTHORIZATION,
50+
header::ACCEPT,
51+
]);
4252

4353
let api_routes = Router::new()
4454
.route("/health", get(system::health))
@@ -157,6 +167,7 @@ pub async fn start_http_server(
157167
.nest("/api", api_routes)
158168
.fallback(static_handler)
159169
.layer(cors)
170+
.layer(DefaultBodyLimit::max(10 * 1024 * 1024)) // 10 MiB
160171
.with_state(state);
161172

162173
let listener = tokio::net::TcpListener::bind(bind).await?;

src/config.rs

Lines changed: 124 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,27 @@ impl<'de> serde::Deserialize<'de> for ApiType {
127127
}
128128

129129
/// Configuration for a single LLM provider.
130-
#[derive(Debug, Clone)]
130+
#[derive(Clone)]
131131
pub struct ProviderConfig {
132132
pub api_type: ApiType,
133133
pub base_url: String,
134134
pub api_key: String,
135135
pub name: Option<String>,
136136
}
137137

138+
impl std::fmt::Debug for ProviderConfig {
139+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140+
f.debug_struct("ProviderConfig")
141+
.field("api_type", &self.api_type)
142+
.field("base_url", &self.base_url)
143+
.field("api_key", &"[REDACTED]")
144+
.field("name", &self.name)
145+
.finish()
146+
}
147+
}
148+
138149
/// LLM provider credentials (instance-level).
139-
#[derive(Debug, Clone)]
150+
#[derive(Clone)]
140151
pub struct LlmConfig {
141152
pub anthropic_key: Option<String>,
142153
pub openai_key: Option<String>,
@@ -159,6 +170,32 @@ pub struct LlmConfig {
159170
pub providers: HashMap<String, ProviderConfig>,
160171
}
161172

173+
impl std::fmt::Debug for LlmConfig {
174+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175+
f.debug_struct("LlmConfig")
176+
.field("anthropic_key", &self.anthropic_key.as_ref().map(|_| "[REDACTED]"))
177+
.field("openai_key", &self.openai_key.as_ref().map(|_| "[REDACTED]"))
178+
.field("openrouter_key", &self.openrouter_key.as_ref().map(|_| "[REDACTED]"))
179+
.field("zhipu_key", &self.zhipu_key.as_ref().map(|_| "[REDACTED]"))
180+
.field("groq_key", &self.groq_key.as_ref().map(|_| "[REDACTED]"))
181+
.field("together_key", &self.together_key.as_ref().map(|_| "[REDACTED]"))
182+
.field("fireworks_key", &self.fireworks_key.as_ref().map(|_| "[REDACTED]"))
183+
.field("deepseek_key", &self.deepseek_key.as_ref().map(|_| "[REDACTED]"))
184+
.field("xai_key", &self.xai_key.as_ref().map(|_| "[REDACTED]"))
185+
.field("mistral_key", &self.mistral_key.as_ref().map(|_| "[REDACTED]"))
186+
.field("gemini_key", &self.gemini_key.as_ref().map(|_| "[REDACTED]"))
187+
.field("ollama_key", &self.ollama_key.as_ref().map(|_| "[REDACTED]"))
188+
.field("ollama_base_url", &self.ollama_base_url)
189+
.field("opencode_zen_key", &self.opencode_zen_key.as_ref().map(|_| "[REDACTED]"))
190+
.field("nvidia_key", &self.nvidia_key.as_ref().map(|_| "[REDACTED]"))
191+
.field("minimax_key", &self.minimax_key.as_ref().map(|_| "[REDACTED]"))
192+
.field("moonshot_key", &self.moonshot_key.as_ref().map(|_| "[REDACTED]"))
193+
.field("zai_coding_plan_key", &self.zai_coding_plan_key.as_ref().map(|_| "[REDACTED]"))
194+
.field("providers", &self.providers)
195+
.finish()
196+
}
197+
}
198+
162199
impl LlmConfig {
163200
/// Check if any provider configuration is set.
164201
pub fn has_any_key(&self) -> bool {
@@ -199,7 +236,7 @@ pub(crate) const GEMINI_PROVIDER_BASE_URL: &str =
199236
"https://generativelanguage.googleapis.com/v1beta/openai";
200237

201238
/// Defaults inherited by all agents. Individual agents can override any field.
202-
#[derive(Debug, Clone)]
239+
#[derive(Clone)]
203240
pub struct DefaultsConfig {
204241
pub routing: RoutingConfig,
205242
pub max_concurrent_branches: usize,
@@ -223,6 +260,31 @@ pub struct DefaultsConfig {
223260
pub worker_log_mode: crate::settings::WorkerLogMode,
224261
}
225262

263+
impl std::fmt::Debug for DefaultsConfig {
264+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265+
f.debug_struct("DefaultsConfig")
266+
.field("routing", &self.routing)
267+
.field("max_concurrent_branches", &self.max_concurrent_branches)
268+
.field("max_concurrent_workers", &self.max_concurrent_workers)
269+
.field("max_turns", &self.max_turns)
270+
.field("branch_max_turns", &self.branch_max_turns)
271+
.field("context_window", &self.context_window)
272+
.field("compaction", &self.compaction)
273+
.field("memory_persistence", &self.memory_persistence)
274+
.field("coalesce", &self.coalesce)
275+
.field("ingestion", &self.ingestion)
276+
.field("cortex", &self.cortex)
277+
.field("browser", &self.browser)
278+
.field("mcp", &self.mcp)
279+
.field("brave_search_key", &self.brave_search_key.as_ref().map(|_| "[REDACTED]"))
280+
.field("history_backfill_count", &self.history_backfill_count)
281+
.field("cron", &self.cron)
282+
.field("opencode", &self.opencode)
283+
.field("worker_log_mode", &self.worker_log_mode)
284+
.finish()
285+
}
286+
}
287+
226288
/// MCP server configuration.
227289
#[derive(Debug, Clone, PartialEq, Eq)]
228290
pub struct McpServerConfig {
@@ -786,7 +848,7 @@ pub struct MessagingConfig {
786848
pub twitch: Option<TwitchConfig>,
787849
}
788850

789-
#[derive(Debug, Clone)]
851+
#[derive(Clone)]
790852
pub struct DiscordConfig {
791853
pub enabled: bool,
792854
pub token: String,
@@ -796,6 +858,17 @@ pub struct DiscordConfig {
796858
pub allow_bot_messages: bool,
797859
}
798860

861+
impl std::fmt::Debug for DiscordConfig {
862+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
863+
f.debug_struct("DiscordConfig")
864+
.field("enabled", &self.enabled)
865+
.field("token", &"[REDACTED]")
866+
.field("dm_allowed_users", &self.dm_allowed_users)
867+
.field("allow_bot_messages", &self.allow_bot_messages)
868+
.finish()
869+
}
870+
}
871+
799872
/// A single slash command definition for the Slack adapter.
800873
///
801874
/// Maps a Slack slash command (e.g. `/ask`) to a target agent.
@@ -810,7 +883,7 @@ pub struct SlackCommandConfig {
810883
pub description: Option<String>,
811884
}
812885

813-
#[derive(Debug, Clone)]
886+
#[derive(Clone)]
814887
pub struct SlackConfig {
815888
pub enabled: bool,
816889
pub bot_token: String,
@@ -821,6 +894,18 @@ pub struct SlackConfig {
821894
pub commands: Vec<SlackCommandConfig>,
822895
}
823896

897+
impl std::fmt::Debug for SlackConfig {
898+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
899+
f.debug_struct("SlackConfig")
900+
.field("enabled", &self.enabled)
901+
.field("bot_token", &"[REDACTED]")
902+
.field("app_token", &"[REDACTED]")
903+
.field("dm_allowed_users", &self.dm_allowed_users)
904+
.field("commands", &self.commands)
905+
.finish()
906+
}
907+
}
908+
824909
/// Hot-reloadable Discord permission filters.
825910
///
826911
/// Derived from bindings + discord config. Shared with the Discord adapter
@@ -953,14 +1038,24 @@ impl DiscordPermissions {
9531038
}
9541039
}
9551040

956-
#[derive(Debug, Clone)]
1041+
#[derive(Clone)]
9571042
pub struct TelegramConfig {
9581043
pub enabled: bool,
9591044
pub token: String,
9601045
/// User IDs allowed to DM the bot. If empty, DMs are ignored entirely.
9611046
pub dm_allowed_users: Vec<String>,
9621047
}
9631048

1049+
impl std::fmt::Debug for TelegramConfig {
1050+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1051+
f.debug_struct("TelegramConfig")
1052+
.field("enabled", &self.enabled)
1053+
.field("token", &"[REDACTED]")
1054+
.field("dm_allowed_users", &self.dm_allowed_users)
1055+
.finish()
1056+
}
1057+
}
1058+
9641059
/// Hot-reloadable Telegram permission filters.
9651060
///
9661061
/// Shared with the Telegram adapter via `Arc<ArcSwap<..>>` for hot-reloading.
@@ -1015,7 +1110,7 @@ impl TelegramPermissions {
10151110
}
10161111
}
10171112

1018-
#[derive(Debug, Clone)]
1113+
#[derive(Clone)]
10191114
pub struct TwitchConfig {
10201115
pub enabled: bool,
10211116
pub username: String,
@@ -1026,6 +1121,18 @@ pub struct TwitchConfig {
10261121
pub trigger_prefix: Option<String>,
10271122
}
10281123

1124+
impl std::fmt::Debug for TwitchConfig {
1125+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1126+
f.debug_struct("TwitchConfig")
1127+
.field("enabled", &self.enabled)
1128+
.field("username", &self.username)
1129+
.field("oauth_token", &"[REDACTED]")
1130+
.field("channels", &self.channels)
1131+
.field("trigger_prefix", &self.trigger_prefix)
1132+
.finish()
1133+
}
1134+
}
1135+
10291136
/// Hot-reloadable Twitch permission filters.
10301137
///
10311138
/// Shared with the Twitch adapter via `Arc<ArcSwap<..>>` for hot-reloading.
@@ -2159,18 +2266,23 @@ impl Config {
21592266
.providers
21602267
.into_iter()
21612268
.map(|(provider_id, config)| {
2162-
(
2269+
let api_key = resolve_env_value(&config.api_key).ok_or_else(|| {
2270+
anyhow::anyhow!(
2271+
"failed to resolve API key for provider '{}'",
2272+
provider_id
2273+
)
2274+
})?;
2275+
Ok((
21632276
provider_id.to_lowercase(),
21642277
ProviderConfig {
21652278
api_type: config.api_type,
21662279
base_url: config.base_url,
2167-
api_key: resolve_env_value(&config.api_key)
2168-
.expect("Failed to resolve API key for provider"),
2280+
api_key,
21692281
name: config.name,
21702282
},
2171-
)
2283+
))
21722284
})
2173-
.collect(),
2285+
.collect::<anyhow::Result<_>>()?,
21742286
};
21752287

21762288
if let Some(anthropic_key) = llm.anthropic_key.clone() {

0 commit comments

Comments
 (0)