bugfixes batch

This commit is contained in:
jaberjaber23
2026-03-03 21:26:06 +03:00
parent 7c85308cf6
commit fe96cd1004
8 changed files with 159 additions and 110 deletions
Generated
+14 -14
View File
@@ -3866,7 +3866,7 @@ dependencies = [
[[package]]
name = "openfang-api"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"async-trait",
"axum",
@@ -3902,7 +3902,7 @@ dependencies = [
[[package]]
name = "openfang-channels"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"async-trait",
"axum",
@@ -3933,7 +3933,7 @@ dependencies = [
[[package]]
name = "openfang-cli"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"clap",
"clap_complete",
@@ -3960,7 +3960,7 @@ dependencies = [
[[package]]
name = "openfang-desktop"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"axum",
"open",
@@ -3986,7 +3986,7 @@ dependencies = [
[[package]]
name = "openfang-extensions"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"aes-gcm",
"argon2",
@@ -4014,7 +4014,7 @@ dependencies = [
[[package]]
name = "openfang-hands"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"chrono",
"dashmap",
@@ -4031,7 +4031,7 @@ dependencies = [
[[package]]
name = "openfang-kernel"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"async-trait",
"chrono",
@@ -4067,7 +4067,7 @@ dependencies = [
[[package]]
name = "openfang-memory"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"async-trait",
"chrono",
@@ -4086,7 +4086,7 @@ dependencies = [
[[package]]
name = "openfang-migrate"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"chrono",
"dirs 6.0.0",
@@ -4105,7 +4105,7 @@ dependencies = [
[[package]]
name = "openfang-runtime"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"anyhow",
"async-trait",
@@ -4137,7 +4137,7 @@ dependencies = [
[[package]]
name = "openfang-skills"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"chrono",
"hex",
@@ -4160,7 +4160,7 @@ dependencies = [
[[package]]
name = "openfang-types"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"async-trait",
"chrono",
@@ -4179,7 +4179,7 @@ dependencies = [
[[package]]
name = "openfang-wire"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"async-trait",
"chrono",
@@ -8791,7 +8791,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "xtask"
version = "0.3.5"
version = "0.3.6"
[[package]]
name = "yoke"
+1 -1
View File
@@ -18,7 +18,7 @@ members = [
]
[workspace.package]
version = "0.3.6"
version = "0.3.7"
edition = "2021"
license = "Apache-2.0 OR MIT"
repository = "https://github.com/RightNow-AI/openfang"
+32 -51
View File
@@ -1959,7 +1959,7 @@ pub async fn configure_channel(
let home = openfang_kernel::config::openfang_home();
let secrets_path = home.join("secrets.env");
let config_path = home.join("config.toml");
let mut config_fields: HashMap<String, String> = HashMap::new();
let mut config_fields: HashMap<String, (String, FieldType)> = HashMap::new();
for field_def in meta.fields {
let value = fields
@@ -1983,8 +1983,8 @@ pub async fn configure_channel(
std::env::set_var(env_var, value);
}
} else {
// Config field — collect for TOML write
config_fields.insert(field_def.key.to_string(), value.to_string());
// Config field — collect for TOML write with type info
config_fields.insert(field_def.key.to_string(), (value.to_string(), field_def.field_type));
}
}
@@ -2455,19 +2455,14 @@ pub async fn get_template(Path(name): Path<String>) -> impl IntoResponse {
// ---------------------------------------------------------------------------
/// GET /api/memory/agents/:id/kv — List KV pairs for an agent.
///
/// Note: memory_store tool writes to a shared namespace, so we read from that
/// same namespace regardless of which agent ID is in the URL.
pub async fn get_agent_kv(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Path(_id): Path<String>,
) -> impl IntoResponse {
let agent_id: AgentId = match id.parse() {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid agent ID"})),
);
}
};
let agent_id = openfang_kernel::kernel::shared_memory_agent_id();
match state.kernel.memory.list_kv(agent_id) {
Ok(pairs) => {
@@ -2478,7 +2473,7 @@ pub async fn get_agent_kv(
(StatusCode::OK, Json(serde_json::json!({"kv_pairs": kv})))
}
Err(e) => {
tracing::warn!("Memory list_kv failed for agent {id}: {e}");
tracing::warn!("Memory list_kv failed: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Memory operation failed"})),
@@ -2490,17 +2485,9 @@ pub async fn get_agent_kv(
/// GET /api/memory/agents/:id/kv/:key — Get a specific KV value.
pub async fn get_agent_kv_key(
State(state): State<Arc<AppState>>,
Path((id, key)): Path<(String, String)>,
Path((_id, key)): Path<(String, String)>,
) -> impl IntoResponse {
let agent_id: AgentId = match id.parse() {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid agent ID"})),
);
}
};
let agent_id = openfang_kernel::kernel::shared_memory_agent_id();
match state.kernel.memory.structured_get(agent_id, &key) {
Ok(Some(val)) => (
@@ -2512,7 +2499,7 @@ pub async fn get_agent_kv_key(
Json(serde_json::json!({"error": "Key not found"})),
),
Err(e) => {
tracing::warn!("Memory get failed for agent {id}, key '{key}': {e}");
tracing::warn!("Memory get failed for key '{key}': {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Memory operation failed"})),
@@ -2524,18 +2511,10 @@ pub async fn get_agent_kv_key(
/// PUT /api/memory/agents/:id/kv/:key — Set a KV value.
pub async fn set_agent_kv_key(
State(state): State<Arc<AppState>>,
Path((id, key)): Path<(String, String)>,
Path((_id, key)): Path<(String, String)>,
Json(body): Json<serde_json::Value>,
) -> impl IntoResponse {
let agent_id: AgentId = match id.parse() {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid agent ID"})),
);
}
};
let agent_id = openfang_kernel::kernel::shared_memory_agent_id();
let value = body.get("value").cloned().unwrap_or(body);
@@ -2545,7 +2524,7 @@ pub async fn set_agent_kv_key(
Json(serde_json::json!({"status": "stored", "key": key})),
),
Err(e) => {
tracing::warn!("Memory set failed for agent {id}, key '{key}': {e}");
tracing::warn!("Memory set failed for key '{key}': {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Memory operation failed"})),
@@ -2557,17 +2536,9 @@ pub async fn set_agent_kv_key(
/// DELETE /api/memory/agents/:id/kv/:key — Delete a KV value.
pub async fn delete_agent_kv_key(
State(state): State<Arc<AppState>>,
Path((id, key)): Path<(String, String)>,
Path((_id, key)): Path<(String, String)>,
) -> impl IntoResponse {
let agent_id: AgentId = match id.parse() {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid agent ID"})),
);
}
};
let agent_id = openfang_kernel::kernel::shared_memory_agent_id();
match state.kernel.memory.structured_delete(agent_id, &key) {
Ok(()) => (
@@ -2575,7 +2546,7 @@ pub async fn delete_agent_kv_key(
Json(serde_json::json!({"status": "deleted", "key": key})),
),
Err(e) => {
tracing::warn!("Memory delete failed for agent {id}, key '{key}': {e}");
tracing::warn!("Memory delete failed for key '{key}': {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Memory operation failed"})),
@@ -6788,7 +6759,7 @@ fn remove_secret_env(path: &std::path::Path, key: &str) -> Result<(), std::io::E
fn upsert_channel_config(
config_path: &std::path::Path,
channel_name: &str,
fields: &HashMap<String, String>,
fields: &HashMap<String, (String, FieldType)>,
) -> Result<(), Box<dyn std::error::Error>> {
let content = if config_path.exists() {
std::fs::read_to_string(config_path)?
@@ -6816,10 +6787,20 @@ fn upsert_channel_config(
.and_then(|v| v.as_table_mut())
.ok_or("channels is not a table")?;
// Build channel sub-table
// Build channel sub-table with correct TOML types
let mut ch_table = toml::map::Map::new();
for (k, v) in fields {
ch_table.insert(k.clone(), toml::Value::String(v.clone()));
for (k, (v, ft)) in fields {
let toml_val = match ft {
FieldType::Number => {
if let Ok(n) = v.parse::<i64>() {
toml::Value::Integer(n)
} else {
toml::Value::String(v.clone())
}
}
_ => toml::Value::String(v.clone()),
};
ch_table.insert(k.clone(), toml_val);
}
channels_table.insert(channel_name.to_string(), toml::Value::Table(ch_table));
@@ -589,6 +589,14 @@
<!-- Messages area -->
<div class="messages" id="messages" @dragover.prevent="dragOver = true" @dragleave="dragOver = false" @drop.prevent="handleDrop($event); dragOver = false">
<!-- Empty state: no agent selected -->
<template x-if="!currentAgent">
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center;padding:32px;opacity:0.8">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--text-dim)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom:16px;opacity:0.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<h3 style="margin:0 0 8px;font-size:16px;font-weight:600">Select an agent to start chatting</h3>
<p class="text-dim" style="font-size:13px;max-width:320px">Choose an agent from the sidebar or go to the Agents tab to create a new one.</p>
</div>
</template>
<!-- Message list -->
<template x-if="currentAgent">
<div>
@@ -3051,6 +3059,12 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]</pre>
<span class="text-xs text-dim ml-2" x-text="customModelStatus"></span>
</div>
<div class="text-xs text-dim mb-2" x-text="filteredModels.length + ' of ' + models.length + ' models'"></div>
<div x-show="!filteredModels.length && !settingsLoading" style="text-align:center;padding:32px 16px">
<div style="font-size:32px;margin-bottom:8px;opacity:0.5">&#x1F916;</div>
<h3 style="margin:0 0 4px;font-size:14px" x-text="models.length ? 'No models match your search' : 'No models available'"></h3>
<p class="text-xs text-dim" x-text="models.length ? 'Try a different search term or clear filters.' : 'Configure an LLM provider to see available models.'"></p>
<button class="btn btn-ghost btn-sm mt-2" x-show="models.length && (modelSearch || modelProviderFilter)" @click="modelSearch=''; modelProviderFilter=''">Clear Filters</button>
</div>
<div class="table-wrap" x-show="filteredModels.length">
<table>
<thead><tr><th>Model</th><th>Provider</th><th>Tier</th><th>Context</th><th>Input Cost</th><th>Output Cost</th><th>Status</th></tr></thead>
@@ -3078,6 +3092,12 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]</pre>
<input placeholder="Search tools..." x-model="toolSearch">
</div>
<div class="text-xs text-dim mb-2" x-text="filteredTools.length + ' of ' + tools.length + ' tools'"></div>
<div x-show="!filteredTools.length && !settingsLoading" style="text-align:center;padding:32px 16px">
<div style="font-size:32px;margin-bottom:8px;opacity:0.5">&#x1F527;</div>
<h3 style="margin:0 0 4px;font-size:14px" x-text="tools.length ? 'No tools match your search' : 'No tools available'"></h3>
<p class="text-xs text-dim" x-text="tools.length ? 'Try a different search term.' : 'Tools will appear once agents are configured.'"></p>
<button class="btn btn-ghost btn-sm mt-2" x-show="tools.length && toolSearch" @click="toolSearch=''">Clear Search</button>
</div>
<div class="table-wrap" x-show="filteredTools.length">
<table>
<thead><tr><th>Tool</th><th>Description</th></tr></thead>
+18 -12
View File
@@ -541,14 +541,14 @@ fn sanitize_telegram_html(text: &str) -> String {
];
let mut result = String::with_capacity(text.len());
let bytes = text.as_bytes();
let mut i = 0;
let mut chars = text.char_indices().peekable();
while i < bytes.len() {
if bytes[i] == b'<' {
while let Some(&(i, ch)) = chars.peek() {
if ch == '<' {
// Try to parse an HTML tag
if let Some(end) = text[i..].find('>') {
let tag_content = &text[i + 1..i + end]; // content between < and >
if let Some(end_offset) = text[i..].find('>') {
let tag_end = i + end_offset;
let tag_content = &text[i + 1..tag_end]; // content between < and >
let tag_name = tag_content
.trim_start_matches('/')
.split(|c: char| c.is_whitespace() || c == '/' || c == '>')
@@ -558,22 +558,28 @@ fn sanitize_telegram_html(text: &str) -> String {
if !tag_name.is_empty() && ALLOWED.contains(&tag_name.as_str()) {
// Allowed tag — keep as-is
result.push_str(&text[i..i + end + 1]);
result.push_str(&text[i..tag_end + 1]);
} else {
// Unknown tag — escape both brackets
result.push_str("&lt;");
result.push_str(&text[i + 1..i + end]);
result.push_str(tag_content);
result.push_str("&gt;");
}
i += end + 1;
// Advance past the whole tag
while let Some(&(j, _)) = chars.peek() {
chars.next();
if j >= tag_end {
break;
}
}
} else {
// No closing > — escape the lone <
result.push_str("&lt;");
i += 1;
chars.next();
}
} else {
result.push(text[i..].chars().next().unwrap());
i += text[i..].chars().next().unwrap().len_utf8();
result.push(ch);
chars.next();
}
}
+25 -7
View File
@@ -2099,7 +2099,20 @@ decay_rate = 0.05
all_ok = false;
}
// --- Check 4: Port 4200 availability ---
// --- Check 4: Port availability ---
// Read api_listen from config (default: 127.0.0.1:4200)
let api_listen = {
let cfg_path = openfang_dir.join("config.toml");
if cfg_path.exists() {
std::fs::read_to_string(&cfg_path)
.ok()
.and_then(|s| toml::from_str::<openfang_types::config::KernelConfig>(&s).ok())
.map(|c| c.api_listen)
.unwrap_or_else(|| "127.0.0.1:4200".to_string())
} else {
"127.0.0.1:4200".to_string()
}
};
if !json {
println!();
}
@@ -2115,19 +2128,24 @@ decay_rate = 0.05
}
checks.push(serde_json::json!({"check": "daemon", "status": "warn"}));
// Check if port 4200 is available
match std::net::TcpListener::bind("127.0.0.1:4200") {
// Check if the configured port is available
let bind_addr = if api_listen.starts_with("0.0.0.0") {
api_listen.replacen("0.0.0.0", "127.0.0.1", 1)
} else {
api_listen.clone()
};
match std::net::TcpListener::bind(&bind_addr) {
Ok(_) => {
if !json {
ui::check_ok("Port 4200 is available");
ui::check_ok(&format!("Port {api_listen} is available"));
}
checks.push(serde_json::json!({"check": "port_4200", "status": "ok"}));
checks.push(serde_json::json!({"check": "port", "status": "ok", "address": api_listen}));
}
Err(_) => {
if !json {
ui::check_warn("Port 4200 is in use by another process");
ui::check_warn(&format!("Port {api_listen} is in use by another process"));
}
checks.push(serde_json::json!({"check": "port_4200", "status": "warn"}));
checks.push(serde_json::json!({"check": "port", "status": "warn", "address": api_listen}));
}
}
}
+39 -20
View File
@@ -1017,23 +1017,24 @@ impl OpenFangKernel {
);
// Apply default_model to restored agents (same logic as spawn)
if restored_entry.manifest.model.api_key_env.is_none()
&& restored_entry.manifest.model.base_url.is_none()
{
let dm = &kernel.config.default_model;
let is_default_provider = restored_entry.manifest.model.provider.is_empty()
|| restored_entry.manifest.model.provider == "default";
let is_default_model = restored_entry.manifest.model.model.is_empty()
|| restored_entry.manifest.model.model == "default";
if is_default_provider && is_default_model {
let dm = &kernel.config.default_model;
if !dm.provider.is_empty() {
restored_entry.manifest.model.provider = dm.provider.clone();
}
if !dm.model.is_empty() {
restored_entry.manifest.model.model = dm.model.clone();
}
if dm.base_url.is_some() {
restored_entry.manifest.model.base_url = dm.base_url.clone();
if !dm.api_key_env.is_empty() && restored_entry.manifest.model.api_key_env.is_none() {
restored_entry.manifest.model.api_key_env = Some(dm.api_key_env.clone());
}
if dm.base_url.is_some() && restored_entry.manifest.model.base_url.is_none() {
restored_entry.manifest.model.base_url.clone_from(&dm.base_url);
}
}
}
@@ -1103,29 +1104,34 @@ impl OpenFangKernel {
// Overlay kernel default_model onto agent if agent didn't explicitly choose.
// Treat empty or "default" as "use the kernel's configured default_model".
// This allows bundled agents to defer to the user's configured provider/model.
if manifest.model.api_key_env.is_none() && manifest.model.base_url.is_none() {
// Check hot-reloaded override first, fall back to boot-time config
let override_guard = self
.default_model_override
.read()
.unwrap_or_else(|e: std::sync::PoisonError<_>| e.into_inner());
let dm = override_guard
.as_ref()
.unwrap_or(&self.config.default_model);
// This allows bundled agents to defer to the user's configured provider/model,
// even if the agent manifest specifies an api_key_env (which is just a hint
// about which env var to check, not a hard lock on provider/model).
{
let is_default_provider =
manifest.model.provider.is_empty() || manifest.model.provider == "default";
let is_default_model =
manifest.model.model.is_empty() || manifest.model.model == "default";
if is_default_provider && is_default_model {
// Check hot-reloaded override first, fall back to boot-time config
let override_guard = self
.default_model_override
.read()
.unwrap_or_else(|e: std::sync::PoisonError<_>| e.into_inner());
let dm = override_guard
.as_ref()
.unwrap_or(&self.config.default_model);
if !dm.provider.is_empty() {
manifest.model.provider = dm.provider.clone();
}
if !dm.model.is_empty() {
manifest.model.model = dm.model.clone();
}
if dm.base_url.is_some() {
manifest.model.base_url = dm.base_url.clone();
if !dm.api_key_env.is_empty() && manifest.model.api_key_env.is_none() {
manifest.model.api_key_env = Some(dm.api_key_env.clone());
}
if dm.base_url.is_some() && manifest.model.base_url.is_none() {
manifest.model.base_url.clone_from(&dm.base_url);
}
}
}
@@ -2889,10 +2895,23 @@ impl OpenFangKernel {
manifest.model.system_prompt, resolved.prompt_block
);
}
if !resolved.env_vars.is_empty() {
// Collect env vars from settings + from requires (api_key/env_var requirements)
let mut allowed_env = resolved.env_vars;
for req in &def.requires {
match req.requirement_type {
openfang_hands::RequirementType::ApiKey
| openfang_hands::RequirementType::EnvVar => {
if !req.check_value.is_empty() && !allowed_env.contains(&req.check_value) {
allowed_env.push(req.check_value.clone());
}
}
_ => {}
}
}
if !allowed_env.is_empty() {
manifest.metadata.insert(
"hand_allowed_env".to_string(),
serde_json::to_value(&resolved.env_vars).unwrap_or_default(),
serde_json::to_value(&allowed_env).unwrap_or_default(),
);
}
@@ -4641,7 +4660,7 @@ fn infer_provider_from_model(model: &str) -> Option<String> {
/// A well-known agent ID used for shared memory operations across agents.
/// This is a fixed UUID so all agents read/write to the same namespace.
fn shared_memory_agent_id() -> AgentId {
pub fn shared_memory_agent_id() -> AgentId {
AgentId(uuid::Uuid::from_bytes([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01,
+10 -5
View File
@@ -91,16 +91,21 @@ impl StructuredStore {
let rows = stmt
.query_map(rusqlite::params![agent_id.0.to_string()], |row| {
let key: String = row.get(0)?;
let val_str: String = row.get(1)?;
Ok((key, val_str))
let blob: Vec<u8> = row.get(1)?;
Ok((key, blob))
})
.map_err(|e| OpenFangError::Memory(e.to_string()))?;
let mut pairs = Vec::new();
for row in rows {
let (key, val_str) = row.map_err(|e| OpenFangError::Memory(e.to_string()))?;
let value: serde_json::Value =
serde_json::from_str(&val_str).unwrap_or(serde_json::Value::String(val_str));
let (key, blob) = row.map_err(|e| OpenFangError::Memory(e.to_string()))?;
let value: serde_json::Value = serde_json::from_slice(&blob)
.unwrap_or_else(|_| {
// Fallback: try as UTF-8 string
String::from_utf8(blob)
.map(serde_json::Value::String)
.unwrap_or(serde_json::Value::Null)
});
pairs.push((key, value));
}
Ok(pairs)