fix(models): keep prepared auth state private

This commit is contained in:
joshavant
2026-08-12 04:05:37 -05:00
parent f8d208a0d5
commit b6813cd1be
14 changed files with 72 additions and 33 deletions
@@ -972,7 +972,6 @@ describe("resolveModel", () => {
activeProjectKeys: [],
allowGatewaySubagentBinding: false,
config: cfg,
authStore: { version: 1, profiles: {} },
authModes: {},
metadataSnapshot: { plugins: [] } as never,
modelCatalog: { entries: [], routeVariants: [] },
@@ -202,7 +202,6 @@ describe("createOpenClawTools browser plugin integration", () => {
workspaceDir: "/tmp",
activeProjectKeys: [],
config,
authStore: { version: 1, profiles: {} },
authModes: {},
metadataSnapshot,
pluginRegistry,
+8 -1
View File
@@ -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,
});
+18 -4
View File
@@ -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,
});
});
+7 -3
View File
@@ -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(
+1 -1
View File
@@ -10,7 +10,7 @@ export type PublishedModelCatalogOwnerCandidate = Readonly<{
workspaceDir?: string;
config: OpenClawConfig;
authModes: PreparedAgentCredentialModes;
authStore: AuthProfileStore;
authStore?: AuthProfileStore;
metadataSnapshot: PluginMetadataSnapshot;
modelCatalog: ModelCatalogSnapshot;
}>;
+26 -8
View File
@@ -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<object, AuthProfileStore>();
const materializationsBySnapshot = new WeakMap<object, readonly RuntimeAuthMaterialization[]>();
// 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));
}
+5 -2
View File
@@ -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)]),
+2 -1
View File
@@ -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 () => {
@@ -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;
@@ -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(),
@@ -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(
+1 -4
View File
@@ -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),
};
}
@@ -186,7 +186,6 @@ function setup(entry: SessionEntry = sessionEntry) {
allowGatewaySubagentBinding: true,
workspaceDir: WORKSPACE,
config,
authStore: { version: 1, profiles: {} },
authModes: {},
metadataSnapshot: { plugins: [] } as never,
modelCatalog: {