Merge pull request #1041 from Hypn0sis/chore/security-deps

fix(deps): upgrade wasmtime 41->43 and rumqttc 0.24->0.25 to resolve active CVEs

Addresses RUSTSEC advisories surfaced by cargo-audit on main. wasmtime
jump required matching adjustments in sandbox.rs error types; rumqttc
switched to `default-features = false` + `use-native-tls` to drop
the vulnerable `rustls-webpki 0.102` path. CI green across Check/
Test/Clippy/Format/Security-Audit on all three platforms.
This commit is contained in:
Jaber Jaber
2026-04-17 21:21:17 +03:00
committed by GitHub
14 changed files with 372 additions and 434 deletions
Generated
+201 -289
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -83,7 +83,7 @@ tokio-tungstenite = { version = "0.24", default-features = false, features = ["c
url = "2"
# WASM sandbox
wasmtime = "41"
wasmtime = "43"
# HTTP server (for API daemon)
axum = { version = "0.8", features = ["ws", "multipart"] }
@@ -147,7 +147,7 @@ native-tls = { version = "0.2", features = ["vendored"] }
mailparse = "0.16"
# MQTT client
rumqttc = "0.24"
rumqttc = { version = "0.25", default-features = false, features = ["use-native-tls"] }
# OpenSSL (vendored = statically compiled, no runtime libssl dependency on Linux)
openssl = { version = "0.10", features = ["vendored"] }
+1 -4
View File
@@ -11,10 +11,7 @@ pub(crate) fn percent_decode(input: &str) -> String {
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let (Some(hi), Some(lo)) = (
hex_val(bytes[i + 1]),
hex_val(bytes[i + 2]),
) {
if let (Some(hi), Some(lo)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) {
out.push(hi << 4 | lo);
i += 3;
continue;
+1 -1
View File
@@ -169,7 +169,7 @@ pub async fn agent_ws(
let query_auth = uri
.query()
.and_then(|q| q.split('&').find_map(|pair| pair.strip_prefix("token=")))
.map(|raw| crate::percent_decode(raw))
.map(crate::percent_decode)
.map(|token| ct_eq(&token, api_key))
.unwrap_or(false);
+6 -2
View File
@@ -2466,7 +2466,9 @@ decay_rate = 0.05
if !json {
ui::check_ok("GitHub Copilot (authenticated via device flow)");
}
checks.push(serde_json::json!({"check": "provider", "name": "GitHub Copilot", "status": "ok"}));
checks.push(
serde_json::json!({"check": "provider", "name": "GitHub Copilot", "status": "ok"}),
);
}
}
@@ -4992,7 +4994,9 @@ fn cmd_config_set_key(provider: &str) {
ui::error(&format!("Failed to create async runtime: {e}"));
std::process::exit(1);
});
match rt.block_on(openfang_runtime::drivers::copilot::run_interactive_setup(&openfang_dir)) {
match rt.block_on(openfang_runtime::drivers::copilot::run_interactive_setup(
&openfang_dir,
)) {
Ok(_) => {
ui::success("GitHub Copilot configured successfully");
ui::hint("Restart the daemon: openfang stop && openfang start");
@@ -312,7 +312,10 @@ enum CopilotAuthStatus {
}
enum CopilotAuthEvent {
DeviceCode { user_code: String, verification_uri: String },
DeviceCode {
user_code: String,
verification_uri: String,
},
Authenticated,
Models(Vec<String>),
}
@@ -648,8 +651,7 @@ pub fn run() -> InitResult {
let (test_tx, test_rx) = std::sync::mpsc::channel::<bool>();
let (migrate_tx, migrate_rx) =
std::sync::mpsc::channel::<Result<openfang_migrate::report::MigrationReport, String>>();
let (copilot_tx, copilot_rx) =
std::sync::mpsc::channel::<Result<CopilotAuthEvent, String>>();
let (copilot_tx, copilot_rx) = std::sync::mpsc::channel::<Result<CopilotAuthEvent, String>>();
let result = loop {
terminal
@@ -660,7 +662,10 @@ pub fn run() -> InitResult {
if state.step == Step::CopilotAuth {
while let Ok(event) = copilot_rx.try_recv() {
match event {
Ok(CopilotAuthEvent::DeviceCode { user_code, verification_uri }) => {
Ok(CopilotAuthEvent::DeviceCode {
user_code,
verification_uri,
}) => {
state.copilot_user_code = user_code;
state.copilot_verification_uri = verification_uri;
state.copilot_auth_status = CopilotAuthStatus::WaitingForUser;
@@ -839,7 +844,8 @@ pub fn run() -> InitResult {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(e) => {
let _ = copilot_tx.send(Err(format!("Runtime error: {e}")));
let _ = copilot_tx
.send(Err(format!("Runtime error: {e}")));
return;
}
};
@@ -851,21 +857,31 @@ pub fn run() -> InitResult {
.map_err(|e| format!("HTTP error: {e}"));
let http = match http {
Ok(h) => h,
Err(e) => { let _ = copilot_tx.send(Err(e)); return; }
Err(e) => {
let _ = copilot_tx.send(Err(e));
return;
}
};
// Step 1: request device code
use openfang_runtime::drivers::copilot;
let device = match copilot::request_device_code(&http).await {
Ok(d) => d,
Err(e) => { let _ = copilot_tx.send(Err(e)); return; }
};
let device =
match copilot::request_device_code(&http).await {
Ok(d) => d,
Err(e) => {
let _ = copilot_tx.send(Err(e));
return;
}
};
// Send device code to TUI for display
let _ = copilot_tx.send(Ok(CopilotAuthEvent::DeviceCode {
user_code: device.user_code.clone(),
verification_uri: device.verification_uri.clone(),
}));
let _ =
copilot_tx.send(Ok(CopilotAuthEvent::DeviceCode {
user_code: device.user_code.clone(),
verification_uri: device
.verification_uri
.clone(),
}));
// Browser will be opened by user pressing Enter in TUI
@@ -874,9 +890,14 @@ pub fn run() -> InitResult {
&http,
&device.device_code,
device.interval,
).await {
)
.await
{
Ok(t) => t,
Err(e) => { let _ = copilot_tx.send(Err(e)); return; }
Err(e) => {
let _ = copilot_tx.send(Err(e));
return;
}
};
// Save tokens
@@ -885,16 +906,38 @@ pub fn run() -> InitResult {
return;
}
let _ = copilot_tx.send(Ok(CopilotAuthEvent::Authenticated));
let _ = copilot_tx
.send(Ok(CopilotAuthEvent::Authenticated));
// Step 3: fetch models
let ct = match copilot::exchange_copilot_token(&http, &tokens.access_token).await {
let ct = match copilot::exchange_copilot_token(
&http,
&tokens.access_token,
)
.await
{
Ok(ct) => ct,
Err(e) => { let _ = copilot_tx.send(Err(format!("Token exchange: {e}"))); return; }
Err(e) => {
let _ = copilot_tx
.send(Err(format!("Token exchange: {e}")));
return;
}
};
match copilot::fetch_models(&http, &ct.base_url, &ct.token).await {
Ok(models) => { let _ = copilot_tx.send(Ok(CopilotAuthEvent::Models(models))); }
Err(e) => { let _ = copilot_tx.send(Err(format!("Model fetch: {e}"))); }
match copilot::fetch_models(
&http,
&ct.base_url,
&ct.token,
)
.await
{
Ok(models) => {
let _ = copilot_tx
.send(Ok(CopilotAuthEvent::Models(models)));
}
Err(e) => {
let _ = copilot_tx
.send(Err(format!("Model fetch: {e}")));
}
}
});
});
@@ -924,12 +967,14 @@ pub fn run() -> InitResult {
}
}
KeyCode::Enter => {
if matches!(state.copilot_auth_status, CopilotAuthStatus::WaitingForUser) {
if !state.copilot_verification_uri.is_empty() {
let _ = openfang_runtime::drivers::copilot::open_verification_url(
&state.copilot_verification_uri,
);
}
if matches!(
state.copilot_auth_status,
CopilotAuthStatus::WaitingForUser
) && !state.copilot_verification_uri.is_empty()
{
let _ = openfang_runtime::drivers::copilot::open_verification_url(
&state.copilot_verification_uri,
);
}
}
_ => {}
@@ -1956,14 +2001,15 @@ fn draw_copilot_auth(f: &mut Frame, area: Rect, state: &mut State) {
Constraint::Length(1), // code value
Constraint::Length(1), // blank
Constraint::Length(1), // url
Constraint::Min(0), // spacer
Constraint::Min(0), // spacer
Constraint::Length(1), // hint
])
.split(area);
let title = Paragraph::new(Line::from(vec![
Span::styled(" GitHub Copilot Authentication", Style::default().fg(theme::ACCENT)),
]));
let title = Paragraph::new(Line::from(vec![Span::styled(
" GitHub Copilot Authentication",
Style::default().fg(theme::ACCENT),
)]));
f.render_widget(title, chunks[0]);
let spinner = theme::SPINNER_FRAMES[state.tick % theme::SPINNER_FRAMES.len()];
@@ -1985,9 +2031,7 @@ fn draw_copilot_auth(f: &mut Frame, area: Rect, state: &mut State) {
]));
f.render_widget(line1, chunks[2]);
let code_label = Paragraph::new(Line::from(vec![
Span::raw(" Enter this code:"),
]));
let code_label = Paragraph::new(Line::from(vec![Span::raw(" Enter this code:")]));
f.render_widget(code_label, chunks[5]);
let code_value = Paragraph::new(Line::from(vec![
@@ -2007,9 +2051,10 @@ fn draw_copilot_auth(f: &mut Frame, area: Rect, state: &mut State) {
]));
f.render_widget(url, chunks[8]);
let hint = Paragraph::new(Line::from(vec![
Span::styled(" [Enter] Open browser", theme::dim_style()),
]));
let hint = Paragraph::new(Line::from(vec![Span::styled(
" [Enter] Open browser",
theme::dim_style(),
)]));
f.render_widget(hint, chunks[10]);
}
CopilotAuthStatus::FetchingModels => {
@@ -2040,9 +2085,10 @@ fn draw_copilot_auth(f: &mut Frame, area: Rect, state: &mut State) {
]));
f.render_widget(line, chunks[2]);
let hint = Paragraph::new(Line::from(vec![
Span::styled(" Esc to go back", theme::dim_style()),
]));
let hint = Paragraph::new(Line::from(vec![Span::styled(
" Esc to go back",
theme::dim_style(),
)]));
f.render_widget(hint, chunks[10]);
}
}
+4 -4
View File
@@ -514,7 +514,7 @@ impl OpenFangKernel {
fn fetch_copilot_models(openfang_dir: &Path) -> Result<Vec<String>, String> {
use openfang_runtime::drivers::copilot;
let tokens = copilot::PersistedTokens::load(&openfang_dir.to_path_buf())
let tokens = copilot::PersistedTokens::load(openfang_dir)
.ok_or("No persisted Copilot tokens found")?;
let fetch = async {
@@ -531,9 +531,9 @@ impl OpenFangKernel {
// Otherwise (CLI commands), create a new one.
if let Ok(handle) = tokio::runtime::Handle::try_current() {
std::thread::scope(|s| {
s.spawn(|| {
handle.block_on(fetch)
}).join().unwrap_or(Err("Thread panicked".to_string()))
s.spawn(|| handle.block_on(fetch))
.join()
.unwrap_or(Err("Thread panicked".to_string()))
})
} else {
let rt = tokio::runtime::Runtime::new()
+4 -4
View File
@@ -435,10 +435,10 @@ async fn summarize_messages(
let safe_start = if conversation_text.is_char_boundary(start) {
start
} else {
// Find the nearest valid character boundary moving upward
(start..conversation_text.len())
.find(|&i| conversation_text.is_char_boundary(i))
.unwrap_or(conversation_text.len())
// Find the nearest valid character boundary moving upward
(start..conversation_text.len())
.find(|&i| conversation_text.is_char_boundary(i))
.unwrap_or(conversation_text.len())
};
conversation_text = conversation_text[safe_start..].to_string();
}
+46 -52
View File
@@ -10,7 +10,7 @@
//! `config.toml`. The driver handles the rest — device flow, token persistence,
//! refresh, and Copilot API token exchange — automatically.
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
@@ -55,6 +55,7 @@ const OAUTH_SCOPES: &str = "copilot";
const TOKEN_FILE_NAME: &str = ".copilot-tokens.json";
/// Device flow polling interval (seconds) — GitHub default is 5.
#[allow(dead_code)]
const DEVICE_FLOW_POLL_INTERVAL: Duration = Duration::from_secs(5);
/// Maximum time to wait for user to authorize the device flow.
@@ -83,14 +84,14 @@ impl PersistedTokens {
}
/// Load from the OpenFang data directory.
pub fn load(openfang_dir: &PathBuf) -> Option<Self> {
pub fn load(openfang_dir: &Path) -> Option<Self> {
let path = openfang_dir.join(TOKEN_FILE_NAME);
let data = std::fs::read_to_string(&path).ok()?;
serde_json::from_str(&data).ok()
}
/// Persist to the OpenFang data directory with restricted permissions.
pub fn save(&self, openfang_dir: &PathBuf) -> Result<(), String> {
pub fn save(&self, openfang_dir: &Path) -> Result<(), String> {
let path = openfang_dir.join(TOKEN_FILE_NAME);
let json = serde_json::to_string_pretty(self)
.map_err(|e| format!("Failed to serialize tokens: {e}"))?;
@@ -138,6 +139,7 @@ impl CachedCopilotToken {
#[derive(Clone)]
struct CachedModels {
models: Vec<String>,
#[allow(dead_code)]
fetched_at: Instant,
}
@@ -169,6 +171,7 @@ struct OAuthTokenResponse {
#[serde(default)]
expires_in: Option<i64>,
#[serde(default)]
#[allow(dead_code)]
refresh_token_expires_in: Option<i64>,
#[serde(default)]
error: Option<String>,
@@ -177,9 +180,7 @@ struct OAuthTokenResponse {
}
/// Request a device code from GitHub using the Copilot client ID.
pub async fn request_device_code(
client: &reqwest::Client,
) -> Result<DeviceCodeResponse, String> {
pub async fn request_device_code(client: &reqwest::Client) -> Result<DeviceCodeResponse, String> {
let resp = client
.post(GITHUB_DEVICE_CODE_URL)
.header("Accept", "application/json")
@@ -259,9 +260,7 @@ pub async fn poll_for_token(
let access_token = token_resp
.access_token
.ok_or("Missing access_token in response")?;
let refresh_token = token_resp
.refresh_token
.unwrap_or_default(); // Empty if token expiration is disabled on the OAuth App
let refresh_token = token_resp.refresh_token.unwrap_or_default(); // Empty if token expiration is disabled on the OAuth App
let expires_in = token_resp.expires_in.unwrap_or(0); // 0 = non-expiring
return Ok(PersistedTokens {
@@ -361,7 +360,7 @@ pub async fn exchange_copilot_token(
.ok_or("Missing 'token' field in Copilot response")?;
let expires_at_unix = body.get("expires_at").and_then(|v| v.as_i64()).unwrap_or(0);
let ttl_secs = (expires_at_unix - unix_now() as i64).max(60) as u64;
let ttl_secs = (expires_at_unix - unix_now()).max(60) as u64;
// Extract base URL from endpoints.api or proxy-ep in the token.
let base_url = body
@@ -444,7 +443,11 @@ pub async fn fetch_models(
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|m| m.get("id").and_then(|id| id.as_str()).map(|s| s.to_string()))
.filter_map(|m| {
m.get("id")
.and_then(|id| id.as_str())
.map(|s| s.to_string())
})
.collect()
})
.unwrap_or_default();
@@ -514,12 +517,7 @@ impl CopilotDriver {
};
if let Some(ref rt) = refresh_token {
match refresh_access_token(
&self.http_client,
rt,
)
.await
{
match refresh_access_token(&self.http_client, rt).await {
Ok(new_tokens) => {
info!("Copilot access token refreshed successfully");
if let Err(e) = new_tokens.save(&self.openfang_dir) {
@@ -547,8 +545,9 @@ impl CopilotDriver {
}
/// Ensure we have a valid Copilot API token (tid=…).
async fn ensure_copilot_token(&self) -> Result<CachedCopilotToken, crate::llm_driver::LlmError>
{
async fn ensure_copilot_token(
&self,
) -> Result<CachedCopilotToken, crate::llm_driver::LlmError> {
// Check cache.
{
let lock = self.copilot_token.lock().unwrap_or_else(|e| e.into_inner());
@@ -595,13 +594,16 @@ impl CopilotDriver {
&self,
copilot_token: &CachedCopilotToken,
) -> Result<Vec<String>, crate::llm_driver::LlmError> {
let models =
fetch_models(&self.http_client, &copilot_token.base_url, &copilot_token.token)
.await
.map_err(|e| crate::llm_driver::LlmError::Api {
status: 500,
message: format!("Failed to fetch model list: {e}"),
})?;
let models = fetch_models(
&self.http_client,
&copilot_token.base_url,
&copilot_token.token,
)
.await
.map_err(|e| crate::llm_driver::LlmError::Api {
status: 500,
message: format!("Failed to fetch model list: {e}"),
})?;
let mut lock = self.models.lock().unwrap_or_else(|e| e.into_inner());
*lock = Some(CachedModels {
@@ -638,10 +640,7 @@ impl CopilotDriver {
execute: F,
) -> Result<crate::llm_driver::CompletionResponse, crate::llm_driver::LlmError>
where
F: Fn(
super::openai::OpenAIDriver,
crate::llm_driver::CompletionRequest,
) -> Fut,
F: Fn(super::openai::OpenAIDriver, crate::llm_driver::CompletionRequest) -> Fut,
Fut: std::future::Future<
Output = Result<crate::llm_driver::CompletionResponse, crate::llm_driver::LlmError>,
>,
@@ -652,9 +651,10 @@ impl CopilotDriver {
match execute(driver, request.clone()).await {
Ok(resp) => Ok(resp),
Err(crate::llm_driver::LlmError::Api { status, ref message })
if status == 400 && message.contains("model_not_supported") =>
{
Err(crate::llm_driver::LlmError::Api {
status,
ref message,
}) if status == 400 && message.contains("model_not_supported") => {
// Refresh model list so subsequent calls have updated info.
warn!(
model = %request.model,
@@ -683,9 +683,10 @@ impl crate::llm_driver::LlmDriver for CopilotDriver {
&self,
request: crate::llm_driver::CompletionRequest,
) -> Result<crate::llm_driver::CompletionResponse, crate::llm_driver::LlmError> {
self.execute_with_model_retry(request, |driver, req| async move {
driver.complete(req).await
})
self.execute_with_model_retry(
request,
|driver, req| async move { driver.complete(req).await },
)
.await
}
@@ -700,9 +701,10 @@ impl crate::llm_driver::LlmDriver for CopilotDriver {
match driver.stream(request.clone(), tx.clone()).await {
Ok(resp) => Ok(resp),
Err(crate::llm_driver::LlmError::Api { status, ref message })
if status == 400 && message.contains("model_not_supported") =>
{
Err(crate::llm_driver::LlmError::Api {
status,
ref message,
}) if status == 400 && message.contains("model_not_supported") => {
warn!(
model = %request.model,
"Model not supported — refreshing model catalog"
@@ -732,9 +734,7 @@ impl crate::llm_driver::LlmDriver for CopilotDriver {
///
/// Called from `openfang config set-key github-copilot`, `openfang init`,
/// `openfang onboard`, and `openfang configure`.
pub async fn run_interactive_setup(
openfang_dir: &PathBuf,
) -> Result<PersistedTokens, String> {
pub async fn run_interactive_setup(openfang_dir: &Path) -> Result<PersistedTokens, String> {
run_device_flow(openfang_dir).await
}
@@ -742,9 +742,7 @@ pub async fn run_interactive_setup(
///
/// Prints the user code and verification URL, attempts to open the browser,
/// then polls until the user authorizes.
pub async fn run_device_flow(
openfang_dir: &PathBuf,
) -> Result<PersistedTokens, String> {
pub async fn run_device_flow(openfang_dir: &Path) -> Result<PersistedTokens, String> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
@@ -767,12 +765,7 @@ pub async fn run_device_flow(
println!(" Waiting for authorization...");
// Step 3: Poll for authorization.
let tokens = poll_for_token(
&client,
&device.device_code,
device.interval,
)
.await?;
let tokens = poll_for_token(&client, &device.device_code, device.interval).await?;
// Step 4: Persist.
tokens.save(openfang_dir)?;
@@ -782,6 +775,7 @@ pub async fn run_device_flow(
}
/// Read a line from stdin with a prompt. Used during interactive setup.
#[allow(dead_code)]
fn prompt_line(prompt: &str) -> Result<String, String> {
use std::io::{self, BufRead, Write};
print!("{prompt}");
@@ -825,7 +819,7 @@ pub fn open_verification_url(url: &str) -> bool {
}
/// Check if Copilot OAuth tokens exist on disk.
pub fn copilot_auth_available(openfang_dir: &PathBuf) -> bool {
pub fn copilot_auth_available(openfang_dir: &Path) -> bool {
openfang_dir.join(TOKEN_FILE_NAME).exists()
}
+1 -1
View File
@@ -19,7 +19,7 @@ use openfang_types::model_catalog::{
AI21_BASE_URL, ANTHROPIC_BASE_URL, AZURE_OPENAI_BASE_URL, CEREBRAS_BASE_URL, CHUTES_BASE_URL,
COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, GROQ_BASE_URL,
HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL, LEMONADE_BASE_URL, LMSTUDIO_BASE_URL,
MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, NVIDIA_NIM_BASE_URL, NOVITA_BASE_URL,
MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, NOVITA_BASE_URL, NVIDIA_NIM_BASE_URL,
OLLAMA_BASE_URL, OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL,
QWEN_BASE_URL, REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL,
VLLM_BASE_URL, VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL,
@@ -276,7 +276,7 @@ struct OaiUsage {
/// Strip trailing empty assistant messages without tool calls.
/// Some API proxies reject empty assistant messages as "prefill".
fn strip_trailing_empty_assistant(messages: &mut Vec<OaiMessage>) {
while messages.last().map_or(false, |m| {
while messages.last().is_some_and(|m| {
m.role == "assistant"
&& m.tool_calls.is_none()
&& match &m.content {
+9 -9
View File
@@ -286,18 +286,18 @@ impl WasmSandbox {
|mut caller: Caller<'_, GuestState>,
request_ptr: i32,
request_len: i32|
-> Result<i64, anyhow::Error> {
-> Result<i64, Error> {
// Read request from guest memory
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.ok_or_else(|| anyhow::anyhow!("no memory export"))?;
.ok_or_else(|| format_err!("no memory export"))?;
let data = memory.data(&caller);
let start = request_ptr as usize;
let end = start + request_len as usize;
if end > data.len() {
anyhow::bail!("host_call: request out of bounds");
bail!("host_call: request out of bounds");
}
let request_bytes = data[start..end].to_vec();
@@ -324,7 +324,7 @@ impl WasmSandbox {
let alloc_fn = caller
.get_export("alloc")
.and_then(|e| e.into_func())
.ok_or_else(|| anyhow::anyhow!("no alloc export"))?;
.ok_or_else(|| format_err!("no alloc export"))?;
let alloc_typed = alloc_fn.typed::<i32, i32>(&caller)?;
let ptr = alloc_typed.call(&mut caller, len)?;
@@ -332,12 +332,12 @@ impl WasmSandbox {
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.ok_or_else(|| anyhow::anyhow!("no memory export"))?;
.ok_or_else(|| format_err!("no memory export"))?;
let mem_data = memory.data_mut(&mut caller);
let dest_start = ptr as usize;
let dest_end = dest_start + response_bytes.len();
if dest_end > mem_data.len() {
anyhow::bail!("host_call: response exceeds memory bounds");
bail!("host_call: response exceeds memory bounds");
}
mem_data[dest_start..dest_end].copy_from_slice(&response_bytes);
@@ -356,17 +356,17 @@ impl WasmSandbox {
level: i32,
msg_ptr: i32,
msg_len: i32|
-> Result<(), anyhow::Error> {
-> Result<(), Error> {
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.ok_or_else(|| anyhow::anyhow!("no memory export"))?;
.ok_or_else(|| format_err!("no memory export"))?;
let data = memory.data(&caller);
let start = msg_ptr as usize;
let end = start + msg_len as usize;
if end > data.len() {
anyhow::bail!("host_log: pointer out of bounds");
bail!("host_log: pointer out of bounds");
}
let msg = std::str::from_utf8(&data[start..end]).unwrap_or("<invalid utf8>");
let agent_id = &caller.data().agent_id;
@@ -192,13 +192,13 @@ fn extract_shell_wrapper_commands(command: &str) -> Vec<String> {
let base_lower = base.to_lowercase();
// Also strip .exe suffix for Windows
let base_normalized = base_lower.strip_suffix(".exe").unwrap_or(&base_lower);
if !SHELL_WRAPPERS.iter().any(|w| *w == base_normalized) {
if !SHELL_WRAPPERS.contains(&base_normalized) {
return Vec::new();
}
// Find the inline flag and extract everything after it
for (wrappers, flag) in SHELL_INLINE_FLAGS {
if !wrappers.iter().any(|w| *w == base_normalized) {
if !wrappers.contains(&base_normalized) {
continue;
}
// Search for the flag in the command args (case-insensitive for PowerShell)
@@ -1089,10 +1089,7 @@ mod tests {
allowed_commands: vec!["powershell".to_string(), "Get-Process".to_string()],
..ExecPolicy::default()
};
let result = validate_command_allowlist(
r#"powershell -Command "Get-Process""#,
&policy,
);
let result = validate_command_allowlist(r#"powershell -Command "Get-Process""#, &policy);
assert!(
result.is_ok(),
"Get-Process should be allowed when in allowed_commands"
@@ -1106,10 +1103,8 @@ mod tests {
allowed_commands: vec!["cmd".to_string()],
..ExecPolicy::default()
};
let result = validate_command_allowlist(
r#"cmd /C "del /F /Q C:\temp\secret.txt""#,
&policy,
);
let result =
validate_command_allowlist(r#"cmd /C "del /F /Q C:\temp\secret.txt""#, &policy);
assert!(
result.is_err(),
"del inside cmd /C must be blocked when not in allowlist"
@@ -1123,10 +1118,7 @@ mod tests {
allowed_commands: vec!["bash".to_string()],
..ExecPolicy::default()
};
let result = validate_command_allowlist(
r#"bash -c "curl https://evil.com""#,
&policy,
);
let result = validate_command_allowlist(r#"bash -c "curl https://evil.com""#, &policy);
assert!(
result.is_err(),
"curl inside bash -c must be blocked when not in allowlist"
@@ -1141,10 +1133,7 @@ mod tests {
..ExecPolicy::default()
};
// "echo" is in safe_bins by default
let result = validate_command_allowlist(
r#"bash -c "echo hello""#,
&policy,
);
let result = validate_command_allowlist(r#"bash -c "echo hello""#, &policy);
assert!(
result.is_ok(),
"echo inside bash -c should be allowed (echo is in safe_bins)"
+2 -6
View File
@@ -173,13 +173,9 @@ fn normalize_schema_recursive(schema: &serde_json::Value) -> serde_json::Value {
// JSON Schema allows arrays without `items`, but the Gemini API rejects
// such schemas with INVALID_ARGUMENT. Inject a default string items schema
// so MCP tools (and any other source) don't break Gemini requests.
if result.get("type").and_then(|t| t.as_str()) == Some("array")
&& !result.contains_key("items")
if result.get("type").and_then(|t| t.as_str()) == Some("array") && !result.contains_key("items")
{
result.insert(
"items".to_string(),
serde_json::json!({"type": "string"}),
);
result.insert("items".to_string(), serde_json::json!({"type": "string"}));
}
serde_json::Value::Object(result)