batch fixes

This commit is contained in:
jaberjaber23
2026-03-03 01:17:34 +03:00
parent 444d82e4d6
commit 294f0e7af8
11 changed files with 320 additions and 42 deletions
Generated
+45 -15
View File
@@ -3866,7 +3866,7 @@ dependencies = [
[[package]]
name = "openfang-api"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"async-trait",
"axum",
@@ -3902,7 +3902,7 @@ dependencies = [
[[package]]
name = "openfang-channels"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"async-trait",
"axum",
@@ -3933,7 +3933,7 @@ dependencies = [
[[package]]
name = "openfang-cli"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"clap",
"clap_complete",
@@ -3960,7 +3960,7 @@ dependencies = [
[[package]]
name = "openfang-desktop"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"axum",
"open",
@@ -3986,7 +3986,7 @@ dependencies = [
[[package]]
name = "openfang-extensions"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"aes-gcm",
"argon2",
@@ -4014,7 +4014,7 @@ dependencies = [
[[package]]
name = "openfang-hands"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"chrono",
"dashmap",
@@ -4031,7 +4031,7 @@ dependencies = [
[[package]]
name = "openfang-kernel"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"async-trait",
"chrono",
@@ -4067,7 +4067,7 @@ dependencies = [
[[package]]
name = "openfang-memory"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"async-trait",
"chrono",
@@ -4086,7 +4086,7 @@ dependencies = [
[[package]]
name = "openfang-migrate"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"chrono",
"dirs 6.0.0",
@@ -4105,7 +4105,7 @@ dependencies = [
[[package]]
name = "openfang-runtime"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"anyhow",
"async-trait",
@@ -4137,7 +4137,7 @@ dependencies = [
[[package]]
name = "openfang-skills"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"chrono",
"hex",
@@ -4155,11 +4155,12 @@ dependencies = [
"tracing",
"uuid",
"walkdir",
"zip 2.4.2",
]
[[package]]
name = "openfang-types"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"async-trait",
"chrono",
@@ -4178,7 +4179,7 @@ dependencies = [
[[package]]
name = "openfang-wire"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"async-trait",
"chrono",
@@ -6553,7 +6554,7 @@ dependencies = [
"tokio",
"url",
"windows-sys 0.60.2",
"zip",
"zip 4.6.1",
]
[[package]]
@@ -8790,7 +8791,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "xtask"
version = "0.3.0"
version = "0.3.2"
[[package]]
name = "yoke"
@@ -8970,6 +8971,23 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "zip"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"flate2",
"indexmap 2.13.0",
"memchr",
"thiserror 2.0.18",
"zopfli",
]
[[package]]
name = "zip"
version = "4.6.1"
@@ -8988,6 +9006,18 @@ version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.13.3"
+3
View File
@@ -122,6 +122,9 @@ argon2 = "0.5"
# Lightweight regex
regex-lite = "0.1"
# Zip archive extraction
zip = { version = "2", default-features = false, features = ["deflate"] }
# Email (SMTP + IMAP)
lettre = { version = "0.11", default-features = false, features = ["builder", "hostname", "smtp-transport", "tokio1", "tokio1-rustls-tls"] }
imap = "2"
+67 -10
View File
@@ -483,6 +483,10 @@ pub async fn status(State(state): State<Arc<AppState>>) -> impl IntoResponse {
"default_provider": state.kernel.config.default_model.provider,
"default_model": state.kernel.config.default_model.model,
"uptime_seconds": uptime,
"api_listen": state.kernel.config.api_listen,
"home_dir": state.kernel.config.home_dir.display().to_string(),
"log_level": state.kernel.config.log_level,
"network_enabled": state.kernel.config.network_enabled,
"agents": agents,
}))
}
@@ -2966,6 +2970,46 @@ pub async fn clawhub_skill_detail(
}
}
/// GET /api/clawhub/skill/{slug}/code — Fetch the source code (SKILL.md) of a ClawHub skill.
pub async fn clawhub_skill_code(
State(state): State<Arc<AppState>>,
Path(slug): Path<String>,
) -> impl IntoResponse {
let cache_dir = state.kernel.config.home_dir.join(".cache").join("clawhub");
let client = openfang_skills::clawhub::ClawHubClient::new(cache_dir);
// Try to fetch SKILL.md first, then fallback to package.json
let mut code = String::new();
let mut filename = String::new();
if let Ok(content) = client.get_file(&slug, "SKILL.md").await {
code = content;
filename = "SKILL.md".to_string();
} else if let Ok(content) = client.get_file(&slug, "package.json").await {
code = content;
filename = "package.json".to_string();
} else if let Ok(content) = client.get_file(&slug, "skill.toml").await {
code = content;
filename = "skill.toml".to_string();
}
if code.is_empty() {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "No source code found for this skill"})),
);
}
(
StatusCode::OK,
Json(serde_json::json!({
"slug": slug,
"filename": filename,
"code": code,
})),
)
}
/// POST /api/clawhub/install — Install a skill from ClawHub.
///
/// Runs the full security pipeline: SHA256 verification, format detection,
@@ -5026,6 +5070,10 @@ pub async fn list_providers(State(state): State<Arc<AppState>>) -> impl IntoResp
entry["latency_ms"] = serde_json::json!(probe.latency_ms);
if !probe.discovered_models.is_empty() {
entry["discovered_models"] = serde_json::json!(probe.discovered_models);
// Merge discovered models into the catalog so agents can use them
if let Ok(mut catalog) = state.kernel.model_catalog.write() {
catalog.merge_discovered_models(&p.id, &probe.discovered_models);
}
}
if let Some(err) = &probe.error {
entry["error"] = serde_json::json!(err);
@@ -6387,16 +6435,25 @@ pub async fn set_provider_url(
let probe =
openfang_runtime::provider_health::probe_provider(&name, &base_url).await;
(
StatusCode::OK,
Json(serde_json::json!({
"status": "saved",
"provider": name,
"base_url": base_url,
"reachable": probe.reachable,
"latency_ms": probe.latency_ms,
})),
)
// Merge discovered models into catalog
if !probe.discovered_models.is_empty() {
if let Ok(mut catalog) = state.kernel.model_catalog.write() {
catalog.merge_discovered_models(&name, &probe.discovered_models);
}
}
let mut resp = serde_json::json!({
"status": "saved",
"provider": name,
"base_url": base_url,
"reachable": probe.reachable,
"latency_ms": probe.latency_ms,
});
if !probe.discovered_models.is_empty() {
resp["discovered_models"] = serde_json::json!(probe.discovered_models);
}
(StatusCode::OK, Json(resp))
}
/// Upsert a provider URL in the `[provider_urls]` section of config.toml.
+4
View File
@@ -321,6 +321,10 @@ pub async fn build_router(
"/api/clawhub/skill/{slug}",
axum::routing::get(routes::clawhub_skill_detail),
)
.route(
"/api/clawhub/skill/{slug}/code",
axum::routing::get(routes::clawhub_skill_code),
)
.route(
"/api/clawhub/install",
axum::routing::post(routes::clawhub_install),
+2
View File
@@ -125,6 +125,8 @@ const WEBCHAT_HTML: &str = concat!(
include_str!("../static/js/pages/approvals.js"),
"\n",
include_str!("../static/js/pages/comms.js"),
"\n",
include_str!("../static/js/pages/runtime.js"),
"\n</script>\n",
// Alpine.js MUST be last — it processes x-data and fires alpine:init
"<script>\n",
+77 -1
View File
@@ -156,6 +156,10 @@
</div>
<template x-if="!collapsed">
<div x-transition>
<a class="nav-item" :class="{ active: page === 'runtime' }" @click="navigate('runtime')" :aria-current="page === 'runtime' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></span>
<span class="nav-label">Runtime</span>
</a>
<a class="nav-item" :class="{ active: page === 'settings' }" @click="navigate('settings')" :aria-current="page === 'settings' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><path d="M1 14h6M9 8h6M17 16h6"/></svg></span>
<span class="nav-label">Settings</span>
@@ -2305,7 +2309,14 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]</pre>
</template>
</div>
<div class="flex gap-2">
<button class="btn btn-primary btn-block" @click="installFromClawHub(skillDetail.slug)" :disabled="installingSlug === skillDetail.slug || skillDetail.installed || isSkillInstalled(skillDetail.slug)" x-text="skillDetail.installed || isSkillInstalled(skillDetail.slug) ? 'Already Installed' : installingSlug === skillDetail.slug ? 'Installing...' : 'Install from ClawHub'"></button>
<button class="btn btn-primary" style="flex:1" @click="installFromClawHub(skillDetail.slug)" :disabled="installingSlug === skillDetail.slug || skillDetail.installed || isSkillInstalled(skillDetail.slug)" x-text="skillDetail.installed || isSkillInstalled(skillDetail.slug) ? 'Already Installed' : installingSlug === skillDetail.slug ? 'Installing...' : 'Install from ClawHub'"></button>
<button class="btn btn-ghost" @click="viewSkillCode(skillDetail.slug)" :disabled="skillCodeLoading" x-text="skillCodeLoading ? 'Loading...' : showSkillCode ? 'Hide Code' : 'View Code'"></button>
</div>
<div x-show="showSkillCode && skillCode" x-transition class="mt-3" style="max-height:300px;overflow:auto;border:1px solid var(--border);border-radius:8px;background:var(--bg-inset)">
<div class="flex justify-between items-center" style="padding:6px 12px;border-bottom:1px solid var(--border)">
<span class="text-xs text-dim" x-text="skillCodeFilename"></span>
</div>
<pre style="margin:0;padding:12px;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-all" x-text="skillCode"></pre>
</div>
<div class="text-xs text-dim mt-2 text-center">Skills are security-scanned before installation. Prompt injection and malware patterns are blocked.</div>
</div>
@@ -4563,6 +4574,71 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]</pre>
</div>
</template>
<!-- ─── Runtime page ─── -->
<template x-if="page === 'runtime'">
<div x-data="runtimePage">
<div class="page-header"><h2>Runtime</h2></div>
<div class="page-body" x-init="loadData()">
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading runtime info...</span></div>
<div x-show="!loading">
<div class="grid grid-cols-4" style="gap:16px;margin-bottom:24px">
<div class="card stat-card">
<div class="stat-label">Uptime</div>
<div class="stat-value" x-text="uptime"></div>
</div>
<div class="card stat-card">
<div class="stat-label">Agents</div>
<div class="stat-value" x-text="agentCount"></div>
</div>
<div class="card stat-card">
<div class="stat-label">Version</div>
<div class="stat-value" x-text="version"></div>
</div>
<div class="card stat-card">
<div class="stat-label">Default Model</div>
<div class="stat-value" style="font-size:13px" x-text="defaultModel"></div>
</div>
</div>
<div class="card" style="margin-bottom:16px">
<div class="card-header">System</div>
<table class="table" style="margin-top:8px">
<tbody>
<tr><td style="width:180px;font-weight:500">Platform</td><td x-text="platform"></td></tr>
<tr><td style="font-weight:500">Architecture</td><td x-text="arch"></td></tr>
<tr><td style="font-weight:500">API Listen</td><td x-text="apiListen"></td></tr>
<tr><td style="font-weight:500">Home Directory</td><td x-text="homeDir"></td></tr>
<tr><td style="font-weight:500">Log Level</td><td x-text="logLevel"></td></tr>
<tr><td style="font-weight:500">Network</td><td x-text="networkEnabled ? 'Enabled' : 'Disabled'"></td></tr>
</tbody>
</table>
</div>
<div class="card" style="margin-bottom:16px">
<div class="card-header">Providers</div>
<table class="table" style="margin-top:8px">
<thead><tr><th>Provider</th><th>Status</th><th>Models</th><th>Latency</th></tr></thead>
<tbody>
<template x-for="p in providers" :key="p.id">
<tr>
<td x-text="p.display_name || p.id"></td>
<td><span class="badge" :class="p.reachable ? 'badge-success' : (p.auth_status === 'Configured' ? 'badge-success' : 'badge-dim')" x-text="p.reachable ? 'Online' : (p.auth_status === 'Configured' ? 'Ready' : 'Not configured')"></span></td>
<td x-text="p.model_count"></td>
<td x-text="p.latency_ms ? p.latency_ms + 'ms' : '-'"></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="flex gap-2">
<button class="btn btn-ghost btn-sm" @click="loadData()">Refresh</button>
</div>
</div>
</div>
</div>
</template>
</main>
</div>
+1 -1
View File
@@ -218,7 +218,7 @@ function app() {
});
// Hash routing
var validPages = ['overview','agents','sessions','approvals','comms','workflows','scheduler','channels','skills','hands','analytics','logs','settings','wizard'];
var validPages = ['overview','agents','sessions','approvals','comms','workflows','scheduler','channels','skills','hands','analytics','logs','runtime','settings','wizard'];
var pageRedirects = {
'chat': 'agents',
'templates': 'agents',
@@ -0,0 +1,59 @@
// Runtime page — system overview and provider status
document.addEventListener('alpine:init', function() {
Alpine.data('runtimePage', function() {
return {
loading: true,
uptime: '-',
agentCount: 0,
version: '-',
defaultModel: '-',
platform: '-',
arch: '-',
apiListen: '-',
homeDir: '-',
logLevel: '-',
networkEnabled: false,
providers: [],
async loadData() {
this.loading = true;
try {
var results = await Promise.all([
OpenFangAPI.get('/api/status'),
OpenFangAPI.get('/api/version'),
OpenFangAPI.get('/api/providers'),
OpenFangAPI.get('/api/agents')
]);
var status = results[0];
var ver = results[1];
var prov = results[2];
var agents = results[3];
this.version = ver.version || '-';
this.platform = ver.platform || '-';
this.arch = ver.arch || '-';
this.agentCount = Array.isArray(agents) ? agents.length : 0;
this.defaultModel = status.default_model || '-';
this.apiListen = status.api_listen || status.listen || '-';
this.homeDir = status.home_dir || '-';
this.logLevel = status.log_level || '-';
this.networkEnabled = !!status.network_enabled;
// Compute uptime from uptime_seconds
var diff = status.uptime_seconds || 0;
if (diff < 60) this.uptime = diff + 's';
else if (diff < 3600) this.uptime = Math.floor(diff / 60) + 'm ' + (diff % 60) + 's';
else if (diff < 86400) this.uptime = Math.floor(diff / 3600) + 'h ' + Math.floor((diff % 3600) / 60) + 'm';
else this.uptime = Math.floor(diff / 86400) + 'd ' + Math.floor((diff % 86400) / 3600) + 'h';
this.providers = (prov.providers || []).filter(function(p) {
return p.auth_status === 'Configured' || p.reachable || p.is_local;
});
} catch(e) {
console.error('Runtime load error:', e);
}
this.loading = false;
}
};
});
});
@@ -25,6 +25,10 @@ function skillsPage() {
// Skill detail modal
skillDetail: null,
detailLoading: false,
showSkillCode: false,
skillCode: '',
skillCodeFilename: '',
skillCodeLoading: false,
// MCP servers
mcpServers: [],
@@ -205,6 +209,26 @@ function skillsPage() {
closeDetail() {
this.skillDetail = null;
this.installResult = null;
this.showSkillCode = false;
this.skillCode = '';
this.skillCodeFilename = '';
},
async viewSkillCode(slug) {
if (this.showSkillCode) {
this.showSkillCode = false;
return;
}
this.skillCodeLoading = true;
try {
var data = await OpenFangAPI.get('/api/clawhub/skill/' + encodeURIComponent(slug) + '/code');
this.skillCode = data.code || '';
this.skillCodeFilename = data.filename || 'source';
this.showSkillCode = true;
} catch(e) {
OpenFangToast.error('Could not load skill source code');
}
this.skillCodeLoading = false;
},
// Install from ClawHub
+1
View File
@@ -20,6 +20,7 @@ reqwest = { workspace = true }
sha2 = { workspace = true }
hex = { workspace = true }
serde_yaml = { workspace = true }
zip = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }
+37 -15
View File
@@ -17,7 +17,7 @@ use crate::SkillError;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use tracing::info;
use tracing::{info, warn};
// ---------------------------------------------------------------------------
// API response types (matching actual ClawHub v1 API — verified Feb 2026)
@@ -459,27 +459,49 @@ impl ClawHubClient {
let skill_dir = target_dir.join(slug);
std::fs::create_dir_all(&skill_dir)?;
// Extract: ClawHub delivers as zip bundles. For now, detect content type
// and write accordingly. A full zip extraction would use the `zip` crate.
// Detect content type and extract accordingly
let content_str = String::from_utf8_lossy(&bytes);
let is_skillmd = content_str.trim_start().starts_with("---");
if is_skillmd {
std::fs::write(skill_dir.join("SKILL.md"), &*bytes)?;
} else {
// Try to detect if it's a zip by magic bytes (PK\x03\x04)
if bytes.len() >= 4 && bytes[0] == 0x50 && bytes[1] == 0x4b {
// It's a zip — write as raw zip for future extraction
std::fs::write(skill_dir.join("skill.zip"), &*bytes)?;
// Try to fetch the SKILL.md file directly as fallback
if let Ok(skillmd_content) = self.get_file(slug, "SKILL.md").await {
std::fs::write(skill_dir.join("SKILL.md"), &skillmd_content)?;
} else {
std::fs::write(skill_dir.join("package.json"), &*bytes)?;
} else if bytes.len() >= 4 && bytes[0] == 0x50 && bytes[1] == 0x4b {
// Zip archive — extract all files
let cursor = std::io::Cursor::new(&*bytes);
match zip::ZipArchive::new(cursor) {
Ok(mut archive) => {
for i in 0..archive.len() {
let mut file = match archive.by_index(i) {
Ok(f) => f,
Err(e) => {
warn!(index = i, error = %e, "Skipping zip entry");
continue;
}
};
let Some(enclosed_name) = file.enclosed_name() else {
warn!("Skipping zip entry with unsafe path");
continue;
};
let out_path = skill_dir.join(enclosed_name);
if file.is_dir() {
std::fs::create_dir_all(&out_path)?;
} else {
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut out_file = std::fs::File::create(&out_path)?;
std::io::copy(&mut file, &mut out_file)?;
}
}
info!(slug, entries = archive.len(), "Extracted skill zip");
}
Err(e) => {
warn!(slug, error = %e, "Failed to read zip, saving raw");
std::fs::write(skill_dir.join("skill.zip"), &*bytes)?;
}
} else {
std::fs::write(skill_dir.join("package.json"), &*bytes)?;
}
} else {
std::fs::write(skill_dir.join("package.json"), &*bytes)?;
}
// Step 2-3: Detect format and convert