fix(plugins): expose model auth API to context-engine plugins (#41090)
Merged via squash. Prepared head SHA: ee96e96bb984cc3e1e152d17199357a8f6db312d 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:
@@ -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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -253,6 +253,11 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
|
||||
state: {
|
||||
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: {
|
||||
run: vi.fn(),
|
||||
waitForRun: vi.fn(),
|
||||
|
||||
@@ -801,5 +801,11 @@ export type {
|
||||
export { registerContextEngine } 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
|
||||
export { redactSensitiveText } from "../logging/redact.js";
|
||||
|
||||
@@ -53,4 +53,21 @@ describe("plugin runtime command execution", () => {
|
||||
const runtime = createPluginRuntime();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { createRequire } from "node:module";
|
||||
import {
|
||||
getApiKeyForModel as getApiKeyForModelRaw,
|
||||
resolveApiKeyForProvider as resolveApiKeyForProviderRaw,
|
||||
} from "../../agents/model-auth.js";
|
||||
import { resolveStateDir } from "../../config/paths.js";
|
||||
import { transcribeAudioFile } from "../../media-understanding/transcribe-audio.js";
|
||||
import { textToSpeechTelephony } from "../../tts/tts.js";
|
||||
@@ -59,6 +63,24 @@ export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}):
|
||||
events: createRuntimeEvents(),
|
||||
logging: createRuntimeLogging(),
|
||||
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;
|
||||
|
||||
return runtime;
|
||||
|
||||
@@ -52,4 +52,16 @@ export type PluginRuntimeCore = {
|
||||
state: {
|
||||
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>;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user