diff --git a/Cargo.lock b/Cargo.lock index 227f2e8e..e2246fa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 8d08cc8b..80bc65e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 4c9ec368..06fde54f 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -483,6 +483,10 @@ pub async fn status(State(state): State>) -> 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>, + Path(slug): Path, +) -> 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>) -> 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. diff --git a/crates/openfang-api/src/server.rs b/crates/openfang-api/src/server.rs index 2213c374..8c9a590d 100644 --- a/crates/openfang-api/src/server.rs +++ b/crates/openfang-api/src/server.rs @@ -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), diff --git a/crates/openfang-api/src/webchat.rs b/crates/openfang-api/src/webchat.rs index 040771ff..b7fa6016 100644 --- a/crates/openfang-api/src/webchat.rs +++ b/crates/openfang-api/src/webchat.rs @@ -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\n", // Alpine.js MUST be last — it processes x-data and fires alpine:init "