From a340829b6219b23fc2ce9ab528173ca593ada85e Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 3 Nov 2025 15:09:16 -0800 Subject: [PATCH 01/14] uis: reduce opacity --- .../ui/src/components/Home/components/HomeScreen.tsx | 11 ++++++++--- .../src/components/LargeBackgroundVector.tsx | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx index 1d956e728..303359612 100644 --- a/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx @@ -297,9 +297,14 @@ export const HomeScreen: React.FC = () => { data-is-dark-mode={isDarkMode} > - {/* {backgroundImage && ( -
- )} */} + {backgroundImage && ( +
+ )}
{ return ( -
+
From 75d0b40832ecdec4b6207634864133e395273af6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:10:32 +0000 Subject: [PATCH 02/14] Format Rust code using rustfmt --- hyperdrive/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/hyperdrive/src/main.rs b/hyperdrive/src/main.rs index 956bb2f6b..ebb5318b8 100644 --- a/hyperdrive/src/main.rs +++ b/hyperdrive/src/main.rs @@ -489,7 +489,6 @@ async fn main() { // Create the cache_sources file with test content let data_file_path = initfiles_dir.join("cache_sources"); - #[cfg(not(feature = "simulation-mode"))] { // Write cache_source_vector to cache_sources as JSON From 9285d138c3bbd1e33a7cf2e3c3e4e54c64b7eda6 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 4 Nov 2025 21:19:57 -0800 Subject: [PATCH 03/14] bump version to 1.8.1 --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- hyperdrive/Cargo.toml | 2 +- lib/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff1c1f5c8..92268bafd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3517,7 +3517,7 @@ dependencies = [ [[package]] name = "hyperdrive" -version = "1.8.0" +version = "1.8.1" dependencies = [ "aes-gcm", "alloy", @@ -3577,7 +3577,7 @@ dependencies = [ [[package]] name = "hyperdrive_lib" -version = "1.8.0" +version = "1.8.1" dependencies = [ "lib", ] @@ -4334,7 +4334,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lib" -version = "1.8.0" +version = "1.8.1" dependencies = [ "alloy", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 7a0ec43ee..2527ae900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hyperdrive_lib" authors = ["Sybil Technologies AG"] -version = "1.8.0" +version = "1.8.1" edition = "2021" description = "A general-purpose sovereign cloud computing platform" homepage = "https://hyperware.ai" diff --git a/hyperdrive/Cargo.toml b/hyperdrive/Cargo.toml index 60e9157d3..4f7ad7e66 100644 --- a/hyperdrive/Cargo.toml +++ b/hyperdrive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hyperdrive" authors = ["Sybil Technologies AG"] -version = "1.8.0" +version = "1.8.1" edition = "2021" description = "A general-purpose sovereign cloud computing platform" homepage = "https://hyperware.ai" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 7f65460bd..eae10363d 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lib" authors = ["Sybil Technologies AG"] -version = "1.8.0" +version = "1.8.1" edition = "2021" description = "A general-purpose sovereign cloud computing platform" homepage = "https://hyperware.ai" From 64ca272da0fe6555a07bd631d7322065770ef369 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 7 Nov 2025 16:35:51 -0800 Subject: [PATCH 04/14] contacts: change hero image to 20% opacity --- hyperdrive/packages/contacts/pkg/ui/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hyperdrive/packages/contacts/pkg/ui/index.html b/hyperdrive/packages/contacts/pkg/ui/index.html index ee2641ee7..22c689424 100644 --- a/hyperdrive/packages/contacts/pkg/ui/index.html +++ b/hyperdrive/packages/contacts/pkg/ui/index.html @@ -15,7 +15,7 @@ + class="absolute bottom-0 left-0 w-screen bg-center bg-cover pointer-events-none z-0 opacity-20"> Contacts - \ No newline at end of file + From 5ce675f43a511faa6f007fdeabb0f38b7b254af3 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 13 Nov 2025 16:03:24 -0800 Subject: [PATCH 05/14] register-ui: make it harder to reset other node --- .../src/components/EnterHnsName.tsx | 11 +++- .../src/register-ui/src/pages/ResetName.tsx | 61 ++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/hyperdrive/src/register-ui/src/components/EnterHnsName.tsx b/hyperdrive/src/register-ui/src/components/EnterHnsName.tsx index 7ce5ead15..ceea70c0b 100644 --- a/hyperdrive/src/register-ui/src/components/EnterHnsName.tsx +++ b/hyperdrive/src/register-ui/src/components/EnterHnsName.tsx @@ -22,6 +22,8 @@ type EnterNameProps = { triggerNameCheck: boolean; setTba?: React.Dispatch>; isReset?: boolean; + readOnly?: boolean; + disabled?: boolean; }; function EnterHnsName({ @@ -34,6 +36,8 @@ function EnterHnsName({ triggerNameCheck, setTba, isReset = false, + readOnly = false, + disabled = false, }: EnterNameProps) { const client = usePublicClient(); const debouncer = useRef(null); @@ -118,6 +122,8 @@ function EnterHnsName({ const noSpaces = (e: any) => e.target.value.indexOf(" ") === -1 && setName(e.target.value); + const isLocked = readOnly || disabled; + return (
@@ -128,7 +134,10 @@ function EnterHnsName({ required name="hns-name" placeholder="node-name" - className="grow rounded-r-none" + className={`grow rounded-r-none ${isLocked ? "bg-gray-200 dark:bg-slate-800 cursor-not-allowed" : ""}`} + readOnly={readOnly} + disabled={disabled} + aria-readonly={readOnly || undefined} /> {fixedTlz && ([]) + const [currentNodeName, setCurrentNodeName] = useState(""); + const [resetDifferentNodeId, setResetDifferentNodeId] = useState(false); // Track initial states for checkbox help text const [initiallyDirect, setInitiallyDirect] = useState(undefined); @@ -134,6 +138,14 @@ function ResetHnsName({ res.json() )) as UnencryptedIdentity; + if (infoData?.name) { + setCurrentNodeName(infoData.name); + setName(infoData.name); + setHnsName(infoData.name); + } else { + setResetDifferentNodeId(true); + } + const allowedRouters = Array.isArray(infoData.allowed_routers) ? infoData.allowed_routers : undefined; @@ -162,6 +174,7 @@ function ResetHnsName({ } } catch (error) { console.log("Could not fetch node info:", error); + setResetDifferentNodeId(true); } })(); }, []); @@ -241,6 +254,19 @@ function ResetHnsName({ }, [isConfirmed, setReset, setDirect, direct, navigate]); + const handleResetDifferentNodeIdToggle = () => { + setResetDifferentNodeId((prev) => { + const next = !prev; + if (!next && currentNodeName) { + setName(currentNodeName); + setHnsName(currentNodeName); + } + return next; + }); + }; + + const isNameReadOnly = !resetDifferentNodeId && !!currentNodeName; + return (
@@ -253,7 +279,19 @@ function ResetHnsName({

Node ID to reset:

- +

Nodes use an onchain username in order to identify themselves to other nodes in the network.

@@ -306,6 +344,27 @@ function ResetHnsName({ )}
)} +
+ +
+ Reset different node ID. + If you are unsure, leave unchecked. +
+ +

From 50ac5a7e4f66edf16db3a944301d28aa552189da Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 00:03:51 +0000 Subject: [PATCH 06/14] Format Rust code using rustfmt --- hyperdrive/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/hyperdrive/src/main.rs b/hyperdrive/src/main.rs index 956bb2f6b..ebb5318b8 100644 --- a/hyperdrive/src/main.rs +++ b/hyperdrive/src/main.rs @@ -489,7 +489,6 @@ async fn main() { // Create the cache_sources file with test content let data_file_path = initfiles_dir.join("cache_sources"); - #[cfg(not(feature = "simulation-mode"))] { // Write cache_source_vector to cache_sources as JSON From 7ecc3762f31e6142b8bf98a0edc6ae2c19bae5b5 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 17 Nov 2025 04:47:00 -0800 Subject: [PATCH 07/14] app-store: robustify manual download --- .../packages/app-store/downloads/src/lib.rs | 234 ++++++++++++++++-- 1 file changed, 216 insertions(+), 18 deletions(-) diff --git a/hyperdrive/packages/app-store/downloads/src/lib.rs b/hyperdrive/packages/app-store/downloads/src/lib.rs index 76af53d67..f42e9e98a 100644 --- a/hyperdrive/packages/app-store/downloads/src/lib.rs +++ b/hyperdrive/packages/app-store/downloads/src/lib.rs @@ -40,10 +40,13 @@ //! Note: While this process coordinates file transfers, the actual chunked transfer //! mechanism is implemented in the FT worker for improved modularity and performance. //! -use crate::hyperware::process::downloads::{ - AutoDownloadCompleteRequest, AutoDownloadError, AutoUpdateRequest, DirEntry, - DownloadCompleteRequest, DownloadError, DownloadRequest, DownloadResponse, Entry, FileEntry, - HashMismatch, LocalDownloadRequest, RemoteDownloadRequest, RemoveFileRequest, +use crate::hyperware::process::{ + chain::{ChainRequest, ChainResponse}, + downloads::{ + AutoDownloadCompleteRequest, AutoDownloadError, AutoUpdateRequest, DirEntry, + DownloadCompleteRequest, DownloadError, DownloadRequest, DownloadResponse, Entry, + FileEntry, HashMismatch, LocalDownloadRequest, RemoteDownloadRequest, RemoveFileRequest, + }, }; use ft_worker_lib::{spawn_receive_transfer, spawn_send_transfer}; use hyperware::process::downloads::AutoDownloadSuccess; @@ -72,6 +75,7 @@ wit_bindgen::generate!({ mod ft_worker_lib; pub const VFS_TIMEOUT: u64 = 5; // 5s +pub const CHAIN_TIMEOUT: u64 = 60; // 60s #[derive(Debug, Serialize, Deserialize, process_macros::SerdeJsonInto)] #[serde(untagged)] // untagged as a meta-type for all incoming responses @@ -89,6 +93,15 @@ pub struct AutoUpdateStatus { type AutoUpdates = HashMap<(PackageId, String), AutoUpdateStatus>; +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ManualDownloadStatus { + mirrors_left: Vec, // vec(node/url) + mirrors_failed: Vec<(String, DownloadError)>, // vec(node/url, error) + active_mirror: String, // (node/url) +} + +type ManualDownloads = HashMap<(PackageId, String), ManualDownloadStatus>; + #[derive(Debug, Serialize, Deserialize)] pub struct State { // persisted metadata about which packages we are mirroring @@ -128,6 +141,8 @@ fn init(our: Address) { // metadata for in-flight auto-updates let mut auto_updates: AutoUpdates = HashMap::new(); + // metadata for in-flight manual downloads (used for mirror retries) + let mut manual_downloads: ManualDownloads = HashMap::new(); loop { match await_message() { @@ -139,6 +154,7 @@ fn init(our: Address) { &mut downloads, // &mut tmp, &mut auto_updates, + &mut manual_downloads, ) { print_to_terminal(1, &format!("error handling message: {e:?}")); } @@ -163,6 +179,17 @@ fn init(our: Address) { // Then remove and get metadata if let Some(metadata) = auto_updates.remove(&key) { try_next_mirror(metadata, key, &mut auto_updates, error); + } else if let Some(metadata) = manual_downloads.remove(&key) { + if let Err(err) = + try_next_manual_mirror(metadata, key, &mut manual_downloads, error) + { + print_to_terminal( + 1, + &format!( + "downloads: failed manual mirror retry on send error: {err:?}" + ), + ); + } } } } @@ -182,6 +209,7 @@ fn handle_message( downloads: &mut Directory, // _tmp: &mut Directory, auto_updates: &mut AutoUpdates, + manual_downloads: &mut ManualDownloads, ) -> anyhow::Result<()> { if message.is_request() { match message.body().try_into()? { @@ -208,6 +236,18 @@ fn handle_message( desired_version_hash, } = download_request.clone(); + let key = ( + package_id.clone().to_process_lib(), + desired_version_hash.clone(), + ); + + if !download_from.starts_with("http") + && !auto_updates.contains_key(&key) + && !manual_downloads.contains_key(&key) + { + build_manual_mirror_status(&download_request, manual_downloads)?; + } + if download_from.starts_with("http") { // use http-client to GET it Request::to(("our", "http-client", "distro", "sys")) @@ -310,6 +350,16 @@ fn handle_message( DownloadError::InvalidManifest, ); } + return Ok(()); + } + + if let Some(err) = req.err { + if let Some(metadata) = manual_downloads.remove(&key) { + try_next_manual_mirror(metadata, key, manual_downloads, err)?; + return Ok(()); + } + } else { + manual_downloads.remove(&key); } } DownloadRequest::GetFiles(maybe_id) => { @@ -534,26 +584,31 @@ fn handle_message( if let Some(context) = message.context() { let download_request = serde_json::from_slice::(context)?; + let key = ( + download_request.package_id.clone().to_process_lib(), + download_request.desired_version_hash.clone(), + ); match download_response { DownloadResponse::Err(e) => { print_to_terminal(1, &format!("downloads: got error response: {e:?}")); - let key = ( - download_request.package_id.clone().to_process_lib(), - download_request.desired_version_hash.clone(), - ); - if let Some(metadata) = auto_updates.remove(&key) { try_next_mirror(metadata, key, auto_updates, e); - } else { - // If not an auto-update, forward error normally - Request::to(("our", "main", "app-store", "sys")) - .body(DownloadCompleteRequest { - package_id: download_request.package_id, - version_hash: download_request.desired_version_hash, - err: Some(e), - }) - .send()?; + return Ok(()); + } + + if let Some(metadata) = manual_downloads.remove(&key) { + try_next_manual_mirror(metadata, key, manual_downloads, e)?; + return Ok(()); } + + // If not handled by retry logic, forward error normally + Request::to(("our", "main", "app-store", "sys")) + .body(DownloadCompleteRequest { + package_id: download_request.package_id, + version_hash: download_request.desired_version_hash, + err: Some(e), + }) + .send()?; } DownloadResponse::Success => { // todo: maybe we do something here. @@ -646,6 +701,149 @@ fn handle_message( Ok(()) } +fn build_manual_mirror_status( + download_request: &LocalDownloadRequest, + manual_downloads: &mut ManualDownloads, +) -> anyhow::Result<()> { + let key = ( + download_request.package_id.clone().to_process_lib(), + download_request.desired_version_hash.clone(), + ); + + if manual_downloads.contains_key(&key) { + return Ok(()); + } + + let mut mirror_candidates = match fetch_mirror_candidates(&key.0) { + Ok(candidates) => candidates, + Err(err) => { + print_to_terminal( + 1, + &format!("downloads: failed to fetch mirrors for manual download: {err:?}"), + ); + Vec::new() + } + }; + + // ensure requested mirror is first + match mirror_candidates + .iter() + .position(|m| m == &download_request.download_from) + { + Some(idx) => { + let requested = mirror_candidates.remove(idx); + mirror_candidates.insert(0, requested); + } + None => mirror_candidates.insert(0, download_request.download_from.clone()), + } + + if mirror_candidates.len() <= 1 { + return Ok(()); + } + + manual_downloads.insert( + key, + ManualDownloadStatus { + mirrors_left: mirror_candidates[1..].to_vec(), + mirrors_failed: Vec::new(), + active_mirror: mirror_candidates[0].clone(), + }, + ); + + Ok(()) +} + +fn fetch_mirror_candidates(package_id: &PackageId) -> anyhow::Result> { + let resp = Request::to(("our", "chain", "app-store", "sys")) + .body(serde_json::to_vec(&ChainRequest::GetApp( + crate::hyperware::process::main::PackageId::from_process_lib(package_id.clone()), + ))?) + .send_and_await_response(CHAIN_TIMEOUT)??; + + let msg = serde_json::from_slice::(resp.body())?; + + if let ChainResponse::GetApp(Some(app)) = msg { + if let Some(metadata) = app.metadata { + let mut seen = HashSet::new(); + let mut mirror_candidates: Vec = Vec::new(); + + if !metadata.properties.publisher.is_empty() + && seen.insert(metadata.properties.publisher.clone()) + { + mirror_candidates.push(metadata.properties.publisher); + } + + for mirror in metadata.properties.mirrors { + if mirror.is_empty() { + continue; + } + if seen.insert(mirror.clone()) { + mirror_candidates.push(mirror); + } + } + + return Ok(mirror_candidates); + } + } + + Ok(Vec::new()) +} + +fn try_next_manual_mirror( + mut metadata: ManualDownloadStatus, + key: (PackageId, String), + manual_downloads: &mut ManualDownloads, + error: DownloadError, +) -> anyhow::Result<()> { + print_to_terminal( + 0, + &format!( + "manual_download: got error from mirror {mirror:?} {error:?}, trying next mirror: {next_mirror:?}", + mirror = metadata.active_mirror, + error = error, + next_mirror = metadata.mirrors_left.iter().next().cloned(), + ), + ); + + let (package_id, version_hash) = key.clone(); + + match metadata.mirrors_left.first().cloned() { + Some(next_mirror) => { + metadata + .mirrors_failed + .push((metadata.active_mirror.clone(), error)); + metadata.mirrors_left.remove(0); + metadata.active_mirror = next_mirror.clone(); + manual_downloads.insert(key, metadata); + + Request::to(("our", "downloads", "app-store", "sys")) + .body(serde_json::to_vec(&DownloadRequest::LocalDownload( + LocalDownloadRequest { + package_id: crate::hyperware::process::main::PackageId::from_process_lib( + package_id, + ), + download_from: next_mirror.clone(), + desired_version_hash: version_hash, + }, + ))?) + .send()?; + } + None => { + Request::to(("our", "main", "app-store", "sys")) + .body(DownloadCompleteRequest { + package_id: crate::hyperware::process::main::PackageId::from_process_lib( + package_id, + ), + version_hash, + err: Some(error), + }) + .send()?; + } + } + + Ok(()) +} + /// Try the next available mirror for a download, recording the current mirror's failure fn try_next_mirror( mut metadata: AutoUpdateStatus, From 118b61f614acf915796ad3aa53198198f23eb74c Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 20 Nov 2025 15:51:42 -0800 Subject: [PATCH 08/14] add public mode --- hyperdrive/Cargo.toml | 1 + hyperdrive/src/http/server.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/hyperdrive/Cargo.toml b/hyperdrive/Cargo.toml index 4f7ad7e66..19f298ffd 100644 --- a/hyperdrive/Cargo.toml +++ b/hyperdrive/Cargo.toml @@ -17,6 +17,7 @@ anyhow = "1.0.71" sha2 = "0.10.8" [features] +public-mode = [] simulation-mode = [] [dependencies] diff --git a/hyperdrive/src/http/server.rs b/hyperdrive/src/http/server.rs index 1714057e8..66ee430b0 100644 --- a/hyperdrive/src/http/server.rs +++ b/hyperdrive/src/http/server.rs @@ -511,6 +511,7 @@ async fn ws_handler( .host() .contains("localhost"); + #[cfg(not(feature = "public-mode"))] if bound_path.authenticated { let Some(auth_token) = serialized_headers.get("cookie") else { return Err(warp::reject::not_found()); @@ -644,6 +645,7 @@ async fn http_handler( let is_localhost = host.as_ref().contains("localhost"); + #[cfg(not(feature = "public-mode"))] if bound_path.authenticated { if let Some(ref subdomain) = bound_path.secure_subdomain { let request_subdomain = host.host().split('.').next().unwrap_or(""); From eaa9e53dc9690bf452e8d102dea5cea8e7b78ef8 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 21 Nov 2025 07:18:27 -0800 Subject: [PATCH 09/14] spider: add todo as self-validating app --- hyperdrive/packages/spider/spider/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hyperdrive/packages/spider/spider/src/lib.rs b/hyperdrive/packages/spider/spider/src/lib.rs index 2b1965353..86a568546 100644 --- a/hyperdrive/packages/spider/spider/src/lib.rs +++ b/hyperdrive/packages/spider/spider/src/lib.rs @@ -64,6 +64,7 @@ const API_KEY_DISPENSER_PROCESS_ID: (&str, &str, &str) = ( "ware.hypr", ); const HYPERGRID: &str = "operator:hypergrid:ware.hypr"; +const TODO: &str = "todo:todo:ware.hypr"; const TTSTT: (&str, &str, &str) = ("ttstt", "spider", "sys"); #[hyperprocess( @@ -859,7 +860,9 @@ impl SpiderState { ) -> Result { // Validate admin key let hypergrid: ProcessId = HYPERGRID.parse().unwrap(); - if !(self.validate_admin_key(&request.admin_key) || source().process == hypergrid) { + let todo: ProcessId = TODO.parse().unwrap(); + + if !(self.validate_admin_key(&request.admin_key) || source().process == hypergrid || source().process == todo) { return Err("Unauthorized: Invalid or non-admin Spider API key".to_string()); } From f54aee140e1bc837149f92f683abec8e777d50da Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 24 Nov 2025 16:26:24 -0800 Subject: [PATCH 10/14] spider: update default anthropic model and update hyperware prompts --- .../packages/spider/spider/src/provider/anthropic.rs | 6 ++++-- .../spider/spider/src/tool_providers/hyperware.rs | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/hyperdrive/packages/spider/spider/src/provider/anthropic.rs b/hyperdrive/packages/spider/spider/src/provider/anthropic.rs index accebc8dc..d8cf5eeff 100644 --- a/hyperdrive/packages/spider/spider/src/provider/anthropic.rs +++ b/hyperdrive/packages/spider/spider/src/provider/anthropic.rs @@ -15,6 +15,8 @@ use hyperware_process_lib::println; use crate::provider::LlmProvider; use crate::types::{Message, MessageContent, Tool, ToolCall, ToolResult}; +const DEFAULT_MODEL: &str = "claude-sonnet-4-5-20250929"; + pub struct AnthropicProvider { api_key: String, is_oauth: bool, @@ -50,7 +52,7 @@ impl AnthropicProvider { .complete_with_retry( &[check_message], &[], - Some("claude-sonnet-4-20250514"), + Some(DEFAULT_MODEL), 100, 0.0, ) @@ -441,7 +443,7 @@ impl AnthropicProvider { } // Create the request with the specified model or default - let model_id = model.unwrap_or("claude-sonnet-4-20250514"); + let model_id = model.unwrap_or(DEFAULT_MODEL); let mut request = CreateMessageRequest::new(model_id, sdk_messages, max_tokens) .with_temperature(temperature); diff --git a/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs b/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs index 465988fb9..8762fcc3d 100644 --- a/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs +++ b/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs @@ -25,8 +25,8 @@ impl HyperwareToolProvider { Tool { name: "hyperware_get_api".to_string(), description: "Get the detailed API documentation for a specific package, including all available types and methods.".to_string(), - parameters: r#"{"type":"object","required":["package_id"],"properties":{"package_id":{"type":"string","description":"The package ID in the format 'package-name:publisher-node' (e.g., 'weather-app-9000:foo.os')"}}}"#.to_string(), - input_schema_json: Some(r#"{"type":"object","required":["package_id"],"properties":{"package_id":{"type":"string","description":"The package ID in the format 'package-name:publisher-node' (e.g., 'weather-app-9000:foo.os')"}}}"#.to_string()), + parameters: r#"{"type":"object","required":["package_id"],"properties":{"package_id":{"type":"string","description":"The package ID in the format 'package-name:publisher-node'"}}}"#.to_string(), + input_schema_json: Some(r#"{"type":"object","required":["package_id"],"properties":{"package_id":{"type":"string","description":"The package ID in the format 'package-name:publisher-node'"}}}"#.to_string()), } } @@ -34,8 +34,8 @@ impl HyperwareToolProvider { Tool { name: "hyperware_call_api".to_string(), description: "Call a specific API method on a Hyperware process to execute functionality.".to_string(), - parameters: r#"{"type":"object","required":["process_id","method","args"],"properties":{"process_id":{"type":"string","description":"The process ID in the format 'process-name:package-name:publisher-node'"},"method":{"type":"string","description":"The method name to call on the process. By convention UpperCamelCase"},"args":{"type":"string","description":"JSON string of arguments to pass to the method"},"timeout":{"type":"number","description":"Optional timeout in seconds (default: 15)"}}}"#.to_string(), - input_schema_json: Some(r#"{"type":"object","required":["process_id","method","args"],"properties":{"process_id":{"type":"string","description":"The process ID in the format 'process-name:package-name:publisher-node'"},"method":{"type":"string","description":"The method name to call on the process. By convention UpperCamelCase"},"args":{"type":"string","description":"JSON string of arguments to pass to the method"},"timeout":{"type":"number","description":"Optional timeout in seconds (default: 15)"}}}"#.to_string()), + parameters: r#"{"type":"object","required":["process_id","method","args"],"properties":{"process_id":{"type":"string","description":"The process ID in the format 'process-name:package-name:publisher-node'. Process ID is the process prepended to the package ID. By convention the main process-name of a package shares the package-name"},"method":{"type":"string","description":"The method name to call on the process. By convention UpperCamelCase"},"args":{"type":"string","description":"JSON string of arguments to pass to the method"},"timeout":{"type":"number","description":"Optional timeout in seconds (default: 15)"}}}"#.to_string(), + input_schema_json: Some(r#"{"type":"object","required":["process_id","method","args"],"properties":{"process_id":{"type":"string","description":"The process ID in the format 'process-name:package-name:publisher-node'. Process ID is the process prepended to the package ID. By convention the main process-name of a package shares the package-name"},"method":{"type":"string","description":"The method name to call on the process. By convention UpperCamelCase"},"args":{"type":"string","description":"JSON string of arguments to pass to the method"},"timeout":{"type":"number","description":"Optional timeout in seconds (default: 15)"}}}"#.to_string()), } } } From d31436d40b12f622dc2d1f470cdd287688d34d54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 00:27:18 +0000 Subject: [PATCH 11/14] Format Rust code using rustfmt --- hyperdrive/packages/spider/spider/src/lib.rs | 5 ++++- .../packages/spider/spider/src/provider/anthropic.rs | 8 +------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/hyperdrive/packages/spider/spider/src/lib.rs b/hyperdrive/packages/spider/spider/src/lib.rs index 86a568546..fc037acba 100644 --- a/hyperdrive/packages/spider/spider/src/lib.rs +++ b/hyperdrive/packages/spider/spider/src/lib.rs @@ -862,7 +862,10 @@ impl SpiderState { let hypergrid: ProcessId = HYPERGRID.parse().unwrap(); let todo: ProcessId = TODO.parse().unwrap(); - if !(self.validate_admin_key(&request.admin_key) || source().process == hypergrid || source().process == todo) { + if !(self.validate_admin_key(&request.admin_key) + || source().process == hypergrid + || source().process == todo) + { return Err("Unauthorized: Invalid or non-admin Spider API key".to_string()); } diff --git a/hyperdrive/packages/spider/spider/src/provider/anthropic.rs b/hyperdrive/packages/spider/spider/src/provider/anthropic.rs index d8cf5eeff..a784da46f 100644 --- a/hyperdrive/packages/spider/spider/src/provider/anthropic.rs +++ b/hyperdrive/packages/spider/spider/src/provider/anthropic.rs @@ -49,13 +49,7 @@ impl AnthropicProvider { // Use Sonnet 4 specifically for this check match self - .complete_with_retry( - &[check_message], - &[], - Some(DEFAULT_MODEL), - 100, - 0.0, - ) + .complete_with_retry(&[check_message], &[], Some(DEFAULT_MODEL), 100, 0.0) .await { Ok(response) => { From 8158101dd62c220a525327bc5ac21f430c433ebd Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 25 Nov 2025 20:49:41 -0800 Subject: [PATCH 12/14] spider: increase robustness --- .../crates/hyperware-parse-wit/src/lib.rs | 470 ++++++++++++++++++ .../spider/src/tool_providers/hyperware.rs | 47 +- 2 files changed, 486 insertions(+), 31 deletions(-) diff --git a/hyperdrive/packages/spider/crates/hyperware-parse-wit/src/lib.rs b/hyperdrive/packages/spider/crates/hyperware-parse-wit/src/lib.rs index 84409dfee..1b8dd4c0d 100644 --- a/hyperdrive/packages/spider/crates/hyperware-parse-wit/src/lib.rs +++ b/hyperdrive/packages/spider/crates/hyperware-parse-wit/src/lib.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use serde_json::{Map, Value}; use std::io::Read; use std::path::Path; use wit_parser::Resolve; @@ -114,3 +115,472 @@ pub fn parse_wit_from_zip_to_value( let value = serde_json::to_value(&resolve).context("failed to convert to JSON value")?; Ok(value) } + +/// Parse WIT files from a zip archive and return rustified serde_json::Value +/// +/// This is a wrapper around `parse_wit_from_zip_to_value` that converts +/// WIT naming conventions to Rust conventions: +/// - Type names: kebab-case → PascalCase +/// - Enum values: kebab-case → PascalCase +/// - Variant case names: kebab-case → PascalCase +/// - Property keys: kebab-case → snake_case +/// - Type references: kebab-case → PascalCase +/// +/// # Arguments +/// * `zip_bytes` - The bytes of the zip file containing WIT files +/// * `fallback_wit` - Optional WIT content to use when package header is missing. +/// If None, uses the built-in hyperware.wit +pub fn parse_wit_from_zip_to_value_rustified( + zip_bytes: &[u8], + fallback_wit: Option>, +) -> Result { + let value = parse_wit_from_zip_to_value(zip_bytes, fallback_wit)?; + Ok(rustify_value(value)) +} + +/// Convert kebab-case to PascalCase +pub fn to_pascal_case(s: &str) -> String { + let parts = s.split('-'); + let mut result = String::new(); + + for part in parts { + if !part.is_empty() { + let mut chars = part.chars(); + if let Some(first_char) = chars.next() { + result.push(first_char.to_uppercase().next().unwrap()); + result.extend(chars); + } + } + } + + result +} + +/// Convert kebab-case to snake_case +pub fn to_snake_case(s: &str) -> String { + s.replace('-', "_") +} + +/// Recursively rustify a JSON value from WIT conventions to Rust conventions +fn rustify_value(value: Value) -> Value { + match value { + Value::Array(arr) => Value::Array(arr.into_iter().map(rustify_value).collect()), + Value::Object(obj) => rustify_object(obj), + other => other, + } +} + +/// Rustify a JSON object, handling WIT-specific structures +fn rustify_object(obj: Map) -> Value { + // Check what kind of WIT structure this is + let obj_type = obj.get("type").and_then(|v| v.as_str()); + + match obj_type { + Some("enum") => rustify_enum(obj), + Some("variant") => rustify_variant(obj), + Some("object") => rustify_struct(obj), + Some("option") | Some("result") | Some("array") | Some("tuple") => { + rustify_generic_type(obj) + } + None => { + // Could be a top-level type definition or other structure + rustify_type_definition(obj) + } + _ => { + // Unknown type, just recurse + let new_obj: Map = obj + .into_iter() + .map(|(k, v)| (k, rustify_value(v))) + .collect(); + Value::Object(new_obj) + } + } +} + +/// Rustify an enum definition - convert values to PascalCase +fn rustify_enum(mut obj: Map) -> Value { + if let Some(Value::Array(values)) = obj.remove("values") { + let rustified_values: Vec = values + .into_iter() + .map(|v| { + if let Value::String(s) = v { + Value::String(to_pascal_case(&s)) + } else { + v + } + }) + .collect(); + obj.insert("values".to_string(), Value::Array(rustified_values)); + } + + // Recurse on other fields + let new_obj: Map = obj + .into_iter() + .map(|(k, v)| (k, rustify_value(v))) + .collect(); + Value::Object(new_obj) +} + +/// Rustify a variant definition - convert case names to PascalCase +fn rustify_variant(mut obj: Map) -> Value { + if let Some(Value::Array(cases)) = obj.remove("cases") { + let rustified_cases: Vec = cases + .into_iter() + .map(|case| { + if let Value::Object(mut case_obj) = case { + // Convert the case name to PascalCase + if let Some(Value::String(name)) = case_obj.remove("name") { + case_obj.insert("name".to_string(), Value::String(to_pascal_case(&name))); + } + // Recurse on the type field + if let Some(type_val) = case_obj.remove("type") { + case_obj.insert("type".to_string(), rustify_type_reference(type_val)); + } + Value::Object(case_obj) + } else { + case + } + }) + .collect(); + obj.insert("cases".to_string(), Value::Array(rustified_cases)); + } + + let new_obj: Map = obj + .into_iter() + .map(|(k, v)| (k, rustify_value(v))) + .collect(); + Value::Object(new_obj) +} + +/// Rustify a struct/object definition - convert property keys to snake_case +fn rustify_struct(mut obj: Map) -> Value { + if let Some(Value::Object(props)) = obj.remove("properties") { + let rustified_props: Map = props + .into_iter() + .map(|(k, v)| (to_snake_case(&k), rustify_type_reference(v))) + .collect(); + obj.insert("properties".to_string(), Value::Object(rustified_props)); + } + + // Don't recurse on remaining fields - they're just metadata like "type": "object" + Value::Object(obj) +} + +/// Rustify generic types (option, result, array, tuple) +fn rustify_generic_type(mut obj: Map) -> Value { + // Handle "value" field (option, result ok/err) + if let Some(val) = obj.remove("value") { + obj.insert("value".to_string(), rustify_type_reference(val)); + } + if let Some(val) = obj.remove("ok") { + obj.insert("ok".to_string(), rustify_type_reference(val)); + } + if let Some(val) = obj.remove("err") { + obj.insert("err".to_string(), rustify_type_reference(val)); + } + // Handle "items" field (array, tuple) + if let Some(items) = obj.remove("items") { + obj.insert("items".to_string(), rustify_type_reference(items)); + } + + Value::Object(obj) +} + +/// Rustify a top-level type definition +fn rustify_type_definition(mut obj: Map) -> Value { + // Convert the type name to PascalCase + if let Some(Value::String(name)) = obj.remove("name") { + obj.insert("name".to_string(), Value::String(to_pascal_case(&name))); + } + + // Recurse on definition + if let Some(def) = obj.remove("definition") { + obj.insert("definition".to_string(), rustify_type_reference(def)); + } + + // Recurse on other fields (args, returning, target, etc.) + let new_obj: Map = obj + .into_iter() + .map(|(k, v)| (k, rustify_value(v))) + .collect(); + Value::Object(new_obj) +} + +/// Rustify a type reference - could be a string type name or a complex type +fn rustify_type_reference(value: Value) -> Value { + match value { + Value::String(s) => { + // Convert WIT type to Rust type (handles primitives and custom types) + Value::String(wit_type_to_rust(&s)) + } + Value::Object(obj) => rustify_object(obj), + Value::Array(arr) => Value::Array(arr.into_iter().map(rustify_type_reference).collect()), + other => other, + } +} + +/// Convert a WIT type string to its Rust equivalent +/// This handles primitives and keeps custom types for PascalCase conversion +fn wit_type_to_rust(wit_type: &str) -> String { + match wit_type { + // Signed integer types: WIT uses s8/s16/s32/s64, Rust uses i8/i16/i32/i64 + "s8" => "i8".to_string(), + "s16" => "i16".to_string(), + "s32" => "i32".to_string(), + "s64" => "i64".to_string(), + // Unsigned integer types (same in both) + "u8" => "u8".to_string(), + "u16" => "u16".to_string(), + "u32" => "u32".to_string(), + "u64" => "u64".to_string(), + // Floating point types (same in both) + "f32" => "f32".to_string(), + "f64" => "f64".to_string(), + // String type: WIT uses lowercase, Rust uses String + "string" => "String".to_string(), + // Other primitives (same in both) + "bool" => "bool".to_string(), + "char" => "char".to_string(), + // Unit type + "_" => "()".to_string(), + // Custom types get PascalCase conversion + _ => to_pascal_case(wit_type), + } +} + +/// Check if a type name is a WIT primitive (used for deciding whether to recurse) +#[allow(dead_code)] +fn is_wit_primitive(s: &str) -> bool { + matches!( + s, + "bool" + | "u8" + | "u16" + | "u32" + | "u64" + | "s8" + | "s16" + | "s32" + | "s64" + | "f32" + | "f64" + | "char" + | "string" + | "_" + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_to_pascal_case() { + assert_eq!(to_pascal_case("up-next"), "UpNext"); + assert_eq!(to_pascal_case("in-progress"), "InProgress"); + assert_eq!(to_pascal_case("this-week"), "ThisWeek"); + assert_eq!(to_pascal_case("low"), "Low"); + assert_eq!(to_pascal_case("node-id"), "NodeId"); + assert_eq!(to_pascal_case("NodeId"), "NodeId"); + } + + #[test] + fn test_to_snake_case() { + assert_eq!(to_snake_case("publisher-node"), "publisher_node"); + assert_eq!(to_snake_case("package-name"), "package_name"); + assert_eq!(to_snake_case("already_snake"), "already_snake"); + } + + #[test] + fn test_is_wit_primitive() { + assert!(is_wit_primitive("bool")); + assert!(is_wit_primitive("u8")); + assert!(is_wit_primitive("u64")); + assert!(is_wit_primitive("s64")); + assert!(is_wit_primitive("string")); + assert!(is_wit_primitive("_")); + assert!(!is_wit_primitive("NodeId")); + assert!(!is_wit_primitive("my-type")); + } + + #[test] + fn test_wit_type_to_rust() { + // Signed integers: s* -> i* + assert_eq!(wit_type_to_rust("s8"), "i8"); + assert_eq!(wit_type_to_rust("s16"), "i16"); + assert_eq!(wit_type_to_rust("s32"), "i32"); + assert_eq!(wit_type_to_rust("s64"), "i64"); + // Unsigned stay the same + assert_eq!(wit_type_to_rust("u8"), "u8"); + assert_eq!(wit_type_to_rust("u64"), "u64"); + // String -> String (capitalized) + assert_eq!(wit_type_to_rust("string"), "String"); + // Unit type + assert_eq!(wit_type_to_rust("_"), "()"); + // Custom types get PascalCase + assert_eq!(wit_type_to_rust("my-custom-type"), "MyCustomType"); + assert_eq!(wit_type_to_rust("NodeId"), "NodeId"); + } + + #[test] + fn test_rustify_enum() { + let input = json!({ + "type": "enum", + "values": ["low", "medium", "high", "up-next", "in-progress"] + }); + let expected = json!({ + "type": "enum", + "values": ["Low", "Medium", "High", "UpNext", "InProgress"] + }); + assert_eq!(rustify_value(input), expected); + } + + #[test] + fn test_rustify_variant() { + let input = json!({ + "type": "variant", + "cases": [ + {"name": "request", "type": "Request"}, + {"name": "response", "type": "Response"} + ] + }); + let expected = json!({ + "type": "variant", + "cases": [ + {"name": "Request", "type": "Request"}, + {"name": "Response", "type": "Response"} + ] + }); + assert_eq!(rustify_value(input), expected); + } + + #[test] + fn test_rustify_struct() { + let input = json!({ + "type": "object", + "properties": { + "publisher-node": "NodeId", + "package-name": "string" + } + }); + let expected = json!({ + "type": "object", + "properties": { + "publisher_node": "NodeId", + "package_name": "String" + } + }); + assert_eq!(rustify_value(input), expected); + } + + #[test] + fn test_rustify_type_definition() { + let input = json!({ + "name": "entry-status", + "definition": { + "type": "enum", + "values": ["backlog", "up-next", "in-progress", "done"] + } + }); + let expected = json!({ + "name": "EntryStatus", + "definition": { + "type": "enum", + "values": ["Backlog", "UpNext", "InProgress", "Done"] + } + }); + assert_eq!(rustify_value(input), expected); + } + + #[test] + fn test_rustify_converts_wit_primitives() { + // WIT primitives get converted to Rust equivalents + // string -> String, s64 -> i64, etc. + let input = json!({ + "type": "object", + "properties": { + "count": "u64", + "name": "string", + "active": "bool", + "timestamp": "s64" + } + }); + let expected = json!({ + "type": "object", + "properties": { + "count": "u64", + "name": "String", + "active": "bool", + "timestamp": "i64" + } + }); + assert_eq!(rustify_value(input), expected); + } + + #[test] + fn test_rustify_nested_types() { + let input = json!({ + "type": "option", + "value": "my-custom-type" + }); + let expected = json!({ + "type": "option", + "value": "MyCustomType" + }); + assert_eq!(rustify_value(input), expected); + } + + #[test] + fn test_rustify_full_enum_type_definition() { + // Test the exact format from parse_wit_from_zip_to_value output + let input = json!({ + "definition": { + "type": "enum", + "values": ["backlog", "up-next", "in-progress", "blocked", "review", "done"] + }, + "documentation": null, + "name": "EntryStatus", + "process_name": "todo" + }); + let result = rustify_value(input); + + // Check that enum values are converted to PascalCase + let definition = result.get("definition").unwrap(); + let values = definition.get("values").unwrap().as_array().unwrap(); + let value_strings: Vec<&str> = values.iter().map(|v| v.as_str().unwrap()).collect(); + + assert_eq!( + value_strings, + vec!["Backlog", "UpNext", "InProgress", "Blocked", "Review", "Done"] + ); + } + + #[test] + fn test_rustify_array_of_definitions() { + // Test array input (like from parse_wit_from_zip_to_value) + let input = json!([ + { + "definition": {"type": "enum", "values": ["low", "medium", "high"]}, + "name": "EntryPriority", + "process_name": "todo" + }, + { + "definition": {"type": "enum", "values": ["backlog", "up-next"]}, + "name": "EntryStatus" + } + ]); + let result = rustify_value(input); + let arr = result.as_array().unwrap(); + + // Check first enum + let first_values = arr[0]["definition"]["values"].as_array().unwrap(); + let first_strings: Vec<&str> = first_values.iter().map(|v| v.as_str().unwrap()).collect(); + assert_eq!(first_strings, vec!["Low", "Medium", "High"]); + + // Check second enum + let second_values = arr[1]["definition"]["values"].as_array().unwrap(); + let second_strings: Vec<&str> = second_values.iter().map(|v| v.as_str().unwrap()).collect(); + assert_eq!(second_strings, vec!["Backlog", "UpNext"]); + } +} diff --git a/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs b/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs index 8762fcc3d..ed5fb6c4d 100644 --- a/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs +++ b/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs @@ -1,6 +1,6 @@ use crate::tool_providers::{ToolExecutionCommand, ToolProvider}; use crate::types::{SpiderState, Tool}; -use hyperware_parse_wit::parse_wit_from_zip_to_resolve; +use hyperware_parse_wit::{parse_wit_from_zip_to_resolve, to_pascal_case, to_snake_case}; use hyperware_process_lib::{get_blob, hyperapp::send, ProcessId, ProcessIdParseError, Request}; use serde_json::{json, Value}; use wit_parser::Docs; @@ -244,7 +244,7 @@ pub async fn get_api(package_id: &str) -> Result { | "response" | "message" ) { - let rust_type_name = to_upper_camel_case(type_name); + let rust_type_name = to_pascal_case(type_name); if seen_types.insert(rust_type_name.clone()) { let type_def = &resolve.types[*type_id]; let docs = extract_docs(&type_def.docs); @@ -265,7 +265,7 @@ pub async fn get_api(package_id: &str) -> Result { // Add types within the interface for (type_name, type_id) in &iface.types { - let type_name_camel = to_upper_camel_case(type_name); + let type_name_camel = to_pascal_case(type_name); // Skip types ending with SignatureHttp or SignatureRemote if type_name_camel.ends_with("SignatureHttp") @@ -425,23 +425,6 @@ fn extract_docs(docs: &Docs) -> Option { docs.contents.clone() } -// Helper function to convert snake_case to UpperCamelCase -fn to_upper_camel_case(s: &str) -> String { - s.split('-') - .map(|word| { - let mut chars = word.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_uppercase().collect::() + chars.as_str(), - } - }) - .collect::() -} - -// Helper function to convert kebab-case to snake_case -fn to_snake_case(s: &str) -> String { - s.replace('-', "_") -} // Convert a WIT type definition to a JSON schema representation fn type_to_json_schema(type_def: &wit_parser::TypeDef, resolve: &wit_parser::Resolve) -> Value { @@ -475,7 +458,7 @@ fn type_to_json_schema(type_def: &wit_parser::TypeDef, resolve: &wit_parser::Res None => json!("null"), }; json!({ - "name": case.name, + "name": to_pascal_case(&case.name), "type": case_schema }) }) @@ -487,14 +470,14 @@ fn type_to_json_schema(type_def: &wit_parser::TypeDef, resolve: &wit_parser::Res }) } TypeDefKind::Enum(enum_def) => { - let cases = enum_def + let values = enum_def .cases .iter() - .map(|case| &case.name) + .map(|case| to_pascal_case(&case.name)) .collect::>(); json!({ "type": "enum", - "values": cases + "values": values }) } TypeDefKind::List(ty) => { @@ -543,7 +526,7 @@ fn type_to_json_schema(type_def: &wit_parser::TypeDef, resolve: &wit_parser::Res } } -// Convert a WIT type reference to a JSON representation +// Convert a WIT type reference to a JSON representation with Rust type names fn type_ref_to_json(type_ref: &wit_parser::Type, resolve: &wit_parser::Resolve) -> Value { use wit_parser::Type; @@ -553,20 +536,22 @@ fn type_ref_to_json(type_ref: &wit_parser::Type, resolve: &wit_parser::Resolve) Type::U16 => json!("u16"), Type::U32 => json!("u32"), Type::U64 => json!("u64"), - Type::S8 => json!("s8"), - Type::S16 => json!("s16"), - Type::S32 => json!("s32"), - Type::S64 => json!("s64"), + // WIT signed integers map to Rust i* types + Type::S8 => json!("i8"), + Type::S16 => json!("i16"), + Type::S32 => json!("i32"), + Type::S64 => json!("i64"), Type::F32 => json!("f32"), Type::F64 => json!("f64"), Type::Char => json!("char"), - Type::String => json!("string"), + // WIT string maps to Rust String + Type::String => json!("String"), Type::Id(id) => { // Look up the referenced type if let Some(type_def) = resolve.types.get(*id) { // If it has a name, use the name; otherwise, inline the definition if let Some(name) = &type_def.name { - json!(to_upper_camel_case(name)) + json!(to_pascal_case(name)) } else { type_to_json_schema(type_def, resolve) } From 125426e2fa05ff054584719d69c54d1c517108b9 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 25 Nov 2025 21:33:17 -0800 Subject: [PATCH 13/14] spider: replace call_api method+args with signature --- hyperdrive/packages/spider/spider/src/lib.rs | 5 ++- .../spider/src/tool_providers/hyperware.rs | 35 ++++++++----------- .../spider/spider/src/tool_providers/mod.rs | 3 +- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/hyperdrive/packages/spider/spider/src/lib.rs b/hyperdrive/packages/spider/spider/src/lib.rs index fc037acba..19123e8ed 100644 --- a/hyperdrive/packages/spider/spider/src/lib.rs +++ b/hyperdrive/packages/spider/spider/src/lib.rs @@ -3323,10 +3323,9 @@ impl SpiderState { } ToolExecutionCommand::HyperwareCallApi { process_id, - method, - args, + signature, timeout, - } => tool_providers::hyperware::call_api(&process_id, &method, &args, timeout).await, + } => tool_providers::hyperware::call_api(&process_id, &signature, timeout).await, ToolExecutionCommand::DirectResult(result) => result, } } diff --git a/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs b/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs index ed5fb6c4d..916ca0b96 100644 --- a/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs +++ b/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs @@ -34,8 +34,8 @@ impl HyperwareToolProvider { Tool { name: "hyperware_call_api".to_string(), description: "Call a specific API method on a Hyperware process to execute functionality.".to_string(), - parameters: r#"{"type":"object","required":["process_id","method","args"],"properties":{"process_id":{"type":"string","description":"The process ID in the format 'process-name:package-name:publisher-node'. Process ID is the process prepended to the package ID. By convention the main process-name of a package shares the package-name"},"method":{"type":"string","description":"The method name to call on the process. By convention UpperCamelCase"},"args":{"type":"string","description":"JSON string of arguments to pass to the method"},"timeout":{"type":"number","description":"Optional timeout in seconds (default: 15)"}}}"#.to_string(), - input_schema_json: Some(r#"{"type":"object","required":["process_id","method","args"],"properties":{"process_id":{"type":"string","description":"The process ID in the format 'process-name:package-name:publisher-node'. Process ID is the process prepended to the package ID. By convention the main process-name of a package shares the package-name"},"method":{"type":"string","description":"The method name to call on the process. By convention UpperCamelCase"},"args":{"type":"string","description":"JSON string of arguments to pass to the method"},"timeout":{"type":"number","description":"Optional timeout in seconds (default: 15)"}}}"#.to_string()), + parameters: r#"{"type":"object","required":["process_id","signature"],"properties":{"process_id":{"type":"string","description":"The process ID in the format 'process-name:package-name:publisher-node'. Process ID is the process prepended to the package ID. By convention the main process-name of a package shares the package-name"},"signature":{"type":"string","description":"JSON string containing the method call signature, e.g. '{\"MethodName\": [\"value1\", \"value2\"]}}' for multiple args, '{\"MethodName\": \"value1\"}}' for one arg, or '{\"MethodName\": null}' for no args"},"timeout":{"type":"number","description":"Optional timeout in seconds (default: 15)"}}}"#.to_string(), + input_schema_json: Some(r#"{"type":"object","required":["process_id","signature"],"properties":{"process_id":{"type":"string","description":"The process ID in the format 'process-name:package-name:publisher-node'. Process ID is the process prepended to the package ID. By convention the main process-name of a package shares the package-name"},"signature":{"type":"string","description":"JSON string containing the method call signature, e.g. '{\"MethodName\": [\"value1\", \"value2\"]}}' for multiple args, '{\"MethodName\": \"value1\"}}' for one arg, or '{\"MethodName\": null}' for no args"},"timeout":{"type":"number","description":"Optional timeout in seconds (default: 15)"}}}"#.to_string()), } } } @@ -85,19 +85,13 @@ impl ToolProvider for HyperwareToolProvider { let process_id = parameters .get("process_id") .and_then(|v| v.as_str()) - .ok_or_else(|| "Missing package_id parameter".to_string())? - .to_string(); - - let method = parameters - .get("method") - .and_then(|v| v.as_str()) - .ok_or_else(|| "Missing method parameter".to_string())? + .ok_or_else(|| "Missing process_id parameter".to_string())? .to_string(); - let args = parameters - .get("args") + let signature = parameters + .get("signature") .and_then(|v| v.as_str()) - .ok_or_else(|| "Missing args parameter".to_string())? + .ok_or_else(|| "Missing signature parameter".to_string())? .to_string(); let timeout = parameters @@ -107,8 +101,7 @@ impl ToolProvider for HyperwareToolProvider { Ok(ToolExecutionCommand::HyperwareCallApi { process_id, - method, - args, + signature, timeout, }) } @@ -337,19 +330,19 @@ pub async fn get_api(package_id: &str) -> Result { pub async fn call_api( process_id: &str, - method: &str, - args: &str, + signature: &str, timeout: u64, ) -> Result { let process_id: ProcessId = process_id .parse() .map_err(|e: ProcessIdParseError| e.to_string())?; - // Create request body with method and args - let request_body = serde_json::to_vec(&json!({ - method: serde_json::from_str::(args).unwrap_or_else(|_| json!(args)) - })) - .unwrap(); + // Parse the signature JSON string and use it directly as the request body + let signature_value: Value = serde_json::from_str(signature) + .map_err(|e| format!("Invalid signature JSON: {}", e))?; + + let request_body = serde_json::to_vec(&signature_value) + .map_err(|e| format!("Failed to serialize request body: {}", e))?; // Send the request to the package let request = Request::to(("our", process_id)) diff --git a/hyperdrive/packages/spider/spider/src/tool_providers/mod.rs b/hyperdrive/packages/spider/spider/src/tool_providers/mod.rs index 85816d330..d29d8e457 100644 --- a/hyperdrive/packages/spider/spider/src/tool_providers/mod.rs +++ b/hyperdrive/packages/spider/spider/src/tool_providers/mod.rs @@ -57,8 +57,7 @@ pub enum ToolExecutionCommand { }, HyperwareCallApi { process_id: String, - method: String, - args: String, + signature: String, timeout: u64, }, // Direct result (for synchronous operations) From 84781b512a39433d080084028579e1e9fd7194e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 05:33:43 +0000 Subject: [PATCH 14/14] Format Rust code using rustfmt --- .../spider/crates/hyperware-parse-wit/src/lib.rs | 9 ++++++++- .../spider/spider/src/tool_providers/hyperware.rs | 11 +++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/hyperdrive/packages/spider/crates/hyperware-parse-wit/src/lib.rs b/hyperdrive/packages/spider/crates/hyperware-parse-wit/src/lib.rs index 1b8dd4c0d..40d78395c 100644 --- a/hyperdrive/packages/spider/crates/hyperware-parse-wit/src/lib.rs +++ b/hyperdrive/packages/spider/crates/hyperware-parse-wit/src/lib.rs @@ -552,7 +552,14 @@ mod tests { assert_eq!( value_strings, - vec!["Backlog", "UpNext", "InProgress", "Blocked", "Review", "Done"] + vec![ + "Backlog", + "UpNext", + "InProgress", + "Blocked", + "Review", + "Done" + ] ); } diff --git a/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs b/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs index 916ca0b96..ed8b77151 100644 --- a/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs +++ b/hyperdrive/packages/spider/spider/src/tool_providers/hyperware.rs @@ -328,18 +328,14 @@ pub async fn get_api(package_id: &str) -> Result { Ok(json!(types_with_definitions)) } -pub async fn call_api( - process_id: &str, - signature: &str, - timeout: u64, -) -> Result { +pub async fn call_api(process_id: &str, signature: &str, timeout: u64) -> Result { let process_id: ProcessId = process_id .parse() .map_err(|e: ProcessIdParseError| e.to_string())?; // Parse the signature JSON string and use it directly as the request body - let signature_value: Value = serde_json::from_str(signature) - .map_err(|e| format!("Invalid signature JSON: {}", e))?; + let signature_value: Value = + serde_json::from_str(signature).map_err(|e| format!("Invalid signature JSON: {}", e))?; let request_body = serde_json::to_vec(&signature_value) .map_err(|e| format!("Failed to serialize request body: {}", e))?; @@ -418,7 +414,6 @@ fn extract_docs(docs: &Docs) -> Option { docs.contents.clone() } - // Convert a WIT type definition to a JSON schema representation fn type_to_json_schema(type_def: &wit_parser::TypeDef, resolve: &wit_parser::Resolve) -> Value { use wit_parser::TypeDefKind;