ClawHub skill install fails with 409 Ambiguous slug — ownerHandle not supported
Summary
The ClawHub skill installer cannot install any skill whose slug is shared by more than one publisher. The ClawHub API returns 409 Conflict ("Ambiguous skill slug") and requires an ownerHandle to disambiguate, but OpenFang's ClawHubInstallRequest only carries a slug field and the client never sends ownerHandle. As a result, the dashboard "install skill" flow fails for a large fraction of popular skills (e.g. weather, which has 4 publishers).
Symptoms
- Component:
openfang-api→POST /api/clawhub/install, backed byopenfang_skills::clawhub::ClawHubClient::install_with_options. - Repro:Response:
POST /api/clawhub/install {"slug":"weather"}{"error":"Network error: ClawHub download returned 409 Conflict"} - Upstream API behaviour (clawhub.ai):
GET /api/v1/download?slug=weather → 409 Ambiguous skill slug "weather". Multiple publishers use this slug. Retry with ownerHandle, for example: /api/v1/download?slug=weather&ownerHandle=<owner>. GET /api/v1/skills/weather → {"code":"AMBIGUOUS_SKILL_SLUG","matches":[{"ownerHandle":"steipete",...}, ...]} GET /api/v1/download?slug=weather&ownerHandle=steipete → 200 OK
Root cause
crates/openfang-api/src/types.rs:
pub struct ClawHubInstallRequest {
/// ClawHub skill slug (e.g., "github-helper").
pub slug: String,
}and crates/openfang-skills/src/clawhub.rs builds the download URL without an owner:
let url = format!("{}/download?slug={}", self.base_url, urlencoded(slug));ClawHub's /api/v1/download endpoint is ambiguous-slug-aware and returns 409 unless ownerHandle is supplied. Because the request type has no field for it and the client never appends one, every ambiguous slug fails. Unique slugs install fine; the failure is a server-side ambiguity rejection, not a permissions/network issue.
Proposed fix
- Add an optional
owner_handle: Option<String>toClawHubInstallRequest. - Thread it through
ClawHubClient::install/install_with_optionsand append&ownerHandle=<handle>when present. - On a
409response, surface the ambiguity (optionally by calling/api/v1/skills/{slug}to list matches) so the UI can let the user pick a publisher instead of failing opaquely.
Minimal change sketch:
// types.rs
pub struct ClawHubInstallRequest {
pub slug: String,
#[serde(default)]
pub owner_handle: Option<String>,
}
// clawhub.rs
let mut url = format!("{}/download?slug={}", self.base_url, urlencoded(slug));
if let Some(owner) = &owner_handle {
url.push_str(&format!("&ownerHandle={}", urlencoded(owner)));
}Affected resources
| Resource | Identifier | Notes |
|---|---|---|
| Request type | crates/openfang-api/src/types.rs (ClawHubInstallRequest) |
add owner_handle |
| Client | crates/openfang-skills/src/clawhub.rs (install_with_options, get_with_retry) |
append ownerHandle, handle 409 |
| Route | crates/openfang-api/src/routes.rs (clawhub_install) |
pass through new field |
| UI | dashboard install dialog | optional publisher picker on ambiguity |
Investigation audit trail
- Reproduced
409 Conflictvia the live API on a running instance (Dockerized, container has outbound internet). - Confirmed the ClawHub API itself works when disambiguated:
?slug=weather&ownerHandle=steipete→200. - Confirmed
ClawHubInstallRequesthas noowner_handlefield and the client never sends one (grep forownerHandle/owner_handleinclawhub.rsreturns nothing). - Ruled out permissions/network: container runs as root,
/datais writable, outbound HTTPS to bothapi.github.comandclawhub.aisucceeds.
Severity
Standard defect, low blast radius — install works for unique slugs; only ambiguous slugs are affected. Workaround exists (install a uniquely-named skill, or drop the file into the skills dir manually).
Source: RightNow-AI/openfang