Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f46ba0e

Browse filesBrowse files
authored
fix: unify plugin credential injection with OAuth and env var support (MoonshotAI#1530)
1 parent c2ef61b commit f46ba0e
Copy full SHA for f46ba0e

5 files changed

+144-14Lines changed: 144 additions & 14 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/kimi_cli/app.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/app.py
+14Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,20 @@ async def create(
172172
runtime.notifications.recover()
173173
runtime.background_tasks.reconcile()
174174

175+
# Refresh plugin configs with fresh credentials (e.g. OAuth tokens)
176+
try:
177+
from kimi_cli.plugin.manager import (
178+
collect_host_values,
179+
get_plugins_dir,
180+
refresh_plugin_configs,
181+
)
182+
183+
host_values = collect_host_values(config, oauth)
184+
if host_values.get("api_key"):
185+
refresh_plugin_configs(get_plugins_dir(), host_values)
186+
except Exception:
187+
logger.debug("Failed to refresh plugin configs, skipping")
188+
175189
if agent_file is None:
176190
agent_file = DEFAULT_AGENT_FILE
177191
if startup_progress is not None:
Collapse file

‎src/kimi_cli/cli/plugin.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/cli/plugin.py
+10-6Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,16 +199,20 @@ def install_cmd(
199199
try:
200200
config = load_config()
201201

202-
# Collect host values from the current default provider
203-
host_values: dict[str, str] = {}
202+
from kimi_cli.auth.oauth import OAuthManager
203+
from kimi_cli.llm import augment_provider_with_env_vars
204+
from kimi_cli.plugin.manager import collect_host_values
205+
206+
# Apply env var overrides (install runs outside normal startup)
204207
if config.default_model and config.default_model in config.models:
205208
model = config.models[config.default_model]
206209
if model.provider in config.providers:
207-
provider = config.providers[model.provider]
208-
host_values["api_key"] = provider.api_key.get_secret_value()
209-
host_values["base_url"] = provider.base_url
210+
augment_provider_with_env_vars(config.providers[model.provider], model)
211+
212+
oauth = OAuthManager(config)
213+
host_values = collect_host_values(config, oauth)
210214

211-
if not host_values:
215+
if not host_values.get("api_key"):
212216
typer.echo(
213217
"Warning: No LLM provider configured. "
214218
"Plugins requiring API key injection will fail. "
Collapse file

‎src/kimi_cli/plugin/manager.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/plugin/manager.py
+49Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import shutil
66
import tempfile
77
from pathlib import Path
8+
from typing import TYPE_CHECKING
89

910
from kimi_cli.plugin import (
1011
PLUGIN_JSON,
@@ -17,12 +18,39 @@
1718
)
1819
from kimi_cli.share import get_share_dir
1920

21+
if TYPE_CHECKING:
22+
from kimi_cli.auth.oauth import OAuthManager
23+
from kimi_cli.config import Config
24+
2025

2126
def get_plugins_dir() -> Path:
2227
"""Return the plugins installation directory (~/.kimi/plugins/)."""
2328
return get_share_dir() / "plugins"
2429

2530

31+
def collect_host_values(config: Config, oauth: OAuthManager) -> dict[str, str]:
32+
"""Collect host values (api_key, base_url) for plugin injection.
33+
34+
Resolves credentials from the default provider, handling OAuth tokens
35+
and static API keys. Callers that run outside the normal startup flow
36+
(e.g. ``install_cmd``) should apply environment-variable overrides
37+
(``augment_provider_with_env_vars``) to the provider **before** calling
38+
this function; the main app startup already does that.
39+
"""
40+
values: dict[str, str] = {}
41+
if not config.default_model or config.default_model not in config.models:
42+
return values
43+
model = config.models[config.default_model]
44+
if model.provider not in config.providers:
45+
return values
46+
provider = config.providers[model.provider]
47+
api_key = oauth.resolve_api_key(provider.api_key, provider.oauth)
48+
if api_key:
49+
values["api_key"] = api_key
50+
values["base_url"] = provider.base_url
51+
return values
52+
53+
2654
def _validate_name(name: str, plugins_dir: Path) -> Path:
2755
"""Resolve and validate plugin name, returning the safe destination path."""
2856
dest = (plugins_dir / name).resolve()
@@ -80,6 +108,27 @@ def install_plugin(
80108
return parse_plugin_json(dest / PLUGIN_JSON)
81109

82110

111+
def refresh_plugin_configs(plugins_dir: Path, host_values: dict[str, str]) -> None:
112+
"""Re-inject host values into all installed plugin config files.
113+
114+
Called at startup so that OAuth tokens and other credentials
115+
stay fresh even after the initial install.
116+
"""
117+
if not plugins_dir.is_dir():
118+
return
119+
120+
for child in sorted(plugins_dir.iterdir()):
121+
plugin_json = child / PLUGIN_JSON
122+
if not child.is_dir() or not plugin_json.is_file():
123+
continue
124+
try:
125+
spec = parse_plugin_json(plugin_json)
126+
if spec.inject and spec.config_file:
127+
inject_config(child, spec, host_values)
128+
except Exception:
129+
continue
130+
131+
83132
def list_plugins(plugins_dir: Path) -> list[PluginSpec]:
84133
"""List all installed plugins."""
85134
if not plugins_dir.is_dir():
Collapse file

‎src/kimi_cli/plugin/tool.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/plugin/tool.py
+5-8Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,11 @@ def _get_host_values(config: Config) -> dict[str, str]:
2727
Reads the latest provider credentials, which may have been
2828
refreshed by OAuth since plugin install time.
2929
"""
30-
values: dict[str, str] = {}
31-
if config.default_model and config.default_model in config.models:
32-
model = config.models[config.default_model]
33-
if model.provider in config.providers:
34-
provider = config.providers[model.provider]
35-
values["api_key"] = provider.api_key.get_secret_value()
36-
values["base_url"] = provider.base_url
37-
return values
30+
from kimi_cli.auth.oauth import OAuthManager
31+
from kimi_cli.plugin.manager import collect_host_values
32+
33+
oauth = OAuthManager(config)
34+
return collect_host_values(config, oauth)
3835

3936

4037
class PluginTool(CallableTool):
Collapse file

‎tests/core/test_plugin_manager.py‎

Copy file name to clipboardExpand all lines: tests/core/test_plugin_manager.py
+66Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import json
44
from pathlib import Path
5+
from unittest.mock import MagicMock
56

67
import pytest
8+
from pydantic import SecretStr
79

810
from kimi_cli.plugin import PluginError
911
from kimi_cli.plugin.manager import (
12+
collect_host_values,
1013
install_plugin,
1114
list_plugins,
1215
remove_plugin,
@@ -214,3 +217,66 @@ async def test_skill_discovery_includes_plugins_dir(tmp_path: Path, monkeypatch)
214217
roots = await resolve_skills_roots(KaosPath(str(tmp_path)))
215218
root_strs = [str(r) for r in roots]
216219
assert str(plugins_dir) in root_strs
220+
221+
222+
# --- collect_host_values tests ---
223+
224+
225+
def _make_config(*, api_key: str = "sk-test", oauth: object = None):
226+
"""Build a minimal mock Config with a default model and provider."""
227+
provider = MagicMock()
228+
provider.api_key = SecretStr(api_key)
229+
provider.oauth = oauth
230+
provider.base_url = "https://api.example.com/v1"
231+
232+
model = MagicMock()
233+
model.provider = "test-provider"
234+
235+
config = MagicMock()
236+
config.default_model = "test-model"
237+
config.models = {"test-model": model}
238+
config.providers = {"test-provider": provider}
239+
return config
240+
241+
242+
def test_collect_host_values_static_key():
243+
"""Static API key (no OAuth) is returned correctly."""
244+
config = _make_config(api_key="sk-static-key")
245+
oauth = MagicMock()
246+
oauth.resolve_api_key.return_value = "sk-static-key"
247+
248+
values = collect_host_values(config, oauth)
249+
assert values["api_key"] == "sk-static-key"
250+
assert values["base_url"] == "https://api.example.com/v1"
251+
252+
253+
def test_collect_host_values_oauth_token():
254+
"""OAuth token is returned when provider has OAuth configured."""
255+
oauth_ref = MagicMock()
256+
config = _make_config(api_key="", oauth=oauth_ref)
257+
oauth = MagicMock()
258+
oauth.resolve_api_key.return_value = "eyJ-oauth-token"
259+
260+
values = collect_host_values(config, oauth)
261+
assert values["api_key"] == "eyJ-oauth-token"
262+
oauth.resolve_api_key.assert_called_once()
263+
264+
265+
def test_collect_host_values_no_default_model():
266+
"""Returns empty dict when no default_model is configured."""
267+
config = MagicMock()
268+
config.default_model = None
269+
oauth = MagicMock()
270+
271+
values = collect_host_values(config, oauth)
272+
assert values == {}
273+
274+
275+
def test_collect_host_values_empty_key():
276+
"""Empty API key is not included in values."""
277+
config = _make_config(api_key="")
278+
oauth = MagicMock()
279+
oauth.resolve_api_key.return_value = ""
280+
281+
values = collect_host_values(config, oauth)
282+
assert "api_key" not in values

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.