fix(plugins): expose model auth API to context-engine plugins#41090
fix(plugins): expose model auth API to context-engine plugins#41090jalehman merged 6 commits intoopenclaw:mainfrom
Conversation
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟡 Plugin-controlled
|
| Property | Value |
|---|---|
| Severity | Medium |
| CWE | CWE-284 |
| Location | src/plugins/runtime/index.ts:66-83 |
Description
createPluginRuntime() exposes runtime.modelAuth.getApiKeyForModel() / resolveApiKeyForProvider() as wrappers around the raw auth helpers, but the wrappers forward a plugin-supplied cfg object directly into the auth pipeline.
This undermines the stated goal of preventing credential steering:
- The wrapper strips
agentDir/store/profileId/preferredProfile, but still forwardscfg. - The raw resolver uses
cfgto influence which auth profile is selected:resolveApiKeyForProvider()callsresolveAuthProfileOrder({ cfg, ... }).resolveAuthProfileOrder()consultscfg.auth.orderandcfg.auth.profilesto build/override the candidate order.- Therefore a plugin can supply a crafted
cfgto bias selection toward a particular stored profile for a provider (credential steering), despiteprofileId/preferredProfilebeing stripped.
- Additionally, if stored credentials use
keyRef/tokenRefSecretRefs,resolveApiKeyForProfile()usescfg ?? loadConfig()as the configuration for SecretRef resolution; a plugin-controlledcfg.secrets.providerscan change the file/exec/env provider configuration used during secret resolution.
Vulnerable code (wrapper forwarding plugin-controlled cfg):
getApiKeyForModel: (params) =>
getApiKeyForModelRaw({ model: params.model, cfg: params.cfg }),
resolveApiKeyForProvider: (params) =>
resolveApiKeyForProviderRaw({ provider: params.provider, cfg: params.cfg }),Raw signatures showing additional steering inputs exist (even if not forwarded):
resolveApiKeyForProvider(params: { provider; cfg?; profileId?; preferredProfile?; store?; agentDir? })getApiKeyForModel(params: { model; cfg?; profileId?; preferredProfile?; store?; agentDir? })
Impact: In a threat model where plugins are less trusted than core, a plugin can influence which stored credential/profile is used (and potentially the SecretRef resolution configuration), contrary to the wrapper’s security comments.
Recommendation
Do not accept arbitrary cfg from plugins for credential resolution.
Options (in increasing strictness):
- Ignore plugin-supplied cfg entirely and use only host/runtime config:
import { loadConfig } from "../../config/config.js";
modelAuth: {
getApiKeyForModel: ({ model }) => getApiKeyForModelRaw({ model, cfg: loadConfig() }),
resolveApiKeyForProvider: ({ provider }) =>
resolveApiKeyForProviderRaw({ provider, cfg: loadConfig() }),
}-
If plugins must influence behavior, deep-pick an allowlist of safe fields and explicitly exclude
auth.order,auth.profiles, and allsecrets.*configuration (to prevent profile steering and SecretRef provider reconfiguration). -
For multi-agent isolation, pass a trusted
agentDirsourced from the core runtime/request context (not from plugin input), and never allow plugins to override it.
Also add behavioral tests proving that supplying cfg.auth.order/cfg.auth.profiles from a plugin does not affect the selected profile.
2. 🔵 Plugins can retrieve raw provider API keys via runtime.modelAuth without authorization
| Property | Value |
|---|---|
| Severity | Low |
| CWE | CWE-200 |
| Location | src/plugins/runtime/index.ts:66-83 |
Description
createPluginRuntime() now exposes runtime.modelAuth.getApiKeyForModel() and runtime.modelAuth.resolveApiKeyForProvider() to all loaded plugins.
Security impact:
- These helpers return
ResolvedProviderAuth, which includes a rawapiKey?: stringfield. - Any plugin can request credentials for any provider name it supplies (e.g.
"openai","anthropic", etc.). - There is no authorization layer binding allowed providers/models to a plugin’s manifest (
openclaw.plugin.jsonhas aprovidersfield, but it is not enforced here). - Plugins run in-process with the Gateway (per docs), are typically able to perform network I/O, and therefore can exfiltrate resolved credentials.
Vulnerable code:
modelAuth: {
getApiKeyForModel: (params) =>
getApiKeyForModelRaw({ model: params.model, cfg: params.cfg }),
resolveApiKeyForProvider: (params) =>
resolveApiKeyForProviderRaw({ provider: params.provider, cfg: params.cfg }),
},And the returned type includes the secret:
export type ResolvedProviderAuth = {
apiKey?: string;
...
}While the wrappers strip agentDir/store/profileId steering, they still expose cross-provider key lookup, which is sufficient for credential theft by a malicious or compromised third-party plugin.
Recommendation
Do not return raw long-lived provider secrets to plugins by default. Prefer a brokered/capability-based design:
- Option A (recommended): broker requests: expose a method that performs provider API calls on the plugin’s behalf without ever returning the API key.
- Option B: enforce provider allowlists: restrict
provider/modelresolution to the plugin’s declaredmanifest.providers(and/or explicitplugins.entries.<id>.allowedProviders) and require explicit admin opt-in. - Option C: return opaque handles: return an opaque auth handle scoped to a provider and plugin, usable only with runtime-brokered calls.
Example (enforce allowlist + avoid returning keys):
// runtime creation: pass pluginId into runtime calls or bind per-plugin runtime
resolveProviderAuth: async ({ pluginId, provider, cfg }) => {
if (!isProviderAllowedForPlugin(pluginId, provider)) {
throw new Error("provider not allowed for this plugin");
}
// return non-secret metadata only
const { mode, source } = await resolveApiKeyForProviderRaw({ provider, cfg });
return { mode, source };
}If plugins truly need to call providers directly, require an explicit configuration flag (default off) and strongly document that enabling it grants plugins access to host credentials.
Analyzed PR: #41090 at commit ee96e96
Last updated on: 2026-03-09T23:45:44Z
Greptile SummaryThis PR exposes model authentication to context-engine plugins by adding a Key changes:
No issues were found. The implementation is clean and correctly follows the established patterns used by the other runtime namespaces ( Confidence Score: 5/5
Last reviewed commit: dbb966c |
Addressing Aisle Security FindingFinding: 🟠 High — Plugin runtime exposes raw API keys with no permission gating (CWE-200) Mitigation applied (commit 33c956a): Wrapped Regarding the broader concern (raw API key exposure): Plugins already have access to A full host-mediated auth layer (Aisle recommendation #1) would be a larger architectural change beyond the scope of this fix. The |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 33c956a41d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| resolveApiKeyForProviderRaw({ | ||
| provider: params.provider, | ||
| cfg: params.cfg, | ||
| profileId: params.profileId, | ||
| }), |
There was a problem hiding this comment.
Forward preferredProfile in provider auth wrapper
runtime.modelAuth.resolveApiKeyForProvider accepts the full resolveApiKeyForProvider parameter shape, but this wrapper only forwards provider, cfg, and profileId. If a plugin passes preferredProfile (for example to select a specific auth profile when multiple profiles exist), the value is silently dropped, so the resolver falls back to default profile ordering and can return credentials for the wrong account/provider profile.
Useful? React with 👍 / 👎.
33c956a to
59e3da3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59e3da3807
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| resolveApiKeyForProviderRaw({ | ||
| provider: params.provider, | ||
| cfg: params.cfg, | ||
| profileId: params.profileId, | ||
| }), |
There was a problem hiding this comment.
Forward preferredProfile in provider auth wrapper
Fresh evidence in this commit: runtime.modelAuth.resolveApiKeyForProvider currently forwards only provider, cfg, and profileId, so plugin callers that pass preferredProfile will have that hint silently dropped and credential resolution will fall back to default profile ordering. This can select the wrong account when multiple profiles exist for the same provider.
Useful? React with 👍 / 👎.
59e3da3 to
9b62d35
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9b62d35553
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/plugins/runtime/index.ts
Outdated
| resolveApiKeyForProviderRaw({ | ||
| provider: params.provider, | ||
| cfg: params.cfg, | ||
| profileId: params.profileId, |
There was a problem hiding this comment.
Forward preferredProfile in provider auth wrapper
runtime.modelAuth.resolveApiKeyForProvider drops preferredProfile even though the underlying resolver supports it, so plugin callers cannot force profile selection when multiple auth profiles exist for the same provider. In that scenario, resolution falls back to default profile ordering and can return credentials for the wrong account, which is a functional regression compared with calling resolveApiKeyForProvider directly.
Useful? React with 👍 / 👎.
Addressing Aisle Follow-up (CWE-285)Finding: 🟠 High — Plugin SDK exports raw model-auth functions, allowing credential steering via Good catch. The Fix applied (commit 7c34f43): Removed |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f247386a6c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
src/plugins/runtime/types-core.ts
Outdated
| getApiKeyForModel: typeof import("../../agents/model-auth.js").getApiKeyForModel; | ||
| resolveApiKeyForProvider: typeof import("../../agents/model-auth.js").resolveApiKeyForProvider; |
There was a problem hiding this comment.
Narrow modelAuth params to the forwarded fields
PluginRuntimeCore exposes modelAuth with the raw model-auth function signatures, so plugin code is allowed to pass store/agentDir, but createPluginRuntime intentionally drops those fields and only forwards provider/model/cfg/profile hints. That means callers can compile successfully yet silently resolve credentials from the default auth store instead of the requested one (for example in non-default agent contexts or test stores), which can return the wrong credentials and is very hard to debug because no error is raised for the ignored inputs.
Useful? React with 👍 / 👎.
f247386 to
8f8a737
Compare
|
Rebased onto latest main — CHANGELOG conflict resolved. Branch is stable now, all changes finalized. |
|
@aisle-research-bot review |
|
Fixed CI: oxfmt whitespace issue in CONTRIBUTING.md (inherited from upstream rebase). All changes finalized. |
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🔵 Plugin runtime
|
| Property | Value |
|---|---|
| Severity | Low |
| CWE | CWE-693 |
| Location | src/plugins/runtime/types-core.ts:55-58 |
Description
PluginRuntimeCore.modelAuth is typed as the raw model-auth helper functions, even though the runtime implementation intentionally strips agentDir and store to prevent credential-store steering.
This creates a security regression risk / defense-in-depth bypass at the type level:
- The raw helper parameter object includes optional
agentDirandstore. - By reusing the raw function types in the plugin runtime surface, plugin authors can pass
agentDir/storewithout any TypeScript error. - Although
createPluginRuntime()currently forwards only safe fields, this typing makes it easy for future refactors (e.g.,...params) to accidentally reintroduce credential steering and will not be caught by the type system.
Vulnerable typing (exposes raw signature):
modelAuth: {
getApiKeyForModel: typeof import("../../agents/model-auth.js").getApiKeyForModel;
resolveApiKeyForProvider: typeof import("../../agents/model-auth.js").resolveApiKeyForProvider;
};Raw helper parameters include the unsafe fields:
resolveApiKeyForProvider(params: { ...; store?: AuthProfileStore; agentDir?: string; })getApiKeyForModel(params: { ...; store?: AuthProfileStore; agentDir?: string; })
While this is not an immediate runtime exploit (wrappers currently drop those fields), it undermines the intended security boundary for plugins and increases the chance of a future credential isolation bypass.
Recommendation
Define restricted parameter types for the plugin runtime wrappers and avoid exporting the raw helper function types.
Example (omit unsafe overrides):
import type {
getApiKeyForModel as getApiKeyForModelRaw,
resolveApiKeyForProvider as resolveApiKeyForProviderRaw,
} from "../../agents/model-auth.js";
type GetApiKeyForModelParams = Omit<
Parameters<typeof getApiKeyForModelRaw>[0],
"agentDir" | "store"
>;
type ResolveApiKeyForProviderParams = Omit<
Parameters<typeof resolveApiKeyForProviderRaw>[0],
"agentDir" | "store"
>;
export type PluginRuntimeCore = {
// ...
modelAuth: {
getApiKeyForModel: (params: GetApiKeyForModelParams) => ReturnType<typeof getApiKeyForModelRaw>;
resolveApiKeyForProvider: (
params: ResolveApiKeyForProviderParams,
) => ReturnType<typeof resolveApiKeyForProviderRaw>;
};
};Add a type-level/contract test asserting that agentDir and store are rejected for runtime.modelAuth.* calls (so accidental spreading/forwarding is more likely to be caught during review/CI).
2. 🔵 Plugin runtime exposes unrestricted modelAuth API enabling API key exfiltration via profileId/cfg injection
| Property | Value |
|---|---|
| Severity | Low |
| CWE | CWE-862 |
| Location | src/plugins/runtime/index.ts:66-85 |
Description
createPluginRuntime() now exposes runtime.modelAuth.getApiKeyForModel() / resolveApiKeyForProvider() to plugin code. These wrappers return raw provider credentials (API keys / OAuth tokens) and forward plugin-controlled parameters (profileId, preferredProfile, cfg) into the host credential-resolution logic.
In src/agents/model-auth.ts, resolveApiKeyForProvider():
- Accepts an arbitrary
profileIdand, if provided, resolves and returns that profile’s credential without any authorization check or binding to a caller/session identity. - Falls back to resolving credentials from the global auth profile store (
ensureAuthProfileStore(undefined)whenagentDir/storearen’t provided) and from process environment variables. - Accepts a caller-provided
cfgwhich influences credential resolution (provider config, auth ordering, secret-ref resolution defaults). A plugin can supply a crafted config object rather than the host’s active config snapshot.
Because plugins execute as third-party code, this newly exposed API provides a straightforward credential-exfiltration primitive: a plugin can call runtime.modelAuth.resolveApiKeyForProvider({ provider: "openai" }) (or specify profileId) and receive the raw key/token in-process.
Vulnerable code (newly added):
modelAuth: {
getApiKeyForModel: (params) =>
getApiKeyForModelRaw({
model: params.model,
cfg: params.cfg,
profileId: params.profileId,
preferredProfile: params.preferredProfile,
}),
resolveApiKeyForProvider: (params) =>
resolveApiKeyForProviderRaw({
provider: params.provider,
cfg: params.cfg,
profileId: params.profileId,
preferredProfile: params.preferredProfile,
}),
},Impact depends on trust model, but if plugins are not fully trusted, this is a direct bypass of credential isolation (and can be used to exfiltrate user/host API keys and bearer tokens).
Recommendation
If plugins are intended to be untrusted or semi-trusted, do not expose raw credential material to them.
Recommended fixes (choose one depending on intended trust boundary):
-
Remove/replace the API: expose a higher-level capability that performs provider calls on behalf of the plugin (returning only the requested model result), not the API key.
-
Enforce authorization/scoping:
- Do not accept
cfgfrom plugin callers; load the host’s active config snapshot internally. - Do not allow arbitrary
profileId/preferredProfilefrom plugin callers; instead:- derive the profile selection from host policy (e.g., per-plugin allowlist of providers/profiles), and/or
- only allow selecting from a set of profiles explicitly granted to that plugin.
- Do not accept
Example: ignore caller-controlled cfg/profileId and restrict provider selection:
resolveApiKeyForProvider: async ({ provider }) => {
if (!ALLOWED_PROVIDERS_FOR_PLUGIN.has(provider)) {
throw new Error("Provider not allowed");
}
const cfg = loadConfig(); // or a runtime snapshot
return resolveApiKeyForProviderRaw({ provider, cfg });
}- If the goal is only to ensure a provider is configured, expose a boolean/metadata API (e.g.,
hasAuthForProvider) rather than returning credentials.
Analyzed PR: #41090 at commit 81ac956
Last updated on: 2026-03-09T19:01:11Z
Addressing Aisle CWE-862 (profile steering)Stripped Remaining Aisle concern (raw API key exposure): This is inherent to the plugin trust model — plugins are explicitly installed by the user ( |
|
Note on Aisle 🟠 High: There's a fundamental trade-off here — the whole point of this PR is to let context-engine plugins (like lossless-claw) call LLM APIs, which requires API keys. If we don't expose keys, #40902 stays broken. We've hardened the surface as much as possible (stripped agentDir, store, profileId, preferredProfile), but the raw key exposure is inherent to the feature. |
cf8a878 to
ae7a218
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae7a218c99
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| getApiKeyForModelRaw({ | ||
| model: params.model, | ||
| cfg: params.cfg, | ||
| }), |
There was a problem hiding this comment.
Bind modelAuth lookups to the active agent store
runtime.modelAuth.getApiKeyForModel forwards only model and cfg, so auth resolution always falls back to the default store path instead of the request’s agent-specific store. In multi-agent setups where credentials live only in a non-default agent directory, a context-engine plugin calling this API can resolve the wrong profile (or no credentials) even when the agent is correctly configured, because there is no way to pass/derive that agent scope through this wrapper.
Useful? React with 👍 / 👎.
ae7a218 to
88dfe13
Compare
Context-engine plugins (e.g. lossless-claw) that call LLM APIs via completeSimple cannot resolve API keys from the main OpenClaw config. This causes repeated 'No API key for provider' errors and forces fallback to truncation. Add runtime.modelAuth to PluginRuntimeCore with getApiKeyForModel and resolveApiKeyForProvider so plugins can resolve credentials through the standard auth pipeline (config, env vars, auth profiles). Also re-export these helpers and the ResolvedProviderAuth type from the plugin-sdk barrel for direct import by plugin authors. Fixes openclaw#40902
Address Aisle security review: wrap getApiKeyForModel and resolveApiKeyForProvider so plugins cannot pass arbitrary agentDir or store overrides to steer credential lookups outside their own context. Only provider, model, cfg, profileId, and preferredProfile are forwarded to the underlying auth pipeline. Add test verifying the wrappers are not direct references to the raw functions.
Address Aisle follow-up: the plugin-sdk barrel re-exported getApiKeyForModel and resolveApiKeyForProvider directly from model-auth.ts, allowing plugins to bypass the runtime.modelAuth wrappers and pass arbitrary agentDir/store overrides for credential steering. Remove these raw exports. Plugins must use runtime.modelAuth which strips unsafe parameters. Keep requireApiKey (sync null-check helper with no agentDir parameter) and the ResolvedProviderAuth type export.
…apper The wrapper for resolveApiKeyForProvider silently dropped the preferredProfile parameter, causing plugins to fall back to default profile ordering when multiple auth profiles exist for the same provider.
…wrappers Address Aisle CWE-862: plugins could use profileId to resolve credentials for arbitrary profiles regardless of provider, enabling cross-provider credential access. Now plugins can only specify provider/model — the core auth pipeline picks the appropriate credential. The TypeScript type is also narrowed so plugin authors cannot pass profileId at compile time.
88dfe13 to
ee96e96
Compare
* main: (33 commits) Exec: mark child command env with OPENCLAW_CLI (openclaw#41411) fix(plugins): expose model auth API to context-engine plugins (openclaw#41090) Add HTTP 499 to transient error codes for model fallback (openclaw#41468) Logging: harden probe suppression for observations (openclaw#41338) fix(discord): apply effective maxLinesPerMessage in live replies (openclaw#40133) build(protocol): regenerate Swift models after pending node work schemas (openclaw#41477) Agents: add fallback error observations (openclaw#41337) acp: harden follow-up reliability and attachments (openclaw#41464) fix(agents): probe single-provider billing cooldowns (openclaw#41422) acp: add regression coverage and smoke-test docs (openclaw#41456) acp: forward attachments into ACP runtime sessions (openclaw#41427) acp: enrich streaming updates for ide clients (openclaw#41442) Sandbox: import STATE_DIR from paths directly (openclaw#41439) acp: restore session context and controls (openclaw#41425) acp: fail honestly in bridge mode (openclaw#41424) Gateway: tighten node pending drain semantics (openclaw#41429) Gateway: add pending node work primitives (openclaw#41409) fix(auth): reset cooldown error counters on expiry to prevent infinite escalation (openclaw#41028) fix(cron): do not misclassify empty/NO_REPLY as interim acknowledgement (openclaw#41401) iOS: reconnect gateway on foreground return (openclaw#41384) ...
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman (cherry picked from commit 4790e40)
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
…aw#41090) Merged via squash. Prepared head SHA: ee96e96 Co-authored-by: xinhuagu <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
Problem
Context-engine plugins (e.g.
@martian-engineering/lossless-claw) that call LLM APIs viacompleteSimplecannot resolve API keys from the main OpenClaw configuration. This causes repeated errors:The plugin runtime (
PluginRuntimeCore) exposes config, system, media, tts, stt, tools, events, logging, and state — but not model authentication. Plugins have no way to resolve API keys through the standard auth pipeline.Solution
Add
runtime.modelAuthtoPluginRuntimeCorewith:getApiKeyForModel— resolve auth for a specific model (config → env → auth profiles)resolveApiKeyForProvider— resolve auth for a provider by nameAlso re-export these helpers and the
ResolvedProviderAuthtype from the plugin-sdk barrel (openclaw/plugin-sdk) so plugin authors can import them directly.Changes
src/plugins/runtime/types-core.tsmodelAuthtoPluginRuntimeCoretypesrc/plugins/runtime/index.tsgetApiKeyForModelandresolveApiKeyForProvidersrc/plugin-sdk/index.tsextensions/test-utils/plugin-runtime-mock.tsmodelAuthmocksrc/plugins/runtime/index.test.tsmodelAuthis exposed correctlyTesting
runtime.modelAuth.getApiKeyForModelandruntime.modelAuth.resolveApiKeyForProviderare wired to the correct implementationstsc --noEmitpasses with zero errorsFixes #40902