mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 23:05:08 +00:00
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:
Generated
+201
-289
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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"] }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user