diff --git a/src/agents/embedded-agent-runner/model.test.ts b/src/agents/embedded-agent-runner/model.test.ts index c07bbfc45ffe..b990bf7d5d83 100644 --- a/src/agents/embedded-agent-runner/model.test.ts +++ b/src/agents/embedded-agent-runner/model.test.ts @@ -972,7 +972,6 @@ describe("resolveModel", () => { activeProjectKeys: [], allowGatewaySubagentBinding: false, config: cfg, - authStore: { version: 1, profiles: {} }, authModes: {}, metadataSnapshot: { plugins: [] } as never, modelCatalog: { entries: [], routeVariants: [] }, diff --git a/src/agents/openclaw-tools.browser-plugin.integration.test.ts b/src/agents/openclaw-tools.browser-plugin.integration.test.ts index 0d0353b61000..197e9a1e1d7d 100644 --- a/src/agents/openclaw-tools.browser-plugin.integration.test.ts +++ b/src/agents/openclaw-tools.browser-plugin.integration.test.ts @@ -202,7 +202,6 @@ describe("createOpenClawTools browser plugin integration", () => { workspaceDir: "/tmp", activeProjectKeys: [], config, - authStore: { version: 1, profiles: {} }, authModes: {}, metadataSnapshot, pluginRegistry, diff --git a/src/agents/prepared-model-catalog-owner.ts b/src/agents/prepared-model-catalog-owner.ts index 4500e11abf81..283dc9991eaf 100644 --- a/src/agents/prepared-model-catalog-owner.ts +++ b/src/agents/prepared-model-catalog-owner.ts @@ -4,6 +4,7 @@ import type { PublishedModelCatalogOwnerCandidate, ResolvedPublishedModelCatalogOwner, } from "./prepared-model-catalog.types.js"; +import { getPreparedModelRuntimeAuthStore } from "./prepared-model-runtime-auth.js"; class PublishedModelCatalogOwnerResolutionError extends Error { constructor(message: string) { @@ -37,13 +38,19 @@ export function resolvePublishedModelCatalogOwner( `published model catalog owner did not identify a workspace (${agentId})`, ); } + const authStore = snapshot.authStore ?? getPreparedModelRuntimeAuthStore(snapshot); + if (!authStore) { + throw new PublishedModelCatalogOwnerResolutionError( + `published model catalog owner is missing prepared auth state (${agentId})`, + ); + } return Object.freeze({ agentId, agentDir: snapshot.agentDir, workspaceDir, config: snapshot.config, authModes: snapshot.authModes, - authStore: snapshot.authStore, + authStore, metadataSnapshot: snapshot.metadataSnapshot, modelCatalog: snapshot.modelCatalog, }); diff --git a/src/agents/prepared-model-catalog.test.ts b/src/agents/prepared-model-catalog.test.ts index ef8111ff3826..f4d6c385642a 100644 --- a/src/agents/prepared-model-catalog.test.ts +++ b/src/agents/prepared-model-catalog.test.ts @@ -61,15 +61,23 @@ import { PreparedModelCatalogConfigReplacedError } from "./prepared-model-catalo import { getPublishedPreparedModelCatalogOwnerSnapshot, getPreparedModelCatalogSnapshot, + loadPreparedModelCatalogOwnerSnapshot, loadPreparedModelCatalogSnapshot, loadResolvedPublishedModelCatalogOwner, loadPublishedPreparedModelCatalog, loadPublishedPreparedModelCatalogOwnerSnapshot, } from "./prepared-model-catalog.js"; +import { + getPreparedModelRuntimeAuthStore, + setPreparedModelRuntimeAuthStore, +} from "./prepared-model-runtime-auth.js"; import { PreparedModelRuntimeOwnerNotPublishedError } from "./prepared-model-runtime.js"; const fullSnapshot = { config: mocks.config, + authModes: {}, + authStore: { version: 1, profiles: {} }, + metadataSnapshot: { index: { plugins: [] }, plugins: [] }, modelCatalog: { entries: [{ provider: "test", id: "full", name: "Full" }], routeVariants: [] }, }; const readOnlySnapshot = { @@ -191,11 +199,13 @@ describe("prepared model catalog access", () => { routeVariants: [], }; const loadFullModelCatalog = vi.fn(async () => discoveredCatalog); + const { authStore, ...snapshotFacts } = fullSnapshot; const snapshot = { - ...fullSnapshot, + ...snapshotFacts, modelCatalog: configuredCatalog, loadFullModelCatalog, }; + setPreparedModelRuntimeAuthStore(snapshot, authStore); mocks.prepareSnapshot.mockResolvedValue(snapshot); await expect(loadPreparedModelCatalogSnapshot({ readOnly: true })).resolves.toBe( @@ -203,9 +213,10 @@ describe("prepared model catalog access", () => { ); expect(loadFullModelCatalog).not.toHaveBeenCalled(); - await expect(loadPreparedModelCatalogSnapshot({ readOnly: false })).resolves.toBe( - discoveredCatalog, - ); + const materialized = await loadPreparedModelCatalogOwnerSnapshot({ readOnly: false }); + expect(materialized.modelCatalog).toBe(discoveredCatalog); + expect(materialized).not.toHaveProperty("authStore"); + expect(getPreparedModelRuntimeAuthStore(materialized)).toBe(authStore); expect(loadFullModelCatalog).toHaveBeenCalledOnce(); mocks.getSnapshot.mockReturnValue(snapshot); @@ -292,6 +303,9 @@ describe("prepared model catalog access", () => { agentDir: "/tmp/prepared-model-catalog-agent", workspaceDir: "/tmp/prepared-model-catalog-workspace", config: committedSnapshot.config, + authModes: {}, + authStore: { version: 1, profiles: {} }, + metadataSnapshot: fullSnapshot.metadataSnapshot, modelCatalog: committedSnapshot.modelCatalog, }); }); diff --git a/src/agents/prepared-model-catalog.ts b/src/agents/prepared-model-catalog.ts index 02c296050419..bd1479561cf2 100644 --- a/src/agents/prepared-model-catalog.ts +++ b/src/agents/prepared-model-catalog.ts @@ -13,6 +13,7 @@ import type { ModelCatalogEntry, ModelCatalogSnapshot } from "./model-catalog.ty import { resolvePublishedModelCatalogOwner } from "./prepared-model-catalog-owner.js"; import { PreparedModelCatalogConfigReplacedError } from "./prepared-model-catalog.errors.js"; import type { ResolvedPublishedModelCatalogOwner } from "./prepared-model-catalog.types.js"; +import { copyPreparedModelRuntimeAuthState } from "./prepared-model-runtime-auth.js"; import { isPreparedModelCatalogFull } from "./prepared-model-runtime.facts.js"; import { acquireAgentRunPreparedModelRuntime, @@ -62,9 +63,12 @@ async function materializeRequestedModelCatalog( return snapshot; } const modelCatalog = await snapshot.loadFullModelCatalog(); - return modelCatalog === snapshot.modelCatalog - ? snapshot - : Object.freeze({ ...snapshot, modelCatalog }); + if (modelCatalog === snapshot.modelCatalog) { + return snapshot; + } + const materialized = Object.freeze({ ...snapshot, modelCatalog }); + copyPreparedModelRuntimeAuthState(snapshot, materialized); + return materialized; } function acceptsPreparedSnapshotConfig( diff --git a/src/agents/prepared-model-catalog.types.ts b/src/agents/prepared-model-catalog.types.ts index a20be8080cbf..210e3d59a64d 100644 --- a/src/agents/prepared-model-catalog.types.ts +++ b/src/agents/prepared-model-catalog.types.ts @@ -10,7 +10,7 @@ export type PublishedModelCatalogOwnerCandidate = Readonly<{ workspaceDir?: string; config: OpenClawConfig; authModes: PreparedAgentCredentialModes; - authStore: AuthProfileStore; + authStore?: AuthProfileStore; metadataSnapshot: PluginMetadataSnapshot; modelCatalog: ModelCatalogSnapshot; }>; diff --git a/src/agents/prepared-model-runtime-auth.ts b/src/agents/prepared-model-runtime-auth.ts index e3cc0995f360..f94c12da0c04 100644 --- a/src/agents/prepared-model-runtime-auth.ts +++ b/src/agents/prepared-model-runtime-auth.ts @@ -1,21 +1,39 @@ -/** Secret-free successful-auth facts owned by an immutable prepared model generation. */ import type { RuntimeAuthMaterialization } from "./auth-profiles/runtime-materializations.js"; -import type { PreparedModelRuntimeSnapshot } from "./prepared-model-runtime.types.js"; +import type { AuthProfileStore } from "./auth-profiles/types.js"; -const materializationsBySnapshot = new WeakMap< - PreparedModelRuntimeSnapshot, - readonly RuntimeAuthMaterialization[] ->(); +/** Private auth facts owned by an immutable prepared model generation. */ +const authStoreBySnapshot = new WeakMap(); +const materializationsBySnapshot = new WeakMap(); + +// Secret-bearing state stays lifecycle-owned without becoming part of the public snapshot shape. +export function setPreparedModelRuntimeAuthStore( + snapshot: object, + authStore: AuthProfileStore, +): void { + authStoreBySnapshot.set(snapshot, authStore); +} + +export function getPreparedModelRuntimeAuthStore(snapshot: object): AuthProfileStore | undefined { + return authStoreBySnapshot.get(snapshot); +} export function setPreparedModelRuntimeAuthMaterializations( - snapshot: PreparedModelRuntimeSnapshot, + snapshot: object, materializations: readonly RuntimeAuthMaterialization[], ): void { materializationsBySnapshot.set(snapshot, materializations); } export function getPreparedModelRuntimeAuthMaterializations( - snapshot: PreparedModelRuntimeSnapshot, + snapshot: object, ): readonly RuntimeAuthMaterialization[] { return materializationsBySnapshot.get(snapshot) ?? []; } + +export function copyPreparedModelRuntimeAuthState(source: object, target: object): void { + const authStore = authStoreBySnapshot.get(source); + if (authStore) { + authStoreBySnapshot.set(target, authStore); + } + materializationsBySnapshot.set(target, getPreparedModelRuntimeAuthMaterializations(source)); +} diff --git a/src/agents/prepared-model-runtime.build.ts b/src/agents/prepared-model-runtime.build.ts index e4b1a712bbd2..905f1772754e 100644 --- a/src/agents/prepared-model-runtime.build.ts +++ b/src/agents/prepared-model-runtime.build.ts @@ -10,7 +10,10 @@ import { createPreparedModelCatalogWorkerInput, runPreparedModelCatalogWorker, } from "./prepared-model-catalog-worker.js"; -import { setPreparedModelRuntimeAuthMaterializations } from "./prepared-model-runtime-auth.js"; +import { + setPreparedModelRuntimeAuthMaterializations, + setPreparedModelRuntimeAuthStore, +} from "./prepared-model-runtime-auth.js"; import { PreparedModelRuntimePublicationSupersededError } from "./prepared-model-runtime.errors.js"; import { fingerprintPreparedRuntimeFacts, @@ -180,7 +183,6 @@ function createSnapshot( ...(input.inheritedAuthDir ? { inheritedAuthDir: input.inheritedAuthDir } : {}), ...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}), config: input.config, - authStore: agentFacts.authStore, authModes: resolveUsableAgentCredentialModes(credentials), metadataSnapshot: pluginMetadataSnapshot, allowGatewaySubagentBinding: input.allowGatewaySubagentBinding === true, @@ -193,6 +195,7 @@ function createSnapshot( inlineProviderModels, createStores, }); + setPreparedModelRuntimeAuthStore(snapshot, agentFacts.authStore); setPreparedModelRuntimeAuthMaterializations( snapshot, Object.freeze([...getPreparedRuntimeAuthMaterializations(input.agentDir)]), diff --git a/src/agents/prepared-model-runtime.test.ts b/src/agents/prepared-model-runtime.test.ts index 58b60aed565c..e0ce20896871 100644 --- a/src/agents/prepared-model-runtime.test.ts +++ b/src/agents/prepared-model-runtime.test.ts @@ -2,6 +2,7 @@ import "./prepared-model-runtime.test-harness.js"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { requireActivePluginRegistry } from "../plugins/runtime.js"; +import { getPreparedModelRuntimeAuthStore } from "./prepared-model-runtime-auth.js"; import { startSerializedSnapshotBuild } from "./prepared-model-runtime.build.js"; import { acquireReadOnlyPreparedModelRuntime, @@ -558,7 +559,7 @@ describe("prepared model runtime snapshots", () => { expect(credentialFree).not.toBe(await prepareModelRuntimeSnapshot({ config, agentDir })); expect(mocks.discoverAuthStorage).toHaveBeenCalledOnce(); - expect(credentialFree.authStore).toEqual({ version: 1, profiles: {} }); + expect(getPreparedModelRuntimeAuthStore(credentialFree)).toEqual({ version: 1, profiles: {} }); }); it("reuses one lifecycle-owned snapshot without rediscovering files", async () => { diff --git a/src/agents/prepared-model-runtime.types.ts b/src/agents/prepared-model-runtime.types.ts index 47ccd83c4295..3ee582ce6e6d 100644 --- a/src/agents/prepared-model-runtime.types.ts +++ b/src/agents/prepared-model-runtime.types.ts @@ -6,7 +6,6 @@ import type { PreparedProviderStaticCatalog } from "../plugins/provider-discover import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js"; import type { PluginRegistry } from "../plugins/registry-types.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"; import type { ModelCatalogEntry, ModelCatalogSnapshot } from "./model-catalog.types.js"; @@ -41,8 +40,6 @@ export type PreparedModelRuntimeSnapshot = Readonly<{ /** Session active project set, ordered most-recent first; empty before run binding. */ activeProjectKeys: readonly string[]; config: OpenClawConfig; - /** Effective materialized auth store captured by this exact lifecycle generation. */ - authStore: AuthProfileStore; /** Secret-free usable auth modes captured by this exact lifecycle generation. */ authModes: PreparedAgentCredentialModes; metadataSnapshot: PluginMetadataSnapshot; diff --git a/src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts b/src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts index 4e090d2fe7d3..1ec91e37fd6a 100644 --- a/src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts +++ b/src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts @@ -81,7 +81,6 @@ function createEmptyPreparedModelRuntimeSnapshot( ...(input.workspaceDir !== undefined ? { workspaceDir: input.workspaceDir } : {}), activeProjectKeys: [], config: input.config, - authStore: { version: 1, profiles: {} }, authModes: {}, metadataSnapshot: createEmptyPluginMetadataSnapshot(input.workspaceDir), pluginRegistry: createEmptyPluginRegistry(), diff --git a/src/gateway/server-methods/chat-metadata-runtime.test.ts b/src/gateway/server-methods/chat-metadata-runtime.test.ts index 848c56363c7b..101a1a8d77b7 100644 --- a/src/gateway/server-methods/chat-metadata-runtime.test.ts +++ b/src/gateway/server-methods/chat-metadata-runtime.test.ts @@ -7,6 +7,7 @@ import { } from "../../agents/agent-auth-credentials.js"; import type { AuthProfileStore } from "../../agents/auth-profiles.js"; import type { ModelCatalogEntry } from "../../agents/model-catalog.types.js"; +import { setPreparedModelRuntimeAuthStore } from "../../agents/prepared-model-runtime-auth.js"; import type { PreparedModelRuntimeSnapshot } from "../../agents/prepared-model-runtime.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js"; @@ -30,13 +31,12 @@ function createOwner( ]), ), }; - return { + const owner: PreparedModelRuntimeSnapshot = { agentId: "main", agentDir: `/tmp/${id}/agent`, workspaceDir: `/tmp/${id}/workspace`, activeProjectKeys: [], config, - authStore, authModes: resolveUsableAgentCredentialModes(credentials), metadataSnapshot: { index: { plugins: [] }, plugins: [] } as never, allowGatewaySubagentBinding: false, @@ -51,6 +51,8 @@ function createOwner( modelRegistry: {} as never, }), }; + setPreparedModelRuntimeAuthStore(owner, authStore); + return owner; } function createHarness( diff --git a/src/gateway/server-model-catalog.ts b/src/gateway/server-model-catalog.ts index c20c636c81f1..29417944cac1 100644 --- a/src/gateway/server-model-catalog.ts +++ b/src/gateway/server-model-catalog.ts @@ -1,7 +1,6 @@ import { resolvePublishedModelCatalogOwner } from "../agents/prepared-model-catalog-owner.js"; import type { PublishedModelCatalogOwnerCandidate } from "../agents/prepared-model-catalog.types.js"; import { getPreparedModelRuntimeAuthMaterializations } from "../agents/prepared-model-runtime-auth.js"; -import type { PreparedModelRuntimeSnapshot } from "../agents/prepared-model-runtime.types.js"; // Gateway catalog reads use the atomic prepared runtime generation. import { getRuntimeConfig } from "../config/io.js"; import type { @@ -68,9 +67,7 @@ async function loadGatewayModelCatalogOwnerSnapshot( }); return { ...resolvePublishedModelCatalogOwner(candidate), - authMaterializations: getPreparedModelRuntimeAuthMaterializations( - candidate as PreparedModelRuntimeSnapshot, - ), + authMaterializations: getPreparedModelRuntimeAuthMaterializations(candidate), }; } diff --git a/src/gateway/worker-environments/inference-runtime.test.ts b/src/gateway/worker-environments/inference-runtime.test.ts index c6dadc58bda2..1583963a3d99 100644 --- a/src/gateway/worker-environments/inference-runtime.test.ts +++ b/src/gateway/worker-environments/inference-runtime.test.ts @@ -186,7 +186,6 @@ function setup(entry: SessionEntry = sessionEntry) { allowGatewaySubagentBinding: true, workspaceDir: WORKSPACE, config, - authStore: { version: 1, profiles: {} }, authModes: {}, metadataSnapshot: { plugins: [] } as never, modelCatalog: {