Skip to content

Commit 4790e40

Browse files
xinhuagujalehman
andauthored
fix(plugins): expose model auth API to context-engine plugins (openclaw#41090)
Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
1 parent c9a6c54 commit 4790e40

File tree

6 files changed

+63
-0
lines changed

6 files changed

+63
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
3939
- 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.
4040
- Logging/probe observations: suppress structured embedded and model-fallback probe warnings on the console without hiding error or fatal output. (#41338) thanks @altaywtf.
4141
- 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.
42+
- 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.
4243

4344
## 2026.3.8
4445

extensions/test-utils/plugin-runtime-mock.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,11 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
253253
state: {
254254
resolveStateDir: vi.fn(() => "/tmp/openclaw"),
255255
},
256+
modelAuth: {
257+
getApiKeyForModel: vi.fn() as unknown as PluginRuntime["modelAuth"]["getApiKeyForModel"],
258+
resolveApiKeyForProvider:
259+
vi.fn() as unknown as PluginRuntime["modelAuth"]["resolveApiKeyForProvider"],
260+
},
256261
subagent: {
257262
run: vi.fn(),
258263
waitForRun: vi.fn(),

src/plugin-sdk/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,5 +801,11 @@ export type {
801801
export { registerContextEngine } from "../context-engine/registry.js";
802802
export type { ContextEngineFactory } from "../context-engine/registry.js";
803803

804+
// Model authentication types for plugins.
805+
// Plugins should use runtime.modelAuth (which strips unsafe overrides like
806+
// agentDir/store) rather than importing raw helpers directly.
807+
export { requireApiKey } from "../agents/model-auth.js";
808+
export type { ResolvedProviderAuth } from "../agents/model-auth.js";
809+
804810
// Security utilities
805811
export { redactSensitiveText } from "../logging/redact.js";

src/plugins/runtime/index.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,21 @@ describe("plugin runtime command execution", () => {
5353
const runtime = createPluginRuntime();
5454
expect(runtime.system.requestHeartbeatNow).toBe(requestHeartbeatNow);
5555
});
56+
57+
it("exposes runtime.modelAuth with getApiKeyForModel and resolveApiKeyForProvider", () => {
58+
const runtime = createPluginRuntime();
59+
expect(runtime.modelAuth).toBeDefined();
60+
expect(typeof runtime.modelAuth.getApiKeyForModel).toBe("function");
61+
expect(typeof runtime.modelAuth.resolveApiKeyForProvider).toBe("function");
62+
});
63+
64+
it("modelAuth wrappers strip agentDir and store to prevent credential steering", async () => {
65+
// The wrappers should not forward agentDir or store from plugin callers.
66+
// We verify this by checking the wrapper functions exist and are not the
67+
// raw implementations (they are wrapped, not direct references).
68+
const { getApiKeyForModel: rawGetApiKey } = await import("../../agents/model-auth.js");
69+
const runtime = createPluginRuntime();
70+
// Wrappers should NOT be the same reference as the raw functions
71+
expect(runtime.modelAuth.getApiKeyForModel).not.toBe(rawGetApiKey);
72+
});
5673
});

src/plugins/runtime/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { createRequire } from "node:module";
2+
import {
3+
getApiKeyForModel as getApiKeyForModelRaw,
4+
resolveApiKeyForProvider as resolveApiKeyForProviderRaw,
5+
} from "../../agents/model-auth.js";
26
import { resolveStateDir } from "../../config/paths.js";
37
import { transcribeAudioFile } from "../../media-understanding/transcribe-audio.js";
48
import { textToSpeechTelephony } from "../../tts/tts.js";
@@ -59,6 +63,24 @@ export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}):
5963
events: createRuntimeEvents(),
6064
logging: createRuntimeLogging(),
6165
state: { resolveStateDir },
66+
modelAuth: {
67+
// Wrap model-auth helpers so plugins cannot steer credential lookups:
68+
// - agentDir / store: stripped (prevents reading other agents' stores)
69+
// - profileId / preferredProfile: stripped (prevents cross-provider
70+
// credential access via profile steering)
71+
// Plugins only specify provider/model; the core auth pipeline picks
72+
// the appropriate credential automatically.
73+
getApiKeyForModel: (params) =>
74+
getApiKeyForModelRaw({
75+
model: params.model,
76+
cfg: params.cfg,
77+
}),
78+
resolveApiKeyForProvider: (params) =>
79+
resolveApiKeyForProviderRaw({
80+
provider: params.provider,
81+
cfg: params.cfg,
82+
}),
83+
},
6284
} satisfies PluginRuntime;
6385

6486
return runtime;

src/plugins/runtime/types-core.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,16 @@ export type PluginRuntimeCore = {
5252
state: {
5353
resolveStateDir: typeof import("../../config/paths.js").resolveStateDir;
5454
};
55+
modelAuth: {
56+
/** Resolve auth for a model. Only provider/model and optional cfg are used. */
57+
getApiKeyForModel: (params: {
58+
model: import("@mariozechner/pi-ai").Model<import("@mariozechner/pi-ai").Api>;
59+
cfg?: import("../../config/config.js").OpenClawConfig;
60+
}) => Promise<import("../../agents/model-auth.js").ResolvedProviderAuth>;
61+
/** Resolve auth for a provider by name. Only provider and optional cfg are used. */
62+
resolveApiKeyForProvider: (params: {
63+
provider: string;
64+
cfg?: import("../../config/config.js").OpenClawConfig;
65+
}) => Promise<import("../../agents/model-auth.js").ResolvedProviderAuth>;
66+
};
5567
};

0 commit comments

Comments
 (0)