mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user