fix(guardian): use runtime.modelAuth instead of runtime.models

Align with main's PluginRuntime interface: use `modelAuth` (not `models`)
for API key resolution. Remove dependency on `resolveProviderInfo` (not
available on main) — provider info is now resolved from config at
registration time via `resolveModelFromConfig`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ShengtongZhu 2026-03-15 14:06:59 +08:00
parent 400787110c
commit 9fbbc97e9a
3 changed files with 35 additions and 87 deletions

View File

@ -512,8 +512,7 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
function makeMockApi(
overrides: {
pluginConfig?: Record<string, unknown>;
resolveApiKeyForProvider?: PluginRuntime["models"]["resolveApiKeyForProvider"];
resolveProviderInfo?: PluginRuntime["models"]["resolveProviderInfo"];
resolveApiKeyForProvider?: PluginRuntime["modelAuth"]["resolveApiKeyForProvider"];
openclawConfig?: Record<string, unknown>;
} = {},
) {
@ -526,12 +525,6 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
source: "mock",
mode: "api-key",
});
const mockResolveProvider =
overrides.resolveProviderInfo ??
vi.fn().mockResolvedValue({
baseUrl: "https://api.anthropic.com",
api: "anthropic-messages",
});
const api: OpenClawPluginApi = {
id: "guardian",
@ -545,6 +538,15 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
},
},
},
models: {
providers: {
anthropic: {
baseUrl: "https://api.anthropic.com",
api: "anthropic-messages",
models: [],
},
},
},
}) as OpenClawPluginApi["config"],
pluginConfig: {
model: "anthropic/claude-haiku-4-5",
@ -553,9 +555,8 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
...overrides.pluginConfig,
},
runtime: {
models: {
modelAuth: {
resolveApiKeyForProvider: mockResolveAuth,
resolveProviderInfo: mockResolveProvider,
},
} as unknown as PluginRuntime,
logger: {
@ -582,7 +583,7 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
resolvePath: vi.fn((s: string) => s),
};
return { api, hooks, mockResolveAuth, mockResolveProvider };
return { api, hooks, mockResolveAuth };
}
beforeEach(() => {
@ -595,21 +596,16 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
vi.restoreAllMocks();
});
it("resolves provider info + API key from SDK on first before_tool_call", async () => {
it("resolves API key from SDK on first before_tool_call", async () => {
const mockResolveAuth = vi.fn().mockResolvedValue({
apiKey: "sk-from-auth-profiles",
profileId: "anthropic:default",
source: "profile:anthropic:default",
mode: "oauth",
});
const mockResolveProvider = vi.fn().mockResolvedValue({
baseUrl: "https://api.anthropic.com",
api: "anthropic-messages",
});
const { api, hooks } = makeMockApi({
resolveApiKeyForProvider: mockResolveAuth,
resolveProviderInfo: mockResolveProvider,
});
guardianPlugin.register(api);
@ -626,17 +622,12 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
{ sessionKey: "s1", toolName: "exec" },
);
// Provider info should be resolved
expect(mockResolveProvider).toHaveBeenCalledWith(
expect.objectContaining({ provider: "anthropic" }),
);
// Auth should be resolved
expect(mockResolveAuth).toHaveBeenCalledWith(
expect.objectContaining({ provider: "anthropic" }),
);
// callGuardian should receive both baseUrl and apiKey
// callGuardian should receive baseUrl from config and apiKey from auth
expect(callGuardian).toHaveBeenCalledWith(
expect.objectContaining({
model: expect.objectContaining({
@ -648,13 +639,11 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
);
});
it("skips SDK resolution when explicit config already provides baseUrl + apiKey", async () => {
it("skips auth resolution when explicit config already provides apiKey", async () => {
const mockResolveAuth = vi.fn();
const mockResolveProvider = vi.fn();
const { api, hooks } = makeMockApi({
resolveApiKeyForProvider: mockResolveAuth,
resolveProviderInfo: mockResolveProvider,
openclawConfig: {
agents: { defaults: { model: { primary: "myapi/model-x" } } },
models: {
@ -682,9 +671,7 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
{ sessionKey: "s1", toolName: "exec" },
);
// Should NOT call resolveProviderInfo or resolveApiKeyForProvider
// since config provides both baseUrl and apiKey
expect(mockResolveProvider).not.toHaveBeenCalled();
// Should NOT call resolveApiKeyForProvider since config provides apiKey
expect(mockResolveAuth).not.toHaveBeenCalled();
expect(callGuardian).toHaveBeenCalledWith(
@ -697,20 +684,15 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
);
});
it("only resolves once across multiple before_tool_call invocations", async () => {
it("only resolves auth once across multiple before_tool_call invocations", async () => {
const mockResolveAuth = vi.fn().mockResolvedValue({
apiKey: "sk-resolved-once",
source: "profile:anthropic:default",
mode: "api-key",
});
const mockResolveProvider = vi.fn().mockResolvedValue({
baseUrl: "https://api.anthropic.com",
api: "anthropic-messages",
});
const { api, hooks } = makeMockApi({
resolveApiKeyForProvider: mockResolveAuth,
resolveProviderInfo: mockResolveProvider,
});
guardianPlugin.register(api);
@ -726,16 +708,12 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
decisionCache.clear();
await handler({ toolName: "exec", params: {} }, { sessionKey: "s1", toolName: "exec" });
// Each SDK function should be called only once
expect(mockResolveProvider).toHaveBeenCalledTimes(1);
// Auth should be called only once
expect(mockResolveAuth).toHaveBeenCalledTimes(1);
});
it("handles provider resolution failure — falls back per config", async () => {
const mockResolveProvider = vi.fn().mockResolvedValue(undefined); // provider not found
it("handles missing baseUrl — falls back per config", async () => {
const { api, hooks } = makeMockApi({
resolveProviderInfo: mockResolveProvider,
pluginConfig: {
model: "unknown/model",
fallback_on_error: "allow",
@ -753,27 +731,20 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
{ sessionKey: "s1", toolName: "exec" },
);
// Should not call callGuardian since provider couldn't be resolved
// Should not call callGuardian since provider has no baseUrl
expect(callGuardian).not.toHaveBeenCalled();
// With fallback_on_error: "allow", should return undefined (allow)
expect(result).toBeUndefined();
expect(api.logger.warn).toHaveBeenCalledWith(
expect.stringContaining("Provider resolution failed"),
);
expect(api.logger.warn).toHaveBeenCalledWith(expect.stringContaining("not fully resolved"));
});
it("handles auth resolution failure gracefully — still calls guardian", async () => {
const mockResolveAuth = vi.fn().mockRejectedValue(new Error("No API key found"));
const mockResolveProvider = vi.fn().mockResolvedValue({
baseUrl: "https://api.anthropic.com",
api: "anthropic-messages",
});
const { api, hooks } = makeMockApi({
resolveApiKeyForProvider: mockResolveAuth,
resolveProviderInfo: mockResolveProvider,
});
guardianPlugin.register(api);
@ -790,7 +761,7 @@ describe("guardian index — lazy provider + auth resolution via SDK", () => {
{ sessionKey: "s1", toolName: "exec" },
);
// Provider resolved, but auth failed — should still call callGuardian
// baseUrl resolved from config, but auth failed — should still call callGuardian
expect(callGuardian).toHaveBeenCalled();
expect(api.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Auth resolution failed"));

View File

@ -102,43 +102,20 @@ const guardianPlugin = {
if (resolutionAttempted) return !!resolvedModel.baseUrl;
resolutionAttempted = true;
// --- Resolve provider info (baseUrl, api type) via SDK ---
// --- Resolve provider info (baseUrl, api type) from config ---
if (!resolvedModel.baseUrl) {
try {
const info = await runtime.models.resolveProviderInfo({
provider: resolvedModel.provider,
cfg: openclawConfig,
});
if (info) {
resolvedModel.baseUrl = info.baseUrl;
resolvedModel.api = info.api;
if (info.headers) {
resolvedModel.headers = { ...info.headers, ...resolvedModel.headers };
}
api.logger.info(
`[guardian] Provider resolved via SDK: provider=${resolvedModel.provider}, ` +
`baseUrl=${info.baseUrl}, api=${info.api}`,
);
} else {
api.logger.warn(
`[guardian] Provider resolution failed: provider=${resolvedModel.provider} ` +
`not found in config or models.json. Guardian will not function.`,
);
return false;
}
} catch (err) {
api.logger.warn(
`[guardian] Provider resolution error for ${resolvedModel.provider}: ` +
`${err instanceof Error ? err.message : String(err)}`,
);
return false;
}
api.logger.warn(
`[guardian] Provider not fully resolved: provider=${resolvedModel.provider} ` +
`has no baseUrl. Configure models.providers.${resolvedModel.provider}.baseUrl ` +
`in openclaw.json. Guardian will not function.`,
);
return false;
}
// --- Resolve API key via SDK ---
if (!resolvedModel.apiKey) {
try {
const auth = await runtime.models.resolveApiKeyForProvider({
const auth = await runtime.modelAuth.resolveApiKeyForProvider({
provider: resolvedModel.provider,
cfg: openclawConfig,
});

View File

@ -253,13 +253,13 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
state: {
resolveStateDir: vi.fn(() => "/tmp/openclaw"),
},
models: {
modelAuth: {
getApiKeyForModel: vi.fn(
() => undefined,
) as unknown as PluginRuntime["modelAuth"]["getApiKeyForModel"],
resolveApiKeyForProvider: vi.fn(
() => undefined,
) as unknown as PluginRuntime["models"]["resolveApiKeyForProvider"],
resolveProviderInfo: vi.fn(
() => undefined,
) as unknown as PluginRuntime["models"]["resolveProviderInfo"],
) as unknown as PluginRuntime["modelAuth"]["resolveApiKeyForProvider"],
},
subagent: {
run: vi.fn(),