From f8d208a0d5a07ce2c52cd1358e515d0f2f8fdd92 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Wed, 12 Aug 2026 03:47:04 -0500 Subject: [PATCH] fix(models): keep prepared auth contracts acyclic --- src/agents/agent-auth-credential-modes.ts | 2 ++ src/agents/agent-auth-credentials.ts | 2 +- src/agents/model-auth-availability.test.ts | 2 +- src/agents/model-auth-availability.ts | 2 +- ...enclaw-tools.browser-plugin.integration.test.ts | 1 + src/agents/prepared-model-catalog.types.ts | 2 +- src/agents/prepared-model-runtime.types.ts | 2 +- src/gateway/local-request-context.test.ts | 3 +++ .../server-methods/chat-metadata-runtime.test.ts | 10 ++++++++++ .../server-methods/models-list-auth-resolver.ts | 2 +- src/gateway/server-methods/models-list-result.ts | 2 +- src/gateway/server-model-catalog.test.ts | 6 +++++- .../server.chat.gateway-server-chat-b.test.ts | 14 ++++++++++++-- .../worker-environments/inference-runtime.test.ts | 1 + 14 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 src/agents/agent-auth-credential-modes.ts diff --git a/src/agents/agent-auth-credential-modes.ts b/src/agents/agent-auth-credential-modes.ts new file mode 100644 index 000000000000..349b197ad370 --- /dev/null +++ b/src/agents/agent-auth-credential-modes.ts @@ -0,0 +1,2 @@ +/** Secret-free credential modes captured by a prepared agent runtime. */ +export type PreparedAgentCredentialModes = Readonly>; diff --git a/src/agents/agent-auth-credentials.ts b/src/agents/agent-auth-credentials.ts index 5ff392c252a8..b5082319702f 100644 --- a/src/agents/agent-auth-credentials.ts +++ b/src/agents/agent-auth-credentials.ts @@ -4,6 +4,7 @@ import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion" import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { coerceSecretRef } from "../config/types.secrets.js"; +import type { PreparedAgentCredentialModes } from "./agent-auth-credential-modes.js"; import { resolveAuthProfileOrder } from "./auth-profiles/order.js"; import type { AuthProfileCredential, AuthProfileStore } from "./auth-profiles/types.js"; import type { AuthStorageData } from "./sessions/auth-storage.js"; @@ -22,7 +23,6 @@ type AgentOAuthCredential = { /** Credential value shape consumed by agent runtimes after auth-profile normalization. */ type AgentCredential = AgentApiKeyCredential | AgentOAuthCredential; export type AgentCredentialMap = Record; -export type PreparedAgentCredentialModes = Readonly>; type ResolveAgentCredentialMapOptions = { includeSecretRefPlaceholders?: boolean; diff --git a/src/agents/model-auth-availability.test.ts b/src/agents/model-auth-availability.test.ts index 6aab5a566345..1f5528271a8a 100644 --- a/src/agents/model-auth-availability.test.ts +++ b/src/agents/model-auth-availability.test.ts @@ -5,7 +5,7 @@ import type { ProviderModelRouteResolution, } from "../plugin-sdk/provider-model-types.js"; import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js"; -import type { PreparedAgentCredentialModes } from "./agent-auth-credentials.js"; +import type { PreparedAgentCredentialModes } from "./agent-auth-credential-modes.js"; import type { RuntimeAuthMaterialization } from "./auth-profiles/runtime-materializations.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; import { diff --git a/src/agents/model-auth-availability.ts b/src/agents/model-auth-availability.ts index 3366be21c3f7..6d95c709abff 100644 --- a/src/agents/model-auth-availability.ts +++ b/src/agents/model-auth-availability.ts @@ -17,7 +17,7 @@ import type { } from "../plugin-sdk/provider-model-types.js"; import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js"; import { isValidSecretRef } from "../secrets/ref-contract.js"; -import type { PreparedAgentCredentialModes } from "./agent-auth-credentials.js"; +import type { PreparedAgentCredentialModes } from "./agent-auth-credential-modes.js"; import { hasUsableOAuthCredential } from "./auth-profiles/credential-state.js"; import { resolveExternalCliAuthProfiles } from "./auth-profiles/external-cli-sync.js"; import { diff --git a/src/agents/openclaw-tools.browser-plugin.integration.test.ts b/src/agents/openclaw-tools.browser-plugin.integration.test.ts index 197e9a1e1d7d..0d0353b61000 100644 --- a/src/agents/openclaw-tools.browser-plugin.integration.test.ts +++ b/src/agents/openclaw-tools.browser-plugin.integration.test.ts @@ -202,6 +202,7 @@ describe("createOpenClawTools browser plugin integration", () => { workspaceDir: "/tmp", activeProjectKeys: [], config, + authStore: { version: 1, profiles: {} }, authModes: {}, metadataSnapshot, pluginRegistry, diff --git a/src/agents/prepared-model-catalog.types.ts b/src/agents/prepared-model-catalog.types.ts index a26a14e9b55d..a20be8080cbf 100644 --- a/src/agents/prepared-model-catalog.types.ts +++ b/src/agents/prepared-model-catalog.types.ts @@ -1,6 +1,6 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js"; -import type { PreparedAgentCredentialModes } from "./agent-auth-credentials.js"; +import type { PreparedAgentCredentialModes } from "./agent-auth-credential-modes.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; import type { ModelCatalogSnapshot } from "./model-catalog.types.js"; diff --git a/src/agents/prepared-model-runtime.types.ts b/src/agents/prepared-model-runtime.types.ts index 3321435cb730..47ccd83c4295 100644 --- a/src/agents/prepared-model-runtime.types.ts +++ b/src/agents/prepared-model-runtime.types.ts @@ -5,7 +5,7 @@ import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot import type { PreparedProviderStaticCatalog } from "../plugins/provider-discovery.js"; import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js"; import type { PluginRegistry } from "../plugins/registry-types.js"; -import type { PreparedAgentCredentialModes } from "./agent-auth-credentials.js"; +import type { PreparedAgentCredentialModes } from "./agent-auth-credential-modes.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; import type { InlineModelEntry } from "./embedded-agent-runner/model.inline-provider.js"; import type { AgentHarnessPluginSelection } from "./harness/runtime-plugin-load-plan.js"; diff --git a/src/gateway/local-request-context.test.ts b/src/gateway/local-request-context.test.ts index 5562419dc9f9..305505a05e1c 100644 --- a/src/gateway/local-request-context.test.ts +++ b/src/gateway/local-request-context.test.ts @@ -60,6 +60,9 @@ describe("local gateway request context", () => { agentDir: "/tmp/local-model-catalog-agent", workspaceDir: "/tmp/local-model-catalog-workspace", config: cfg, + authModes: {}, + authStore: { version: 1, profiles: {} }, + metadataSnapshot: { index: { plugins: [] }, plugins: [] } as never, modelCatalog: { entries: [], routeVariants: [] }, }); diff --git a/src/gateway/server-methods/chat-metadata-runtime.test.ts b/src/gateway/server-methods/chat-metadata-runtime.test.ts index f7b4cca28e10..848c56363c7b 100644 --- a/src/gateway/server-methods/chat-metadata-runtime.test.ts +++ b/src/gateway/server-methods/chat-metadata-runtime.test.ts @@ -21,12 +21,22 @@ function createOwner( api?: ModelCatalogEntry["api"], ): PreparedModelRuntimeSnapshot { const model = { id, name: id, provider, ...(api ? { api } : {}) }; + const authStore: AuthProfileStore = { + version: 1, + profiles: Object.fromEntries( + Object.entries(credentials).map(([credentialProvider, credential]) => [ + `${credentialProvider}:prepared`, + { ...credential, provider: credentialProvider }, + ]), + ), + }; return { agentId: "main", agentDir: `/tmp/${id}/agent`, workspaceDir: `/tmp/${id}/workspace`, activeProjectKeys: [], config, + authStore, authModes: resolveUsableAgentCredentialModes(credentials), metadataSnapshot: { index: { plugins: [] }, plugins: [] } as never, allowGatewaySubagentBinding: false, diff --git a/src/gateway/server-methods/models-list-auth-resolver.ts b/src/gateway/server-methods/models-list-auth-resolver.ts index f6af1230051c..cbdea1ec71a1 100644 --- a/src/gateway/server-methods/models-list-auth-resolver.ts +++ b/src/gateway/server-methods/models-list-auth-resolver.ts @@ -1,4 +1,4 @@ -import type { PreparedAgentCredentialModes } from "../../agents/agent-auth-credentials.js"; +import type { PreparedAgentCredentialModes } from "../../agents/agent-auth-credential-modes.js"; import { resolveAgentDir } from "../../agents/agent-scope.js"; import type { RuntimeAuthMaterialization } from "../../agents/auth-profiles/runtime-materializations.js"; import type { AuthProfileStore } from "../../agents/auth-profiles/types.js"; diff --git a/src/gateway/server-methods/models-list-result.ts b/src/gateway/server-methods/models-list-result.ts index 9914259e6624..83a0e348010a 100644 --- a/src/gateway/server-methods/models-list-result.ts +++ b/src/gateway/server-methods/models-list-result.ts @@ -3,7 +3,7 @@ import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; import { asPositiveSafeInteger as resolvePositiveSafeInteger } from "@openclaw/normalization-core/number-coercion"; import type { ModelChoice } from "../../../packages/gateway-protocol/src/schema/agents-models-skills.js"; -import type { PreparedAgentCredentialModes } from "../../agents/agent-auth-credentials.js"; +import type { PreparedAgentCredentialModes } from "../../agents/agent-auth-credential-modes.js"; import { resolveAgentEffectiveModelPrimary, resolveAgentWorkspaceDir, diff --git a/src/gateway/server-model-catalog.test.ts b/src/gateway/server-model-catalog.test.ts index b9a64c67954d..3bfba31f44da 100644 --- a/src/gateway/server-model-catalog.test.ts +++ b/src/gateway/server-model-catalog.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import type { ModelCatalogSnapshot } from "../agents/model-catalog.types.js"; +import type { PublishedModelCatalogOwnerCandidate } from "../agents/prepared-model-catalog.types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { loadGatewayModelCatalog, @@ -33,11 +34,14 @@ function ownerSnapshot( config: OpenClawConfig, modelCatalog: ModelCatalogSnapshot = snapshot, agentId?: string, -) { +): PublishedModelCatalogOwnerCandidate { return { ...(agentId ? { agentId } : {}), agentDir: "/tmp/gateway-agent", config, + authModes: {}, + authStore: { version: 1, profiles: {} }, + metadataSnapshot: { index: { plugins: [] }, plugins: [] } as never, modelCatalog, }; } diff --git a/src/gateway/server.chat.gateway-server-chat-b.test.ts b/src/gateway/server.chat.gateway-server-chat-b.test.ts index 427ca56b881b..dc8aa06b94ec 100644 --- a/src/gateway/server.chat.gateway-server-chat-b.test.ts +++ b/src/gateway/server.chat.gateway-server-chat-b.test.ts @@ -1296,6 +1296,7 @@ describe("gateway server chat", () => { compat: { supportedReasoningEfforts: ["low"] }, params: { apiKey: "private-route-token" }, }; + const pluginMetadata = resolvePluginMetadataSnapshot({ config, env: process.env }); const catalogSnapshot = { entries: [subscriptionRoute], routeVariants: [subscriptionRoute, platformRoute], @@ -1317,6 +1318,13 @@ describe("gateway server chat", () => { }), ], ]); + const requirePreparedAuthStore = (agentId: string) => { + const authStore = preparedAuthStoreByAgentId.get(agentId); + if (!authStore) { + throw new Error(`expected prepared auth store for agent "${agentId}"`); + } + return authStore; + }; const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; const { buildModelsListResult, createGatewayAgentModelCatalogProjector } = await import("./server-methods/models-list-result.js"); @@ -1351,7 +1359,8 @@ describe("gateway server chat", () => { cfg: config, agentId, snapshot: catalogSnapshot, - preparedAuthStore: preparedAuthStoreByAgentId.get(agentId), + metadataSnapshot: pluginMetadata, + preparedAuthStore: requirePreparedAuthStore(agentId), ...(profileId ? { preferredProfileId: profileId } : {}), ...(profileId && (profileSource === "user" || legacyUserProfile) ? { lockedProfileId: profileId } @@ -1412,7 +1421,6 @@ describe("gateway server chat", () => { const persistedConfig = getRuntimeConfig(); // Direct handlers bypass Gateway startup, so publish its process-lifecycle handoff once. // Otherwise every route projector rediscovers the full plugin metadata graph. - const pluginMetadata = resolvePluginMetadataSnapshot({ config, env: process.env }); releasePluginMetadata = installTemporaryCurrentPluginMetadataSnapshot(pluginMetadata, { config, compatibleConfigs: [persistedConfig], @@ -1427,6 +1435,8 @@ describe("gateway server chat", () => { cfg: persistedConfig, agentId: "work", snapshot: catalogSnapshot, + metadataSnapshot: pluginMetadata, + preparedAuthStore: requirePreparedAuthStore("work"), preferredProfileId: "openai:expired", }).evaluateEntry(subscriptionRoute, catalogSnapshot.routeVariants); expect(expiredPreferenceEvaluation).toMatchObject({ diff --git a/src/gateway/worker-environments/inference-runtime.test.ts b/src/gateway/worker-environments/inference-runtime.test.ts index 1583963a3d99..c6dadc58bda2 100644 --- a/src/gateway/worker-environments/inference-runtime.test.ts +++ b/src/gateway/worker-environments/inference-runtime.test.ts @@ -186,6 +186,7 @@ function setup(entry: SessionEntry = sessionEntry) { allowGatewaySubagentBinding: true, workspaceDir: WORKSPACE, config, + authStore: { version: 1, profiles: {} }, authModes: {}, metadataSnapshot: { plugins: [] } as never, modelCatalog: {