mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(models): keep prepared auth state private
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -10,7 +10,7 @@ export type PublishedModelCatalogOwnerCandidate = Readonly<{
|
||||
workspaceDir?: string;
|
||||
config: OpenClawConfig;
|
||||
authModes: PreparedAgentCredentialModes;
|
||||
authStore: AuthProfileStore;
|
||||
authStore?: AuthProfileStore;
|
||||
metadataSnapshot: PluginMetadataSnapshot;
|
||||
modelCatalog: ModelCatalogSnapshot;
|
||||
}>;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,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,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: {
|
||||
|
||||
Reference in New Issue
Block a user