Skip to content

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

Merged
jalehman merged 6 commits intoopenclaw:mainfrom
xinhuagu:fix/plugin-model-auth-api
Mar 9, 2026
Merged

fix(plugins): expose model auth API to context-engine plugins#41090
jalehman merged 6 commits intoopenclaw:mainfrom
xinhuagu:fix/plugin-model-auth-api

Conversation

@xinhuagu
Copy link
Copy Markdown
Contributor

@xinhuagu xinhuagu commented Mar 9, 2026

Problem

Context-engine plugins (e.g. @martian-engineering/lossless-claw) that call LLM APIs via completeSimple cannot resolve API keys from the main OpenClaw configuration. This causes repeated errors:

[lcm] completeSimple error: No API key for provider: bailian
[lcm] empty normalized summary on first attempt; falling back to truncation

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.modelAuth to PluginRuntimeCore with:

  • getApiKeyForModel — resolve auth for a specific model (config → env → auth profiles)
  • resolveApiKeyForProvider — resolve auth for a provider by name

Also re-export these helpers and the ResolvedProviderAuth type from the plugin-sdk barrel (openclaw/plugin-sdk) so plugin authors can import them directly.

Changes

File Change
src/plugins/runtime/types-core.ts Add modelAuth to PluginRuntimeCore type
src/plugins/runtime/index.ts Wire getApiKeyForModel and resolveApiKeyForProvider
src/plugin-sdk/index.ts Re-export model auth helpers and types
extensions/test-utils/plugin-runtime-mock.ts Add modelAuth mock
src/plugins/runtime/index.test.ts Test that modelAuth is exposed correctly

Testing

  • Added test verifying runtime.modelAuth.getApiKeyForModel and runtime.modelAuth.resolveApiKeyForProvider are wired to the correct implementations
  • tsc --noEmit passes with zero errors
  • All existing plugin runtime tests pass

Fixes #40902

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 9, 2026

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Plugin-controlled cfg enables credential steering in runtime.modelAuth wrappers
2 🔵 Low Plugins can retrieve raw provider API keys via runtime.modelAuth without authorization

