diff --git a/CHANGELOG.md b/CHANGELOG.md index 561c4af8855..fd5ba6b57c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai - Memory/status: keep plain `openclaw memory status` and `openclaw memory status --json` on the cheap read-only path by reserving vector and embedding provider probes for `--deep` or `--index`. Fixes #76769. Thanks @daruire. - Telegram: suppress stale same-session replies when a newer accepted message arrives before an older in-flight Telegram dispatch finalizes. Fixes #76642. Thanks @chinar-amrutkar. - Gateway/diagnostics: abort-drain embedded runs after an extended no-progress stall so a single dead session no longer leaves queued Discord/channel turns blocked behind repeated `recovery=none` liveness warnings. +- Plugins/ClawHub: accept the live artifact resolver `kind`/`sha256` field names alongside the typed `artifactKind`/`artifactSha256` form so `clawhub:` installs of npm-pack and legacy ZIP packages no longer miss downloadable artifacts. Thanks @romneyda. - Control UI/Sessions: avoid full `sessions.list` reloads for chat-turn `sessions.changed` payloads, so large session stores no longer add multi-second delays while chat responses are being delivered. (#76676) Thanks @VACInc. - Gateway/watch: run `doctor --fix --non-interactive` once and retry when the dev Gateway child exits during startup, so stale local plugin install/config state does not leave the tmux watch session disappearing without a repair attempt. - Doctor/Telegram: warn when selected Telegram quote replies can suppress `streaming.preview.toolProgress`, and document the `replyToMode` trade-off without changing runtime delivery. Fixes #73487. Thanks @GodsBoy. diff --git a/src/plugins/clawhub.test.ts b/src/plugins/clawhub.test.ts index 656e915c6e8..99cc2a922a9 100644 --- a/src/plugins/clawhub.test.ts +++ b/src/plugins/clawhub.test.ts @@ -51,10 +51,12 @@ vi.mock("../infra/archive.js", async () => { }); const { ClawHubRequestError } = await import("../infra/clawhub.js"); +type ClawHubResolvedArtifact = import("../infra/clawhub.js").ClawHubResolvedArtifact; const { CLAWHUB_INSTALL_ERROR_CODE, formatClawHubSpecifier, installPluginFromClawHub } = await import("./clawhub.js"); const DEMO_ARCHIVE_INTEGRITY = "sha256-qerEjGEpvES2+Tyan0j2xwDRkbcnmh4ZFfKN9vWbsa8="; +const DEMO_ARCHIVE_SHA256 = "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af"; const DEMO_CLAWPACK_SHA256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const DEMO_CLAWPACK_INTEGRITY = `sha256-${Buffer.from(DEMO_CLAWPACK_SHA256, "hex").toString( "base64", @@ -463,6 +465,102 @@ describe("installPluginFromClawHub", () => { ); }); + it("accepts the live ClawHub artifact resolver shape with kind/sha256 field names", async () => { + fetchClawHubPackageVersionMock.mockClear(); + fetchClawHubPackageArtifactMock.mockResolvedValueOnce({ + package: { + name: "demo", + displayName: "Demo", + family: "code-plugin", + }, + version: "2026.3.22", + artifact: { + kind: "npm-pack", + sha256: DEMO_CLAWPACK_SHA256, + npmIntegrity: "sha512-clawpack", + npmShasum: "1".repeat(40), + } as unknown as ClawHubResolvedArtifact, + }); + downloadClawHubPackageArchiveMock.mockResolvedValueOnce({ + archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz", + integrity: DEMO_CLAWPACK_INTEGRITY, + sha256Hex: DEMO_CLAWPACK_SHA256, + artifact: "clawpack", + clawpackHeaderSha256: DEMO_CLAWPACK_SHA256, + npmIntegrity: "sha512-clawpack", + npmShasum: "1".repeat(40), + cleanup: archiveCleanupMock, + }); + + const result = await installPluginFromClawHub({ + spec: "clawhub:demo", + baseUrl: "https://clawhub.ai", + }); + + expect(result).toMatchObject({ + ok: true, + clawhub: { + artifactKind: "npm-pack", + artifactFormat: "tgz", + npmIntegrity: "sha512-clawpack", + npmShasum: "1".repeat(40), + clawpackSha256: DEMO_CLAWPACK_SHA256, + }, + }); + expect(fetchClawHubPackageVersionMock).not.toHaveBeenCalled(); + expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( + expect.objectContaining({ + artifact: "clawpack", + name: "demo", + version: "2026.3.22", + }), + ); + }); + + it("accepts the live ClawHub legacy zip resolver shape with kind/sha256 field names", async () => { + fetchClawHubPackageVersionMock.mockClear(); + fetchClawHubPackageArtifactMock.mockResolvedValueOnce({ + package: { + name: "demo", + displayName: "Demo", + family: "code-plugin", + }, + version: "2026.3.22", + artifact: { + kind: "legacy-zip", + sha256: DEMO_ARCHIVE_SHA256, + } as unknown as ClawHubResolvedArtifact, + }); + downloadClawHubPackageArchiveMock.mockResolvedValueOnce({ + archivePath: "/tmp/clawhub-demo/archive.zip", + integrity: DEMO_ARCHIVE_INTEGRITY, + cleanup: archiveCleanupMock, + }); + + const result = await installPluginFromClawHub({ + spec: "clawhub:demo", + baseUrl: "https://clawhub.ai", + }); + + expect(result).toMatchObject({ + ok: true, + pluginId: "demo", + clawhub: { + artifactKind: "legacy-zip", + artifactFormat: "zip", + integrity: DEMO_ARCHIVE_INTEGRITY, + }, + }); + expect(fetchClawHubPackageVersionMock).not.toHaveBeenCalled(); + expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( + expect.objectContaining({ + artifact: "archive", + name: "demo", + version: "2026.3.22", + }), + ); + }); + it("falls back to version metadata when the ClawHub artifact resolver route is missing", async () => { fetchClawHubPackageArtifactMock.mockRejectedValueOnce( new ClawHubRequestError({ diff --git a/src/plugins/clawhub.ts b/src/plugins/clawhub.ts index 606c70bee20..558016b083f 100644 --- a/src/plugins/clawhub.ts +++ b/src/plugins/clawhub.ts @@ -265,29 +265,47 @@ function normalizeArtifactResolverFiles( return files as NonNullable["files"]; } +type ClawHubResolvedArtifactWire = { + artifactKind?: string | null; + kind?: string | null; + artifactSha256?: string | null; + sha256?: string | null; + npmIntegrity?: string | null; + npmShasum?: string | null; + downloadUrl?: string | null; +}; + function resolveTopLevelNpmPackArtifact( artifact: ClawHubResolvedArtifact | null | undefined, ): ClawHubPackageArtifactSummary | null { - if (artifact?.artifactKind !== "npm-pack") { + const wire = artifact as ClawHubResolvedArtifactWire | null | undefined; + const artifactKind = wire?.artifactKind ?? wire?.kind; + if (artifactKind !== "npm-pack") { + return null; + } + if (typeof wire?.npmIntegrity !== "string") { return null; } return { kind: "npm-pack", format: "tgz", - sha256: artifact.artifactSha256 ?? null, - npmIntegrity: artifact.npmIntegrity, - npmShasum: artifact.npmShasum ?? null, - downloadUrl: artifact.downloadUrl ?? null, + sha256: wire.artifactSha256 ?? wire.sha256 ?? null, + npmIntegrity: wire.npmIntegrity, + npmShasum: wire.npmShasum ?? null, + downloadUrl: wire.downloadUrl ?? null, }; } function resolveTopLevelLegacyArchiveVerification( artifact: ClawHubResolvedArtifact | null | undefined, ): ClawHubArchiveVerification | null { - if (artifact?.artifactKind !== "legacy-zip" || typeof artifact.artifactSha256 !== "string") { + const wire = artifact as ClawHubResolvedArtifactWire | null | undefined; + const artifactKind = wire?.artifactKind ?? wire?.kind; + const artifactSha256 = wire?.artifactSha256 ?? wire?.sha256; + if (artifactKind !== "legacy-zip" || typeof artifactSha256 !== "string") { return null; } - const integrity = normalizeClawHubSha256Integrity(artifact.artifactSha256); + const integrity = normalizeClawHubSha256Integrity(artifactSha256); return integrity ? { kind: "archive-integrity", integrity } : null; }