From 8748ae3bb7fbb9bdcf1db9db05eb1510cee34c49 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 03:58:14 +0100 Subject: [PATCH] fix(skills): parse remote which bin maps --- CHANGELOG.md | 1 + src/infra/skills-remote.test.ts | 65 +++++++++++++++++++++++++++++++++ src/infra/skills-remote.ts | 6 +++ 3 files changed, 72 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eacb0c1310f..573a2fb5e90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,6 +121,7 @@ Docs: https://docs.openclaw.ai - Providers/Azure OpenAI: give deployment-scoped image generation requests a longer 600s default timeout so slow `gpt-image-2` generations can complete without a per-call `timeoutMs`. Fixes #71705. Thanks @voytas75. - Gateway/plugins: link source-checkout bundled runtime dependency caches instead of recursively copying `node_modules` on the gateway main thread, preventing local status, node, and skill probes from timing out during startup cache restores. Thanks @steipete. - Skills/remote nodes: only expose remote macOS skill bins for connected nodes, clear stale bin matches when node probes fail, and include probe command, timeout, bin count, and connection state in timeout logs. Thanks @steipete. +- Skills/remote nodes: recognize `system.which` object-map responses when probing connected macOS nodes, so Linux gateways can expose macOS-only skills such as Apple Notes when the required binaries are installed remotely. Fixes #71877. Thanks @miguelarios. - CLI/gateway: keep diagnostic probes from creating first-time read-only device pairings, while still reusing cached device tokens for detailed read probes. Fixes #71766. Thanks @SunboZ. - CLI/plugins: keep `message` startup, `channels logs`, `agents delete`, and `agents set-identity` off broad plugin preloading; message delivery still loads plugins when the action actually runs. - Image understanding: resolve configured image models such as local LM Studio vision entries before reporting `Unknown model` when the discovery registry has not registered that provider. Fixes #66486. Thanks @zhanggpcsu. diff --git a/src/infra/skills-remote.test.ts b/src/infra/skills-remote.test.ts index 60aa80e415c..af26a7ed6d7 100644 --- a/src/infra/skills-remote.test.ts +++ b/src/infra/skills-remote.test.ts @@ -297,4 +297,69 @@ describe("skills-remote", () => { fs.rmSync(workspaceDir, { recursive: true, force: true }); } }); + + it("records bins from system.which object-map responses", async () => { + await resetSkillsRefreshForTest(); + const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-remote-skills-")); + const nodeId = `node-${randomUUID()}`; + const bin = `bin-${randomUUID()}`; + try { + fs.mkdirSync(path.join(workspaceDir, "remote-skill"), { recursive: true }); + fs.writeFileSync( + path.join(workspaceDir, "remote-skill", "SKILL.md"), + [ + "---", + "name: remote-skill", + "description: Needs a remote bin", + `metadata: { "openclaw": { "os": ["darwin"], "requires": { "bins": ["${bin}"] } } }`, + "---", + "# Remote Skill", + "", + ].join("\n"), + ); + const cfg = { + agents: { + defaults: { + workspace: workspaceDir, + }, + }, + } satisfies OpenClawConfig; + const invokeCalls: string[] = []; + setSkillsRemoteRegistry({ + listConnected: () => [], + get: () => undefined, + invoke: async (params: { command: string }) => { + invokeCalls.push(params.command); + return { + ok: true, + payload: { bins: { [bin]: `/opt/homebrew/bin/${bin}`, missing: "" } }, + payloadJSON: JSON.stringify({ bins: { [bin]: `/opt/homebrew/bin/${bin}` } }), + }; + }, + } as unknown as NodeRegistry); + recordRemoteNodeInfo({ + nodeId, + displayName: "Remote Mac", + platform: "darwin", + commands: ["system.run", "system.which"], + }); + const before = getSkillsSnapshotVersion(workspaceDir); + + await refreshRemoteNodeBins({ + nodeId, + platform: "darwin", + commands: ["system.run", "system.which"], + cfg, + timeoutMs: 10, + }); + + expect(invokeCalls).toEqual(["system.which"]); + expect(getRemoteSkillEligibility()?.hasBin(bin)).toBe(true); + expect(getRemoteSkillEligibility()?.hasBin("missing")).toBe(false); + expect(getSkillsSnapshotVersion(workspaceDir)).toBeGreaterThan(before); + } finally { + removeRemoteNodeInfo(nodeId); + fs.rmSync(workspaceDir, { recursive: true, force: true }); + } + }); }); diff --git a/src/infra/skills-remote.ts b/src/infra/skills-remote.ts index f67c28e1bd4..bf1eba30863 100644 --- a/src/infra/skills-remote.ts +++ b/src/infra/skills-remote.ts @@ -254,6 +254,12 @@ function parseBinProbePayload(payloadJSON: string | null | undefined, payload?: if (Array.isArray(parsed.bins)) { return parsed.bins.map((bin) => normalizeOptionalString(String(bin)) ?? "").filter(Boolean); } + if (parsed.bins && typeof parsed.bins === "object") { + return Object.entries(parsed.bins) + .filter(([, resolvedPath]) => normalizeOptionalString(resolvedPath) !== undefined) + .map(([bin]) => normalizeOptionalString(bin) ?? "") + .filter(Boolean); + } if (typeof parsed.stdout === "string") { return parsed.stdout .split(/\r?\n/)