1. 🟡 Plugin-controlled cfg enables credential steering in runtime.modelAuth wrappers

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 forwards cfg.
  • The raw resolver uses cfg to influence which auth profile is selected:
    • resolveApiKeyForProvider() calls resolveAuthProfileOrder({ cfg, ... }).
    • resolveAuthProfileOrder() consults cfg.auth.order and cfg.auth.profiles to build/override the candidate order.
    • Therefore a plugin can supply a crafted cfg to bias selection toward a particular stored profile for a provider (credential steering), despite profileId/preferredProfile being stripped.
  • Additionally, if stored credentials use keyRef/tokenRef SecretRefs, resolveApiKeyForProfile() uses cfg ?? loadConfig() as the configuration for SecretRef resolution; a plugin-controlled cfg.secrets.providers can 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):

  1. 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() }),
}
  1. If plugins must influence behavior, deep-pick an allowlist of safe fields and explicitly exclude auth.order, auth.profiles, and all secrets.* configuration (to prevent profile steering and SecretRef provider reconfiguration).

  2. For multi-agent isolation, pass a trusted agentDir sourced 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 raw apiKey?: string field.
  • 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.json has a providers field, 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/model resolution to the plugin’s declared manifest.providers (and/or explicit plugins.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-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR exposes model authentication to context-engine plugins by adding a modelAuth namespace to PluginRuntimeCore / createPluginRuntime, and re-exporting the related helpers and types from the openclaw/plugin-sdk barrel. The change is small, well-scoped, and consistent with the existing runtime architecture.

Key changes:

  • runtime.modelAuth is added with getApiKeyForModel and resolveApiKeyForProvider wired directly to the existing src/agents/model-auth.ts implementations.
  • requireApiKey and ResolvedProviderAuth are re-exported from the SDK barrel for plugin authors who need the sync helper or the type directly.
  • The mock in test-utils is kept in sync with the new interface field.
  • A new test verifies reference equality, confirming the functions are not wrapped.

No issues were found. The implementation is clean and correctly follows the established patterns used by the other runtime namespaces (state, tts, stt, etc.).

Confidence Score: 5/5

  • This PR is safe to merge — it is a minimal, additive change with no breaking modifications to existing runtime behaviour.
  • All five changed files are consistent with one another (type definition, implementation, mock, and test). The new modelAuth property follows the same structural pattern as existing runtime namespaces, functions are wired directly without wrapping, and the test confirms correct reference equality. No existing code paths are modified.
  • No files require special attention.

Last reviewed commit: dbb966c

@jalehman jalehman self-assigned this Mar 9, 2026
@xinhuagu
Copy link
Copy Markdown
Contributor Author

xinhuagu commented Mar 9, 2026

Addressing Aisle Security Finding

Finding: 🟠 High — Plugin runtime exposes raw API keys with no permission gating (CWE-200)

Mitigation applied (commit 33c956a): Wrapped getApiKeyForModel and resolveApiKeyForProvider so plugins cannot pass agentDir or store overrides. Only provider, model, cfg, profileId, and preferredProfile are forwarded to the underlying auth pipeline. This prevents plugins from steering credential lookups outside their own context.

Regarding the broader concern (raw API key exposure): Plugins already have access to runtime.config.loadConfig() which returns the full config including models.providers.*.apiKey. The plugin trust model in OpenClaw treats installed plugins as trusted code — the user explicitly installs them via openclaw plugins install. The modelAuth helpers provide a standard resolution path (config → env → auth profiles) rather than forcing plugins to parse credentials themselves, which would be more error-prone.

A full host-mediated auth layer (Aisle recommendation #1) would be a larger architectural change beyond the scope of this fix. The agentDir restriction addresses the most actionable part of the finding.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +79 to +83
resolveApiKeyForProviderRaw({
provider: params.provider,
cfg: params.cfg,
profileId: params.profileId,
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@jalehman jalehman force-pushed the fix/plugin-model-auth-api branch from 33c956a to 59e3da3 Compare March 9, 2026 15:24
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +79 to +83
resolveApiKeyForProviderRaw({
provider: params.provider,
cfg: params.cfg,
profileId: params.profileId,
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@jalehman jalehman force-pushed the fix/plugin-model-auth-api branch from 59e3da3 to 9b62d35 Compare March 9, 2026 15:33
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +79 to +82
resolveApiKeyForProviderRaw({
provider: params.provider,
cfg: params.cfg,
profileId: params.profileId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@xinhuagu
Copy link
Copy Markdown
Contributor Author

xinhuagu commented Mar 9, 2026

Addressing Aisle Follow-up (CWE-285)

Finding: 🟠 High — Plugin SDK exports raw model-auth functions, allowing credential steering via agentDir/store bypass

Good catch. The runtime.modelAuth wrappers stripped unsafe parameters, but the plugin-sdk barrel still re-exported the raw functions — plugins could import them directly and bypass the wrappers.

Fix applied (commit 7c34f43): Removed getApiKeyForModel and resolveApiKeyForProvider from the plugin-sdk barrel export. Plugins must now use runtime.modelAuth which enforces parameter stripping. Only requireApiKey (sync null-check, no agentDir param) and the ResolvedProviderAuth type remain exported.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +56 to +57
getApiKeyForModel: typeof import("../../agents/model-auth.js").getApiKeyForModel;
resolveApiKeyForProvider: typeof import("../../agents/model-auth.js").resolveApiKeyForProvider;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@xinhuagu xinhuagu force-pushed the fix/plugin-model-auth-api branch from f247386 to 8f8a737 Compare March 9, 2026 17:54
@xinhuagu
Copy link
Copy Markdown
Contributor Author

xinhuagu commented Mar 9, 2026

Rebased onto latest main — CHANGELOG conflict resolved. Branch is stable now, all changes finalized.

@xinhuagu
Copy link
Copy Markdown
Contributor Author

xinhuagu commented Mar 9, 2026

@aisle-research-bot review

@xinhuagu
Copy link
Copy Markdown
Contributor Author

xinhuagu commented Mar 9, 2026

Fixed CI: oxfmt whitespace issue in CONTRIBUTING.md (inherited from upstream rebase). All changes finalized.

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 9, 2026

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🔵 Low Plugin runtime modelAuth types expose unsafe agentDir/store overrides (type-level guard bypass)
2 🔵 Low Plugin runtime exposes unrestricted modelAuth API enabling API key exfiltration via profileId/cfg injection

1. 🔵 Plugin runtime modelAuth types expose unsafe agentDir/store overrides (type-level guard bypass)

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 agentDir and store.
  • By reusing the raw function types in the plugin runtime surface, plugin authors can pass agentDir/store without 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 profileId and, 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) when agentDir/store aren’t provided) and from process environment variables.
  • Accepts a caller-provided cfg which 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):

  1. 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.

  2. Enforce authorization/scoping:

    • Do not accept cfg from plugin callers; load the host’s active config snapshot internally.
    • Do not allow arbitrary profileId/preferredProfile from 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.

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 });
}
  1. 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

@xinhuagu
Copy link
Copy Markdown
Contributor Author

xinhuagu commented Mar 9, 2026

Addressing Aisle CWE-862 (profile steering)

Stripped profileId and preferredProfile from plugin modelAuth wrappers. Plugins now only specify provider/model — the core auth pipeline selects the credential. TypeScript types are also narrowed so these params cannot be passed at compile time.

Remaining Aisle concern (raw API key exposure): This is inherent to the plugin trust model — plugins are explicitly installed by the user (openclaw plugins install) and already have access to runtime.config.loadConfig() which returns full config including API keys. A host-mediated auth layer would be a larger architectural change beyond this fix's scope.

@xinhuagu
Copy link
Copy Markdown
Contributor Author

xinhuagu commented Mar 9, 2026

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.

@xinhuagu xinhuagu force-pushed the fix/plugin-model-auth-api branch 2 times, most recently from cf8a878 to ae7a218 Compare March 9, 2026 22:25
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +74 to +77
getApiKeyForModelRaw({
model: params.model,
cfg: params.cfg,
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@jalehman jalehman force-pushed the fix/plugin-model-auth-api branch from ae7a218 to 88dfe13 Compare March 9, 2026 22:58
xinhuagu and others added 5 commits March 9, 2026 15:59
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.
@jalehman jalehman force-pushed the fix/plugin-model-auth-api branch from 88dfe13 to ee96e96 Compare March 9, 2026 23:01
@jalehman jalehman merged commit 4790e40 into openclaw:main Mar 9, 2026
27 of 28 checks passed
@jalehman
Copy link
Copy Markdown
Contributor

jalehman commented Mar 9, 2026

Merged via squash.

Thanks @xinhuagu!

mrosmarin added a commit to mrosmarin/openclaw that referenced this pull request Mar 9, 2026
* 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)
  ...
ademczuk pushed a commit to ademczuk/openclaw that referenced this pull request Mar 10, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
mukhtharcm pushed a commit to hnykda/openclaw that referenced this pull request Mar 10, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
jenawant pushed a commit to jenawant/openclaw that referenced this pull request Mar 10, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
aiwatching pushed a commit to aiwatching/openclaw that referenced this pull request Mar 10, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
Moshiii pushed a commit to Moshiii/openclaw that referenced this pull request Mar 11, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
Moshiii pushed a commit to Moshiii/openclaw that referenced this pull request Mar 11, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
dominicnunez pushed a commit to dominicnunez/openclaw that referenced this pull request Mar 11, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
dhoman pushed a commit to dhoman/chrono-claw that referenced this pull request Mar 11, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
Ruijie-Ysp pushed a commit to Ruijie-Ysp/clawdbot that referenced this pull request Mar 12, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
qipyle pushed a commit to qipyle/openclaw that referenced this pull request Mar 12, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
GGzili pushed a commit to GGzili/moltbot that referenced this pull request Mar 12, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
Interstellar-code pushed a commit to Interstellar-code/operator1 that referenced this pull request Mar 16, 2026
…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)
senw-developers pushed a commit to senw-developers/va-openclaw that referenced this pull request Mar 17, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 22, 2026
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 22, 2026
liuy pushed a commit to liuy/openclaw that referenced this pull request Mar 25, 2026
…aw#41090)

Merged via squash.

Prepared head SHA: ee96e96
Co-authored-by: xinhuagu <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] LCM throws "No API key for provider: bailian" and falls back to truncation, despite main config having bailian API key

2 participants