batch fixes

This commit is contained in:
jaberjaber23
2026-03-02 20:52:57 +03:00
parent 516f163dfb
commit d3385f2cdc
9 changed files with 295 additions and 54 deletions
Generated
+14 -14
View File
@@ -3866,7 +3866,7 @@ dependencies = [
[[package]]
name = "openfang-api"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"async-trait",
"axum",
@@ -3902,7 +3902,7 @@ dependencies = [
[[package]]
name = "openfang-channels"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"async-trait",
"axum",
@@ -3933,7 +3933,7 @@ dependencies = [
[[package]]
name = "openfang-cli"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"clap",
"clap_complete",
@@ -3960,7 +3960,7 @@ dependencies = [
[[package]]
name = "openfang-desktop"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"axum",
"open",
@@ -3986,7 +3986,7 @@ dependencies = [
[[package]]
name = "openfang-extensions"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"aes-gcm",
"argon2",
@@ -4014,7 +4014,7 @@ dependencies = [
[[package]]
name = "openfang-hands"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"chrono",
"dashmap",
@@ -4031,7 +4031,7 @@ dependencies = [
[[package]]
name = "openfang-kernel"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"async-trait",
"chrono",
@@ -4067,7 +4067,7 @@ dependencies = [
[[package]]
name = "openfang-memory"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"async-trait",
"chrono",
@@ -4086,7 +4086,7 @@ dependencies = [
[[package]]
name = "openfang-migrate"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"chrono",
"dirs 6.0.0",
@@ -4105,7 +4105,7 @@ dependencies = [
[[package]]
name = "openfang-runtime"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"anyhow",
"async-trait",
@@ -4137,7 +4137,7 @@ dependencies = [
[[package]]
name = "openfang-skills"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"chrono",
"hex",
@@ -4159,7 +4159,7 @@ dependencies = [
[[package]]
name = "openfang-types"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"async-trait",
"chrono",
@@ -4178,7 +4178,7 @@ dependencies = [
[[package]]
name = "openfang-wire"
version = "0.2.8"
version = "0.2.9"
dependencies = [
"async-trait",
"chrono",
@@ -8790,7 +8790,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "xtask"
version = "0.2.8"
version = "0.2.9"
[[package]]
name = "yoke"
+1 -1
View File
@@ -18,7 +18,7 @@ members = [
]
[workspace.package]
version = "0.2.9"
version = "0.3.0"
edition = "2021"
license = "Apache-2.0 OR MIT"
repository = "https://github.com/RightNow-AI/openfang"
+4
View File
@@ -82,6 +82,10 @@ pub async fn auth(
// Public endpoints that don't require auth (dashboard needs these)
let path = request.uri().path();
if path == "/"
|| path == "/logo.png"
|| path == "/favicon.ico"
|| path == "/.well-known/agent.json"
|| path.starts_with("/a2a/")
|| path == "/api/health"
|| path == "/api/health/detail"
|| path == "/api/status"
+39 -7
View File
@@ -1123,7 +1123,7 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[
ChannelField { key: "bot_token_env", label: "Bot Token", field_type: FieldType::Secret, env_var: Some("DISCORD_BOT_TOKEN"), required: true, placeholder: "MTIz...", advanced: false },
ChannelField { key: "allowed_guilds", label: "Allowed Guild IDs", field_type: FieldType::List, env_var: None, required: false, placeholder: "123456789, 987654321", advanced: true },
ChannelField { key: "default_agent", label: "Default Agent", field_type: FieldType::Text, env_var: None, required: false, placeholder: "assistant", advanced: true },
ChannelField { key: "intents", label: "Intents Bitmask", field_type: FieldType::Number, env_var: None, required: false, placeholder: "33280", advanced: true },
ChannelField { key: "intents", label: "Intents Bitmask", field_type: FieldType::Number, env_var: None, required: false, placeholder: "37376", advanced: true },
],
setup_steps: &["Go to discord.com/developers/applications", "Create a bot and copy the token", "Paste it below"],
config_template: "[channels.discord]\nbot_token_env = \"DISCORD_BOT_TOKEN\"",
@@ -3022,13 +3022,18 @@ pub async fn clawhub_install(
)
}
Err(e) => {
let status = if e.to_string().contains("SecurityBlocked") {
let msg = format!("{e}");
let status = if msg.contains("SecurityBlocked") {
StatusCode::FORBIDDEN
} else if msg.contains("429") || msg.contains("rate limit") {
StatusCode::TOO_MANY_REQUESTS
} else if msg.contains("Network error") || msg.contains("returned 4") || msg.contains("returned 5") {
StatusCode::BAD_GATEWAY
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
tracing::warn!("ClawHub install failed: {e}");
(status, Json(serde_json::json!({"error": format!("{e}")})))
tracing::warn!("ClawHub install failed: {msg}");
(status, Json(serde_json::json!({"error": msg})))
}
}
}
@@ -6987,6 +6992,29 @@ pub async fn create_schedule(
}
let agent_id_str = req["agent_id"].as_str().unwrap_or("").to_string();
if agent_id_str.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Missing required field: agent_id"})),
);
}
// Validate agent exists (UUID or name lookup)
let agent_exists = if let Ok(aid) = agent_id_str.parse::<AgentId>() {
state.kernel.registry.get(aid).is_some()
} else {
state
.kernel
.registry
.list()
.iter()
.any(|a| a.name == agent_id_str)
};
if !agent_exists {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": format!("Agent not found: {agent_id_str}")})),
);
}
let message = req["message"].as_str().unwrap_or("").to_string();
let enabled = req.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
@@ -7160,10 +7188,14 @@ pub async fn run_schedule(
.unwrap_or("Scheduled task triggered manually.");
let name = schedule["name"].as_str().unwrap_or("(unnamed)");
// Find the target agent
// Find the target agent — require explicit agent_id, no silent fallback
let target_agent = if !agent_id_str.is_empty() {
if let Ok(aid) = agent_id_str.parse::<AgentId>() {
Some(aid)
if state.kernel.registry.get(aid).is_some() {
Some(aid)
} else {
None
}
} else {
state
.kernel
@@ -7174,7 +7206,7 @@ pub async fn run_schedule(
.map(|a| a.id)
}
} else {
state.kernel.registry.list().first().map(|a| a.id)
None
};
let target_agent = match target_agent {
+1 -1
View File
@@ -684,7 +684,7 @@ mod tests {
#[test]
fn test_discord_adapter_creation() {
let adapter = DiscordAdapter::new("test-token".to_string(), vec!["123".to_string(), "456".to_string()], 33280);
let adapter = DiscordAdapter::new("test-token".to_string(), vec!["123".to_string(), "456".to_string()], 37376);
assert_eq!(adapter.name(), "discord");
assert_eq!(adapter.channel_type(), ChannelType::Discord);
}
+137 -4
View File
@@ -405,6 +405,26 @@ enum HandCommands {
/// Hand ID.
id: String,
},
/// Check dependency status for a hand.
CheckDeps {
/// Hand ID.
id: String,
},
/// Install missing dependencies for a hand.
InstallDeps {
/// Hand ID.
id: String,
},
/// Pause a running hand instance.
Pause {
/// Instance ID (from `hand active`).
id: String,
},
/// Resume a paused hand instance.
Resume {
/// Instance ID (from `hand active`).
id: String,
},
}
#[derive(Subcommand)]
@@ -612,6 +632,9 @@ enum CronCommands {
spec: String,
/// Prompt to send when the job fires.
prompt: String,
/// Optional job name (auto-generated if omitted).
#[arg(long)]
name: Option<String>,
},
/// Delete a scheduled job.
Delete {
@@ -890,6 +913,10 @@ fn main() {
HandCommands::Activate { id } => cmd_hand_activate(&id),
HandCommands::Deactivate { id } => cmd_hand_deactivate(&id),
HandCommands::Info { id } => cmd_hand_info(&id),
HandCommands::CheckDeps { id } => cmd_hand_check_deps(&id),
HandCommands::InstallDeps { id } => cmd_hand_install_deps(&id),
HandCommands::Pause { id } => cmd_hand_pause(&id),
HandCommands::Resume { id } => cmd_hand_resume(&id),
},
Some(Commands::Config(sub)) => match sub {
ConfigCommands::Show => cmd_config_show(),
@@ -940,7 +967,8 @@ fn main() {
agent,
spec,
prompt,
} => cmd_cron_create(&agent, &spec, &prompt),
name,
} => cmd_cron_create(&agent, &spec, &prompt, name.as_deref()),
CronCommands::Delete { id } => cmd_cron_delete(&id),
CronCommands::Enable { id } => cmd_cron_toggle(&id, true),
CronCommands::Disable { id } => cmd_cron_toggle(&id, false),
@@ -3952,6 +3980,87 @@ fn cmd_hand_info(id: &str) {
);
}
fn cmd_hand_check_deps(id: &str) {
let base = require_daemon("hand check-deps");
let client = daemon_client();
let body = daemon_json(
client
.post(format!("{base}/api/hands/{id}/check-deps"))
.send(),
);
if body.get("error").is_some() {
ui::error(&format!(
"Failed: {}",
body["error"].as_str().unwrap_or("?")
));
} else {
println!(
"{}",
serde_json::to_string_pretty(&body).unwrap_or_default()
);
}
}
fn cmd_hand_install_deps(id: &str) {
let base = require_daemon("hand install-deps");
let client = daemon_client();
let body = daemon_json(
client
.post(format!("{base}/api/hands/{id}/install-deps"))
.send(),
);
if body.get("error").is_some() {
ui::error(&format!(
"Failed: {}",
body["error"].as_str().unwrap_or("?")
));
} else {
ui::success(&format!("Dependencies installed for hand '{id}'."));
if let Some(results) = body.get("results") {
println!(
"{}",
serde_json::to_string_pretty(results).unwrap_or_default()
);
}
}
}
fn cmd_hand_pause(id: &str) {
let base = require_daemon("hand pause");
let client = daemon_client();
let body = daemon_json(
client
.post(format!("{base}/api/hands/instances/{id}/pause"))
.send(),
);
if body.get("error").is_some() {
ui::error(&format!(
"Failed: {}",
body["error"].as_str().unwrap_or("?")
));
} else {
ui::success(&format!("Hand instance '{id}' paused."));
}
}
fn cmd_hand_resume(id: &str) {
let base = require_daemon("hand resume");
let client = daemon_client();
let body = daemon_json(
client
.post(format!("{base}/api/hands/instances/{id}/resume"))
.send(),
);
if body.get("error").is_some() {
ui::error(&format!(
"Failed: {}",
body["error"].as_str().unwrap_or("?")
));
} else {
ui::success(&format!("Hand instance '{id}' resumed."));
}
}
// ---------------------------------------------------------------------------
// Provider / API key helpers
// ---------------------------------------------------------------------------
@@ -5149,16 +5258,40 @@ fn cmd_cron_list(json: bool) {
}
}
fn cmd_cron_create(agent: &str, spec: &str, prompt: &str) {
fn cmd_cron_create(agent: &str, spec: &str, prompt: &str, explicit_name: Option<&str>) {
let base = require_daemon("cron create");
let client = daemon_client();
// Use explicit name if provided, otherwise derive from agent + prompt
let name = if let Some(n) = explicit_name {
n.to_string()
} else {
let short_prompt: String = prompt
.split_whitespace()
.take(4)
.collect::<Vec<_>>()
.join("-")
.chars()
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
.take(64)
.collect();
format!("{}-{}", agent, if short_prompt.is_empty() { "job" } else { &short_prompt })
};
let body = daemon_json(
client
.post(format!("{base}/api/cron/jobs"))
.json(&serde_json::json!({
"agent_id": agent,
"cron_expr": spec,
"prompt": prompt,
"name": name,
"schedule": {
"kind": "cron",
"expr": spec
},
"action": {
"kind": "agent_turn",
"message": prompt
}
}))
.send(),
);
+20 -6
View File
@@ -532,7 +532,11 @@ impl OpenFangKernel {
let driver_config = DriverConfig {
provider: config.default_model.provider.clone(),
api_key: std::env::var(&config.default_model.api_key_env).ok(),
base_url: config.default_model.base_url.clone(),
base_url: config
.default_model
.base_url
.clone()
.or_else(|| config.provider_urls.get(&config.default_model.provider).cloned()),
};
let primary_driver = drivers::create_driver(&driver_config)
.map_err(|e| KernelError::BootFailed(format!("LLM driver init failed: {e}")))?;
@@ -548,7 +552,10 @@ impl OpenFangKernel {
} else {
std::env::var(&fb.api_key_env).ok()
},
base_url: fb.base_url.clone(),
base_url: fb
.base_url
.clone()
.or_else(|| config.provider_urls.get(&fb.provider).cloned()),
};
match drivers::create_driver(&fb_config) {
Ok(d) => {
@@ -3698,10 +3705,14 @@ impl OpenFangKernel {
let base_url = if has_custom_url {
manifest.model.base_url.clone()
} else if agent_provider == default_provider {
self.config.default_model.base_url.clone()
self.config
.default_model
.base_url
.clone()
.or_else(|| self.config.provider_urls.get(agent_provider.as_str()).cloned())
} else {
// Let create_driver() use the target provider's default base URL
None
// Check provider_urls before falling back to hardcoded defaults
self.config.provider_urls.get(agent_provider.as_str()).cloned()
};
let driver_config = DriverConfig {
@@ -3727,7 +3738,10 @@ impl OpenFangKernel {
.api_key_env
.as_ref()
.and_then(|env| std::env::var(env).ok()),
base_url: fb.base_url.clone(),
base_url: fb
.base_url
.clone()
.or_else(|| self.config.provider_urls.get(&fb.provider).cloned()),
};
match drivers::create_driver(&config) {
Ok(d) => chain.push((d, fb.model.clone())),
+76 -18
View File
@@ -33,7 +33,12 @@ impl OpenAIDriver {
struct OaiRequest {
model: String,
messages: Vec<OaiMessage>,
max_tokens: u32,
/// Classic token limit field (used by most models).
#[serde(skip_serializing_if = "Option::is_none")]
max_tokens: Option<u32>,
/// New token limit field required by GPT-5 and o-series reasoning models.
#[serde(skip_serializing_if = "Option::is_none")]
max_completion_tokens: Option<u32>,
temperature: f32,
#[serde(skip_serializing_if = "Vec::is_empty")]
tools: Vec<OaiTool>,
@@ -43,6 +48,16 @@ struct OaiRequest {
stream: bool,
}
/// Returns true if a model uses `max_completion_tokens` instead of `max_tokens`.
fn uses_completion_tokens(model: &str) -> bool {
let m = model.to_lowercase();
m.starts_with("gpt-5")
|| m.starts_with("gpt5")
|| m.starts_with("o1")
|| m.starts_with("o3")
|| m.starts_with("o4")
}
#[derive(Debug, Serialize)]
struct OaiMessage {
role: String,
@@ -276,10 +291,16 @@ impl LlmDriver for OpenAIDriver {
Some(serde_json::json!("auto"))
};
let (mt, mct) = if uses_completion_tokens(&request.model) {
(None, Some(request.max_tokens))
} else {
(Some(request.max_tokens), None)
};
let mut oai_request = OaiRequest {
model: request.model.clone(),
messages: oai_messages,
max_tokens: request.max_tokens,
max_tokens: mt,
max_completion_tokens: mct,
temperature: request.temperature,
tools: oai_tools,
tool_choice,
@@ -339,16 +360,31 @@ impl LlmDriver for OpenAIDriver {
}
}
// GPT-5 / o-series: switch from max_tokens to max_completion_tokens
if status == 400
&& body.contains("max_tokens")
&& (body.contains("unsupported_parameter")
|| body.contains("max_completion_tokens"))
&& oai_request.max_tokens.is_some()
&& attempt < max_retries
{
let val = oai_request.max_tokens.unwrap();
warn!(model = %oai_request.model, "Switching to max_completion_tokens for this model");
oai_request.max_tokens = None;
oai_request.max_completion_tokens = Some(val);
continue;
}
// Auto-cap max_tokens when model rejects our value (e.g. Groq Maverick limit 8192)
if status == 400 && body.contains("max_tokens") && attempt < max_retries {
// Extract the limit from error: "must be less than or equal to `8192`"
let cap = extract_max_tokens_limit(&body).unwrap_or(oai_request.max_tokens / 2);
warn!(
old = oai_request.max_tokens,
new = cap,
"Auto-capping max_tokens to model limit"
);
oai_request.max_tokens = cap;
let current = oai_request.max_tokens.or(oai_request.max_completion_tokens).unwrap_or(4096);
let cap = extract_max_tokens_limit(&body).unwrap_or(current / 2);
warn!(old = current, new = cap, "Auto-capping max_tokens to model limit");
if oai_request.max_completion_tokens.is_some() {
oai_request.max_completion_tokens = Some(cap);
} else {
oai_request.max_tokens = Some(cap);
}
continue;
}
@@ -555,10 +591,16 @@ impl LlmDriver for OpenAIDriver {
Some(serde_json::json!("auto"))
};
let (mt, mct) = if uses_completion_tokens(&request.model) {
(None, Some(request.max_tokens))
} else {
(Some(request.max_tokens), None)
};
let mut oai_request = OaiRequest {
model: request.model.clone(),
messages: oai_messages,
max_tokens: request.max_tokens,
max_tokens: mt,
max_completion_tokens: mct,
temperature: request.temperature,
tools: oai_tools,
tool_choice,
@@ -620,15 +662,31 @@ impl LlmDriver for OpenAIDriver {
}
}
// GPT-5 / o-series: switch from max_tokens to max_completion_tokens
if status == 400
&& body.contains("max_tokens")
&& (body.contains("unsupported_parameter")
|| body.contains("max_completion_tokens"))
&& oai_request.max_tokens.is_some()
&& attempt < max_retries
{
let val = oai_request.max_tokens.unwrap();
warn!(model = %oai_request.model, "Switching to max_completion_tokens for this model (stream)");
oai_request.max_tokens = None;
oai_request.max_completion_tokens = Some(val);
continue;
}
// Auto-cap max_tokens when model rejects our value
if status == 400 && body.contains("max_tokens") && attempt < max_retries {
let cap = extract_max_tokens_limit(&body).unwrap_or(oai_request.max_tokens / 2);
warn!(
old = oai_request.max_tokens,
new = cap,
"Auto-capping max_tokens (stream)"
);
oai_request.max_tokens = cap;
let current = oai_request.max_tokens.or(oai_request.max_completion_tokens).unwrap_or(4096);
let cap = extract_max_tokens_limit(&body).unwrap_or(current / 2);
warn!(old = current, new = cap, "Auto-capping max_tokens (stream)");
if oai_request.max_completion_tokens.is_some() {
oai_request.max_completion_tokens = Some(cap);
} else {
oai_request.max_tokens = Some(cap);
}
continue;
}
+3 -3
View File
@@ -1556,7 +1556,7 @@ pub struct DiscordConfig {
pub allowed_guilds: Vec<String>,
/// Default agent name to route messages to.
pub default_agent: Option<String>,
/// Gateway intents bitmask (default: 33280 = GUILD_MESSAGES | MESSAGE_CONTENT).
/// Gateway intents bitmask (default: 37376 = GUILD_MESSAGES | DIRECT_MESSAGES | MESSAGE_CONTENT).
pub intents: u64,
/// Per-channel behavior overrides.
#[serde(default)]
@@ -1569,7 +1569,7 @@ impl Default for DiscordConfig {
bot_token_env: "DISCORD_BOT_TOKEN".to_string(),
allowed_guilds: vec![],
default_agent: None,
intents: 33280,
intents: 37376,
overrides: ChannelOverrides::default(),
}
}
@@ -3226,7 +3226,7 @@ mod tests {
let dc = DiscordConfig::default();
assert_eq!(dc.bot_token_env, "DISCORD_BOT_TOKEN");
assert!(dc.allowed_guilds.is_empty());
assert_eq!(dc.intents, 33280);
assert_eq!(dc.intents, 37376);
}
#[test]