fix: install or review the publisher you picked when ClawHub skills share a slug (#121697)

* fix(skills): keep ClawHub publisher identity from search through install

ClawHub search returns one entry per publisher, so several results can share a
slug. Every client collapsed the selection to that bare slug before calling
skills.detail and skills.install, and ClawHub answered 409 AMBIGUOUS_SKILL_SLUG
with no in-product way forward.

searchClawHubSkills now records the publisher-qualified reference once, on the
result that carries it, and the Gateway protocol documents it. skills.detail
parses the same reference grammar skills.install already accepted, so review and
install cannot resolve to different publishers. Control UI carries that one
reference through row actions, detail, busy state, and acknowledgement retries,
and shows it so otherwise identical rows are distinguishable.

Fixes #117633

* fix(apps): send the ClawHub publisher reference from native skill browsers

macOS, iOS, and Android read the qualified reference from search results and use
it for skills.detail, install, busy state, installed matching, and list identity,
so two publishers sharing a slug stay distinct instead of collapsing into one
ambiguous request.

* fix(skills): refuse external-source skill detail instead of reading a same-slug skill

ClawHub has no source-qualified read endpoint, so a skills-sh reference parsed
down to its bare slug would have returned a registry skill's card while install
resolved the external artifact. Review and install could name different skills.

skills.detail now fails closed on any reference that carries a source, and the
macOS and AgentPro rows show the publisher reference next to the summary instead
of only when a summary is missing, so same-slug rows stay distinguishable.

* chore(apps): refresh native i18n source baseline for the skill row references

* refactor(skills): drop the unread search-result ownerHandle field

installRef is the one reference clients send back, and no client reads the
publisher handle separately, so the protocol and Control UI carry one field
instead of two.

* fix(skills): name the next step when external skill detail is refused

Clients that gate install behind a successful review would otherwise see only a
refusal, so the error names the direct install path and the CLI equivalent.

* fix(macos): use a doc comment on the ClawHub row subtitle

swift-format's docComments rule requires doc comments on declarations; the
subtitle property carried a regular comment and failed macos-swift.

* fix(skills): carry ClawHub trust state to clients that can install

Forwarding installRef let clients install the exact publisher the operator
picked, including external skills-sh sources. It did not forward the trust
state that says ClawHub never scanned that source, so iOS AgentPro — the one
surface that installs in a single tap with no review step — could install an
unscanned artifact with nothing on screen saying so. The CLI already labels
these (docs/clawhub/cli.md, docs/cli/skills.md); native clients could not,
because trustState was never on the wire.

trustState becomes an optional field on SkillsSearchResultSchema. It is purely
additive: older clients ignore an unknown key and the field is absent for
registry results, so downgraded readers are unaffected and no protocol version
moves.

Every client that renders a search row now shows "Not scanned by ClawHub",
matching the CLI wording exactly: iOS AgentPro in the row above the install
button, macOS and Android beside the review action, and Control UI on the row
that explains why review is refused for these sources.

Covered by a wire assertion that the state reaches clients for an external
source and stays absent for registry rows, plus decode-and-label tests on the
shared Swift kit and the Android parser, and a Control UI render assertion.

* fix(ui): size the ClawHub detail dialog to a refusal message

Refusing detail for an external source made an error-only dialog reachable.
The shared preview panel reserves a tall reader height for skill documents, so
a two-line refusal rendered in a mostly empty dialog and read as broken rather
than deliberate. Found by inspecting the review captures.

* revert(ui,apps): drop the ClawHub trust label layer

Maintainer product decision: skills.sh runs its own scanners, so OpenClaw does
not add a second alert layer in the apps. Removes the label from Control UI,
iOS, macOS and Android, and drops the trustState wire field that nothing would
render. The CLI keeps its existing label; changing that is a separate call.

Publisher identity, the fail-closed detail refusal, and the message-only dialog
are unchanged. Splits the oversized skills view test file to satisfy max-lines
without a suppression.

* test(ui): fix ClawHub skill fixture checks

* chore(plugin-sdk): refresh API baseline

---------

Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
This commit is contained in:
Vyctor H. Brzezowski
2026-08-13 00:13:15 -03:00
committed by GitHub
parent f5ad8735d1
commit aba94bbe0b
45 changed files with 1139 additions and 699 deletions
@@ -356,6 +356,14 @@ export const SkillsUploadCommitParamsSchema = closedObject({
sha256: Type.Optional(Sha256String),
});
/**
* ClawHub resolves a bare slug against every publisher, so requests that carry only the slug
* fail with 409 AMBIGUOUS_SKILL_SLUG once two publishers share it. Clients send the reference
* `skills.search` returned for the entry the operator picked.
*/
const CLAWHUB_SKILL_REF_DESCRIPTION =
"ClawHub skill reference: `@owner/slug`, `skills-sh:owner/repo/slug`, or a bare `slug` when no publisher is known.";
/** Installs a skill from legacy install id, ClawHub, or uploaded archive. */
export const SkillsInstallParamsSchema = Type.Union([
closedObject({
@@ -374,7 +382,7 @@ export const SkillsInstallParamsSchema = Type.Union([
closedObject({
agentId: Type.Optional(NonEmptyString),
source: Type.Literal("clawhub"),
slug: NonEmptyString,
slug: Type.String({ minLength: 1, description: CLAWHUB_SKILL_REF_DESCRIPTION }),
version: Type.Optional(NonEmptyString),
force: Type.Optional(Type.Boolean()),
acknowledgeClawHubRisk: Type.Optional(Type.Boolean()),
@@ -420,17 +428,25 @@ export const SkillsSearchResultSchema = closedObject({
closedObject({
score: Type.Number(),
slug: NonEmptyString,
installRef: Type.Optional(
Type.String({
minLength: 1,
description:
"Publisher-qualified reference for this result. Send it as `slug` to skills.detail and skills.install; several publishers can share one slug.",
}),
),
displayName: NonEmptyString,
summary: Type.Optional(Type.String()),
icon: Type.Optional(Type.Union([Type.String(), Type.Null()])),
version: Type.Optional(NonEmptyString),
updatedAt: Type.Optional(Type.Integer()),
}),
),
});
/** Reads registry detail for one skill slug. */
/** Reads registry detail for one skill. */
export const SkillsDetailParamsSchema = closedObject({
slug: NonEmptyString,
slug: Type.String({ minLength: 1, description: CLAWHUB_SKILL_REF_DESCRIPTION }),
});
/** Reads current security verdicts for configured skills. */