From 002011527c8b70d3a72f9d05d82068146dd7caaf Mon Sep 17 00:00:00 2001 From: ziy Date: Fri, 20 Mar 2026 20:52:04 +0800 Subject: [PATCH 1/2] fix(agents): Ollama provider always returns synthetic auth when no config exists When a user sets models.primary = "ollama/..." without running the provider setup wizard, the provider config entry does not exist in models.providers. The previous resolveSyntheticLocalProviderAuth() returned null early because providerConfig was undefined, causing the auth resolver to throw "No API key found for provider ollama". This regression (v2026.3.13) made it impossible to use Ollama local models via direct config without interactive setup. Fix: check for known local providers (ollama) at the top of the function, before resolving providerConfig. For ollama, return synthetic auth regardless of config state. Fixes: #50759 --- src/agents/model-auth.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index e494cc71b8c..09e5c0397d9 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -167,6 +167,18 @@ function resolveSyntheticLocalProviderAuth(params: { cfg: OpenClawConfig | undefined; provider: string; }): ResolvedProviderAuth | null { + // Check for known local providers first — these should always get synthetic auth, + // even when the user sets models.primary without running the provider setup wizard + // (i.e., no explicit models.providers.{name} config entry exists). + const normalizedProvider = normalizeProviderId(params.provider); + if (normalizedProvider === "ollama") { + return { + apiKey: OLLAMA_LOCAL_AUTH_MARKER, + source: "models.providers.ollama (synthetic local key)", + mode: "api-key", + }; + } + const providerConfig = resolveProviderConfig(params.cfg, params.provider); if (!providerConfig) { return null; @@ -180,15 +192,6 @@ function resolveSyntheticLocalProviderAuth(params: { return null; } - const normalizedProvider = normalizeProviderId(params.provider); - if (normalizedProvider === "ollama") { - return { - apiKey: OLLAMA_LOCAL_AUTH_MARKER, - source: "models.providers.ollama (synthetic local key)", - mode: "api-key", - }; - } - const authOverride = resolveProviderAuthOverride(params.cfg, params.provider); if (authOverride && authOverride !== "api-key") { return null; From 6f42fb105ff17f7a674412adc58091e1df899a95 Mon Sep 17 00:00:00 2001 From: ziy Date: Fri, 20 Mar 2026 21:04:02 +0800 Subject: [PATCH 2/2] fix(cli): nodes list uses node.list as primary source to fix k8s pairing store inconsistency `nodes list` called node.pair.list as the primary data source, which returns an empty result in environments where the pairing store (file/DB) is not shared between the gateway and the node host (e.g. k8s volumes, Docker). This caused `nodes list` to show 'Paired: 0' while `nodes status` (which calls node.list) correctly showed the paired node. Fix: make `nodes list` use node.list as the authoritative source and node.pair.list as a secondary enrichment source. This aligns both commands to show consistent results regardless of pairing store availability. Fixes: #50847 --- src/cli/nodes-cli/register.status.ts | 106 ++++++++++++++++++--------- 1 file changed, 71 insertions(+), 35 deletions(-) diff --git a/src/cli/nodes-cli/register.status.ts b/src/cli/nodes-cli/register.status.ts index 03e00cbbec4..abaf5d14c83 100644 --- a/src/cli/nodes-cli/register.status.ts +++ b/src/cli/nodes-cli/register.status.ts @@ -304,54 +304,89 @@ export function registerNodesStatusCommands(nodes: Command) { await runNodesCommand("list", async () => { const connectedOnly = Boolean(opts.connected); const sinceMs = parseSinceMs(opts.lastConnected, "Invalid --last-connected"); - const result = await callGatewayCli("node.pair.list", opts, {}); - const { pending, paired } = parsePairingList(result); + // Call both sources in parallel: + // - node.list: device pairing (always returns known nodes) + // - node.pair.list: gateway-owned node-pairing store — needed because `nodes approve` + // writes here and those nodes may not appear in the device pairing list (k8s volumes, + // separate data directories). Showing both sources fixes the "Paired: 0" bug. + const [nodeListResult, pairingResult] = await Promise.all([ + callGatewayCli("node.list", opts, {}), + callGatewayCli("node.pair.list", opts, {}), + ]); + const { pending, paired: pairingPaired } = parsePairingList(pairingResult); + const deviceNodes = parseNodeList(nodeListResult); + + // Build lookup maps keyed by nodeId + const pairingById = new Map(pairingPaired.map((n) => [n.nodeId, n])); + const deviceById = new Map(deviceNodes.map((n) => [n.nodeId, n])); + + // Merge both sources: start with device nodes, then add pairing-only nodes + const allNodesMap = new Map(deviceById); + for (const pNode of pairingPaired) { + if (!allNodesMap.has(pNode.nodeId)) { + allNodesMap.set(pNode.nodeId, { + nodeId: pNode.nodeId, + displayName: pNode.displayName, + platform: pNode.platform, + version: pNode.version, + coreVersion: pNode.coreVersion, + uiVersion: pNode.uiVersion, + remoteIp: pNode.remoteIp, + paired: true, + connected: false, + connectedAtMs: pNode.lastConnectedAtMs, + }); + } + } + const allNodes = Array.from(allNodesMap.values()); + const { heading, muted, warn } = getNodesTheme(); const tableWidth = getTerminalTableWidth(); const now = Date.now(); const hasFilters = connectedOnly || sinceMs !== undefined; const pendingRows = hasFilters ? [] : pending; - const connectedById = hasFilters - ? new Map( - parseNodeList(await callGatewayCli("node.list", opts, {})).map((node) => [ - node.nodeId, - node, - ]), - ) - : null; - const filteredPaired = paired.filter((node) => { - if (connectedOnly) { - const live = connectedById?.get(node.nodeId); - if (!live?.connected) { - return false; - } - } + + const filteredPaired = allNodes.filter((node) => { + if (!node.paired && !pairingById.has(node.nodeId)) return false; + if (connectedOnly && !node.connected) return false; if (sinceMs !== undefined) { - const live = connectedById?.get(node.nodeId); + const pData = pairingById.get(node.nodeId); const lastConnectedAtMs = - typeof node.lastConnectedAtMs === "number" - ? node.lastConnectedAtMs - : typeof live?.connectedAtMs === "number" - ? live.connectedAtMs + typeof pData?.lastConnectedAtMs === "number" + ? pData.lastConnectedAtMs + : typeof node.connectedAtMs === "number" + ? node.connectedAtMs : undefined; - if (typeof lastConnectedAtMs !== "number") { - return false; - } - if (now - lastConnectedAtMs > sinceMs) { - return false; - } + if (typeof lastConnectedAtMs !== "number") return false; + if (now - lastConnectedAtMs > sinceMs) return false; } return true; }); + + const totalPairedCount = allNodes.filter( + (n) => n.paired || pairingById.has(n.nodeId), + ).length; const filteredLabel = - hasFilters && filteredPaired.length !== paired.length ? ` (of ${paired.length})` : ""; + hasFilters && filteredPaired.length !== totalPairedCount + ? ` (of ${totalPairedCount})` + : ""; defaultRuntime.log( `Pending: ${pendingRows.length} · Paired: ${filteredPaired.length}${filteredLabel}`, ); if (opts.json) { defaultRuntime.log( - JSON.stringify({ pending: pendingRows, paired: filteredPaired }, null, 2), + JSON.stringify( + { + pending: pendingRows, + paired: filteredPaired.map((n) => ({ + ...n, + ...(pairingById.get(n.nodeId) ?? {}), + })), + }, + null, + 2, + ), ); return; } @@ -370,12 +405,12 @@ export function registerNodesStatusCommands(nodes: Command) { if (filteredPaired.length > 0) { const pairedRows = filteredPaired.map((n) => { - const live = connectedById?.get(n.nodeId); + const pData = pairingById.get(n.nodeId); const lastConnectedAtMs = - typeof n.lastConnectedAtMs === "number" - ? n.lastConnectedAtMs - : typeof live?.connectedAtMs === "number" - ? live.connectedAtMs + typeof pData?.lastConnectedAtMs === "number" + ? pData.lastConnectedAtMs + : typeof n.connectedAtMs === "number" + ? n.connectedAtMs : undefined; return { Node: n.displayName?.trim() ? n.displayName.trim() : n.nodeId, @@ -402,6 +437,7 @@ export function registerNodesStatusCommands(nodes: Command) { }).trimEnd(), ); } + } }); }), );