From 41d05f6bdff4096d791f9c5765cb37dcae552ee2 Mon Sep 17 00:00:00 2001 From: Peter Lee Date: Thu, 20 Aug 2026 11:57:53 -0500 Subject: [PATCH] fix(models): Web UI provider probe fails for direct-credential providers (#125816) * fix(models): keep direct-credential gateway probes on isolated runtime generations * fix(models): carry isolated-runtime probe mode on the loader type Drop the call-site cast for the widened runner params: the lazy loader now declares the isolated-read-only capable runEmbeddedAgent shape, keeping the assertion-safety ratchet at its grandfathered baseline for this file. --- ...ared-model-runtime.owner-selection.test.ts | 20 +++++++++++++ src/commands/models/list.probe.test.ts | 14 +++++++++- src/commands/models/list.probe.ts | 28 ++++++++++++++++--- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/agents/prepared-model-runtime.owner-selection.test.ts b/src/agents/prepared-model-runtime.owner-selection.test.ts index 2d31445fae06..13fd8fdcc8ac 100644 --- a/src/agents/prepared-model-runtime.owner-selection.test.ts +++ b/src/agents/prepared-model-runtime.owner-selection.test.ts @@ -13,6 +13,7 @@ import { retainLegacyDefaultAgentId } from "../config/legacy.default-agent-owner import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { acquireAgentRunPreparedModelRuntime, + acquireReadOnlyPreparedModelRuntime, getPreparedModelRuntimeSnapshot, loadPublishedGatewayReplyDispatchRuntime, loadPreparedModelRuntimeSnapshot, @@ -154,6 +155,25 @@ describe("prepared model runtime owner selection", () => { ).rejects.toThrow("prepared model runtime owner was not published"); }); + it("keeps a caller-pinned agent dir for isolated read-only leases on an active gateway", async () => { + mocks.configuredAgentIds = ["default"]; + const config = {}; + await refreshPreparedModelRuntimeSnapshots(config, { gatewayLifecycle: true }); + + // Run-provenance leases rebind a pinned agentDir to the committed configured + // owner; isolated read-only leases keep the pinned dir so synthetic probe + // credentials stay resolvable from that generation's auth store. + const lease = await acquireReadOnlyPreparedModelRuntime({ + agentId: "default", + config, + agentDir: "/tmp/isolated-probe-agent", + inheritedAuthDir: "/tmp/unused-agent", + workspaceDir: "/tmp/isolated-probe-workspace", + }); + expect(lease.snapshot.agentDir).toBe("/tmp/isolated-probe-agent"); + lease.release(); + }); + it("publishes provider selections kept on the core runtime by request parameters", async () => { mocks.configuredAgentIds = ["default"]; const config = { diff --git a/src/commands/models/list.probe.test.ts b/src/commands/models/list.probe.test.ts index 29445d049288..fde02664cf4d 100644 --- a/src/commands/models/list.probe.test.ts +++ b/src/commands/models/list.probe.test.ts @@ -179,6 +179,7 @@ describe("runAuthProbes", () => { authProfileId?: string; authProfileIdSource?: string; config?: OpenClawConfig; + preparedModelRuntimeMode?: string; }): Promise => { if (params.agentHarnessRuntimeOverride !== "openclaw") { throw new Error( @@ -262,6 +263,9 @@ describe("runAuthProbes", () => { authProfileIdSource: "user", }), ); + // Profile targets reuse the committed configured generation: no isolated + // runtime mode is requested. + expect(runEmbeddedAgent.mock.calls[0]?.[0].preparedModelRuntimeMode).toBeUndefined(); runEmbeddedAgent.mockResolvedValueOnce({ payloads: [{ text: "LLM request timed out.", isError: true }], @@ -308,6 +312,7 @@ describe("runAuthProbes", () => { authProfileId?: string; authProfileIdSource?: string; config?: OpenClawConfig; + preparedModelRuntimeMode?: string; }) => ({ payloads: [{ text: "OK" }] }), ); vi.doMock("../../agents/embedded-agent.js", () => ({ runEmbeddedAgent })); @@ -386,6 +391,7 @@ describe("runAuthProbes", () => { ); expect(configKeyCall?.[0].agentDir).not.toBe("/tmp/openclaw-probe-agent"); expect(configKeyCall?.[0].authProfileIdSource).toBe("user"); + expect(configKeyCall?.[0].preparedModelRuntimeMode).toBe("isolated-read-only"); expect(configKeyCall?.[0].config).toMatchObject({ models: { providers: { @@ -421,7 +427,12 @@ describe("runAuthProbes", () => { it("isolates marker credentials from stored profiles without pinning a synthetic one", async () => { const runEmbeddedAgent = vi.fn( - async (_params: { agentDir?: string; authProfileId?: string; config?: OpenClawConfig }) => ({ + async (_params: { + agentDir?: string; + authProfileId?: string; + config?: OpenClawConfig; + preparedModelRuntimeMode?: string; + }) => ({ payloads: [{ text: "OK" }], }), ); @@ -490,6 +501,7 @@ describe("runAuthProbes", () => { const call = runEmbeddedAgent.mock.calls[0]?.[0]; expect(call?.agentDir).not.toBe("/tmp/openclaw-probe-agent"); expect(call?.agentDir).toContain("openclaw-auth-probe-"); + expect(call?.preparedModelRuntimeMode).toBe("isolated-read-only"); expect(call?.config?.auth?.order?.openai).toEqual([]); expect(call?.config?.models?.providers?.openai?.apiKey).toBe( cfg.models.providers.openai.apiKey, diff --git a/src/commands/models/list.probe.ts b/src/commands/models/list.probe.ts index 53b806c511bd..c77b10e78540 100644 --- a/src/commands/models/list.probe.ts +++ b/src/commands/models/list.probe.ts @@ -75,9 +75,22 @@ export function redactAuthProbeError(error: string): string { return redactStatusSecrets(error); } -const embeddedRunnerModuleLoader = createLazyImportLoader( - () => import("../../agents/embedded-agent.js"), -); +/** Widened runner call shape for isolated auth probe generations (see setup-inference-core). */ +type ProbeRunEmbeddedAgentParams = Parameters< + (typeof import("../../agents/embedded-agent.js"))["runEmbeddedAgent"] +>[0] & { + preparedModelRuntimeMode?: "isolated-read-only"; +}; + +type ProbeRunEmbeddedAgent = ( + params: ProbeRunEmbeddedAgentParams, +) => ReturnType<(typeof import("../../agents/embedded-agent.js"))["runEmbeddedAgent"]>; + +// The probe only calls runEmbeddedAgent; the widened loader type lets the call +// request the isolated-read-only runtime generation without a call-site cast. +const embeddedRunnerModuleLoader = createLazyImportLoader<{ + runEmbeddedAgent: ProbeRunEmbeddedAgent; +}>(() => import("../../agents/embedded-agent.js")); function loadEmbeddedRunnerModule() { return embeddedRunnerModuleLoader.load(); @@ -808,7 +821,10 @@ async function probeTarget(params: { // Any bound-value target runs in an empty agent dir so stored profiles are // absent and cannot satisfy the probe via failover. Direct values pin a // synthetic profile; marker values are resolved by the runtime from the - // profile-order-cleared config. + // profile-order-cleared config. Inside a Gateway, the isolated-read-only + // runtime mode set on the runner call keeps this pinned generation + // authoritative: a run-provenance lease would rebind it to the committed + // configured owner and lose the synthetic profile. if (target.boundValue || target.useRuntimeAuth) { // Canonicalize so the isolated agent DB registers and unregisters under // one path. os.tmpdir() is a symlink on macOS (/var -> /private/var), and @@ -875,6 +891,10 @@ async function probeTarget(params: { disableTools: true, modelRun: true, cleanupBundleMcpOnRunEnd: true, + // Keep the isolated generation outside configured Gateway ownership: a + // run-provenance lease rebinds the pinned agentDir to the committed + // configured owner, losing the synthetic probe profile below. + ...(isolatedAgentDir ? { preparedModelRuntimeMode: "isolated-read-only" as const } : {}), abortSignal: params.abortSignal, })) as AgentRunResultView; const terminalError = extractAgentRunTerminalError(runResult);