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);