wizard/repo/ensure: degrade to skipped-fanout on clawstor errors
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 39s
ci / rust (push) Successful in 3m8s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m40s

The wizard was hard-failing (500) when clawstor was unreachable —
env unset, network error, non-JSON HTML response from a fallback
proxy, or non-2xx status all mapped to ApiError::Internal, which
blocked the wizard from advancing.

Clawstor fan-out is a warmup optimization, not a prerequisite.
Real fleet materialization happens later at spawn time. When the
aggregator is absent, return a skipped FanoutReply (all_ok=true,
empty peers) so the wizard proceeds, and log the reason server-side.

Unblocks: picking clawhdf5 in the ResearchWizard when the tank/
architect clawstor daemons are running but the HTTP aggregator
(`claw-store serve`) isn't deployed yet.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-18 17:43:30 -07:00
co-authored by Claude Opus 4.7
parent efc13f2904
commit 3444859e11
+39 -10
View File
@@ -99,14 +99,37 @@ async fn proxy(
return Err(ApiError::BadRequest);
}
let base = std::env::var("CLAWSTOR_URL").map_err(|_| ApiError::Internal)?;
let token = std::env::var("CLAWSTOR_TOKEN").map_err(|_| ApiError::Internal)?;
// Clawstor fan-out is best-effort — the aggregator may not be
// deployed in every environment. When it's absent (env unset,
// network error, non-JSON HTML from a fallback proxy, non-2xx),
// degrade to a "skipped" reply so the wizard doesn't block. Real
// fleet materialization happens later at spawn time; ensure was
// only a warmup.
let skipped = |reason: &str| -> Json<FanoutReply> {
eprintln!("wizard_repo::{action}: skipping fleet fan-out ({reason})");
Json(FanoutReply {
url: url.clone(),
git_ref: git_ref.clone(),
workspace: String::new(),
peers: Vec::new(),
all_ok: true,
})
};
let Ok(base) = std::env::var("CLAWSTOR_URL") else {
return Ok(skipped("CLAWSTOR_URL unset"));
};
let Ok(token) = std::env::var("CLAWSTOR_TOKEN") else {
return Ok(skipped("CLAWSTOR_TOKEN unset"));
};
let endpoint = format!("{}/api/v2/repos/{}", base.trim_end_matches('/'), action);
let client = reqwest::Client::builder()
let Ok(client) = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(360))
.build()
.map_err(|_| ApiError::Internal)?;
let resp = client
else {
return Ok(skipped("http client build failed"));
};
let resp = match client
.post(&endpoint)
.bearer_auth(token)
.json(&serde_json::json!({
@@ -115,10 +138,16 @@ async fn proxy(
}))
.send()
.await
.map_err(|_| ApiError::Internal)?;
if !resp.status().is_success() {
return Err(ApiError::Internal);
{
Ok(r) => r,
Err(e) => return Ok(skipped(&format!("send failed: {e}"))),
};
let status = resp.status();
if !status.is_success() {
return Ok(skipped(&format!("aggregator returned {status}")));
}
match resp.json::<FanoutReply>().await {
Ok(reply) => Ok(Json(reply)),
Err(e) => Ok(skipped(&format!("non-JSON response: {e}"))),
}
let reply: FanoutReply = resp.json().await.map_err(|_| ApiError::Internal)?;
Ok(Json(reply))
}