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.
This commit is contained in:
Peter Lee
2026-08-20 11:57:53 -05:00
committed by GitHub
parent ccabd53235
commit 41d05f6bdf
3 changed files with 57 additions and 5 deletions
@@ -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 = {
+13 -1
View File
@@ -179,6 +179,7 @@ describe("runAuthProbes", () => {
authProfileId?: string;
authProfileIdSource?: string;
config?: OpenClawConfig;
preparedModelRuntimeMode?: string;
}): Promise<AgentRunResultView> => {
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,
+24 -4
View File
@@ -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);