#1284·openfang

ClawHub skill install fails with 409 Ambiguous slug — ownerHandle not supported

Author: spencerkittlesonCreated Sep 5, 2026Updated Sep 5, 2026

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-apiPOST /api/clawhub/install, backed by openfang_skills::clawhub::ClawHubClient::install_with_options.
  • Repro:
    POST /api/clawhub/install
    {"slug":"weather"}
    Response:
    {"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:

rust
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:

rust
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

  1. Add an optional owner_handle: Option<String> to ClawHubInstallRequest.
  2. Thread it through ClawHubClient::install / install_with_options and append &ownerHandle=<handle> when present.
  3. On a 409 response, 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:

rust
// 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

  1. Reproduced 409 Conflict via the live API on a running instance (Dockerized, container has outbound internet).
  2. Confirmed the ClawHub API itself works when disambiguated: ?slug=weather&ownerHandle=steipete200.
  3. Confirmed ClawHubInstallRequest has no owner_handle field and the client never sends one (grep for ownerHandle/owner_handle in clawhub.rs returns nothing).
  4. Ruled out permissions/network: container runs as root, /data is writable, outbound HTTPS to both api.github.com and clawhub.ai succeeds.

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).