fix(plugins): expose model auth API to context-engine plugins (#41090)

Merged via squash.

Prepared head SHA: ee96e96bb9
Co-authored-by: xinhuagu <562450+xinhuagu@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
Xinhua Gu
2026-03-10 00:07:26 +01:00
committed by GitHub
parent c9a6c542ef
commit 4790e40ac6
6 changed files with 63 additions and 0 deletions

View File

@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
- Discord/reply chunking: resolve the effective `maxLinesPerMessage` config across live reply paths and preserve `chunkMode` in the fast send path so long Discord replies no longer split unexpectedly at the default 17-line limit. (#40133) thanks @rbutera. - Discord/reply chunking: resolve the effective `maxLinesPerMessage` config across live reply paths and preserve `chunkMode` in the fast send path so long Discord replies no longer split unexpectedly at the default 17-line limit. (#40133) thanks @rbutera.
- Logging/probe observations: suppress structured embedded and model-fallback probe warnings on the console without hiding error or fatal output. (#41338) thanks @altaywtf. - Logging/probe observations: suppress structured embedded and model-fallback probe warnings on the console without hiding error or fatal output. (#41338) thanks @altaywtf.
- Agents/fallback: treat HTTP 499 responses as transient in both raw-text and structured failover paths so Anthropic-style client-closed overload responses trigger model fallback reliably. (#41468) thanks @zeroasterisk. - Agents/fallback: treat HTTP 499 responses as transient in both raw-text and structured failover paths so Anthropic-style client-closed overload responses trigger model fallback reliably. (#41468) thanks @zeroasterisk.
- Plugins/context-engine model auth: expose `runtime.modelAuth` and plugin-sdk auth helpers so plugins can resolve provider/model API keys through the normal auth pipeline. (#41090) thanks @xinhuagu.
## 2026.3.8 ## 2026.3.8

View File

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

View File

@@ -801,5 +801,11 @@ export type {
export { registerContextEngine } from "../context-engine/registry.js"; export { registerContextEngine } from "../context-engine/registry.js";
export type { ContextEngineFactory } from "../context-engine/registry.js"; export type { ContextEngineFactory } from "../context-engine/registry.js";
// Model authentication types for plugins.
// Plugins should use runtime.modelAuth (which strips unsafe overrides like
// agentDir/store) rather than importing raw helpers directly.
export { requireApiKey } from "../agents/model-auth.js";
export type { ResolvedProviderAuth } from "../agents/model-auth.js";
// Security utilities // Security utilities
export { redactSensitiveText } from "../logging/redact.js"; export { redactSensitiveText } from "../logging/redact.js";

View File

@@ -53,4 +53,21 @@ describe("plugin runtime command execution", () => {
const runtime = createPluginRuntime(); const runtime = createPluginRuntime();
expect(runtime.system.requestHeartbeatNow).toBe(requestHeartbeatNow); expect(runtime.system.requestHeartbeatNow).toBe(requestHeartbeatNow);
}); });
it("exposes runtime.modelAuth with getApiKeyForModel and resolveApiKeyForProvider", () => {
const runtime = createPluginRuntime();
expect(runtime.modelAuth).toBeDefined();
expect(typeof runtime.modelAuth.getApiKeyForModel).toBe("function");
expect(typeof runtime.modelAuth.resolveApiKeyForProvider).toBe("function");
});
it("modelAuth wrappers strip agentDir and store to prevent credential steering", async () => {
// The wrappers should not forward agentDir or store from plugin callers.
// We verify this by checking the wrapper functions exist and are not the
// raw implementations (they are wrapped, not direct references).
const { getApiKeyForModel: rawGetApiKey } = await import("../../agents/model-auth.js");
const runtime = createPluginRuntime();
// Wrappers should NOT be the same reference as the raw functions
expect(runtime.modelAuth.getApiKeyForModel).not.toBe(rawGetApiKey);
});
}); });

View File

@@ -1,4 +1,8 @@
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import {
getApiKeyForModel as getApiKeyForModelRaw,
resolveApiKeyForProvider as resolveApiKeyForProviderRaw,
} from "../../agents/model-auth.js";
import { resolveStateDir } from "../../config/paths.js"; import { resolveStateDir } from "../../config/paths.js";
import { transcribeAudioFile } from "../../media-understanding/transcribe-audio.js"; import { transcribeAudioFile } from "../../media-understanding/transcribe-audio.js";
import { textToSpeechTelephony } from "../../tts/tts.js"; import { textToSpeechTelephony } from "../../tts/tts.js";
@@ -59,6 +63,24 @@ export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}):
events: createRuntimeEvents(), events: createRuntimeEvents(),
logging: createRuntimeLogging(), logging: createRuntimeLogging(),
state: { resolveStateDir }, state: { resolveStateDir },
modelAuth: {
// Wrap model-auth helpers so plugins cannot steer credential lookups:
// - agentDir / store: stripped (prevents reading other agents' stores)
// - profileId / preferredProfile: stripped (prevents cross-provider
// credential access via profile steering)
// Plugins only specify provider/model; the core auth pipeline picks
// the appropriate credential automatically.
getApiKeyForModel: (params) =>
getApiKeyForModelRaw({
model: params.model,
cfg: params.cfg,
}),
resolveApiKeyForProvider: (params) =>
resolveApiKeyForProviderRaw({
provider: params.provider,
cfg: params.cfg,
}),
},
} satisfies PluginRuntime; } satisfies PluginRuntime;
return runtime; return runtime;

View File

@@ -52,4 +52,16 @@ export type PluginRuntimeCore = {
state: { state: {
resolveStateDir: typeof import("../../config/paths.js").resolveStateDir; resolveStateDir: typeof import("../../config/paths.js").resolveStateDir;
}; };
modelAuth: {
/** Resolve auth for a model. Only provider/model and optional cfg are used. */
getApiKeyForModel: (params: {
model: import("@mariozechner/pi-ai").Model<import("@mariozechner/pi-ai").Api>;
cfg?: import("../../config/config.js").OpenClawConfig;
}) => Promise<import("../../agents/model-auth.js").ResolvedProviderAuth>;
/** Resolve auth for a provider by name. Only provider and optional cfg are used. */
resolveApiKeyForProvider: (params: {
provider: string;
cfg?: import("../../config/config.js").OpenClawConfig;
}) => Promise<import("../../agents/model-auth.js").ResolvedProviderAuth>;
};
}; };