-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: auto approval w/ config granularity issue 1631 #2095
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -263,6 +263,20 @@ class Config(BaseModel): | |
| default=True, | ||
| description="Enable anonymous telemetry to help improve kimi-cli. Set to false to disable.", | ||
| ) | ||
| default_auto_approve_actions: list[str] = Field( | ||
| default_factory=list, | ||
| description=( | ||
| "List of action name patterns to auto-approve by default in every session. " | ||
| "Supports glob patterns (*, ?). Examples: 'mcp:obsidian_*', 'edit file'." | ||
| ), | ||
| ) | ||
| auto_approve_workspace_dirs: list[str] = Field( | ||
| default_factory=list, | ||
| description=( | ||
| "List of workspace directory names (relative to work_dir) for which " | ||
| "file write/edit approvals should be skipped automatically." | ||
| ), | ||
| ) | ||
|
|
||
| @model_validator(mode="after") | ||
| def validate_model(self) -> Self: | ||
|
|
@@ -279,6 +293,85 @@ def get_config_file() -> Path: | |
| return get_share_dir() / "config.toml" | ||
|
|
||
|
|
||
| _DEFAULT_CONFIG_TEMPLATE = """\ | ||
| # Kimi Code CLI configuration file | ||
| # Documentation: https://kimi-cli.github.io/configuration/config-files | ||
|
|
||
| # Default model to use for new sessions. Must match a key in the [models] table. | ||
| default_model = "" | ||
|
|
||
| # Default behavior flags | ||
| default_thinking = false | ||
| default_yolo = false | ||
| default_plan_mode = false | ||
|
|
||
| # External editor command (e.g. "vim", "code --wait"). Leave empty for auto-detect. | ||
| default_editor = "" | ||
|
|
||
| # Terminal color theme: "dark" or "light" | ||
| theme = "dark" | ||
|
|
||
| # Stream reasoning text in the live area? Set to false for a compact indicator only. | ||
| show_thinking_stream = true | ||
|
|
||
| # Merge skills from all brand directories (kimi, claude, codex, etc.) | ||
| merge_all_available_skills = true | ||
|
|
||
| # ------------------------------------------------------------------------------ | ||
| # Auto-approval configuration | ||
| # ------------------------------------------------------------------------------ | ||
| # Glob patterns for actions that should be auto-approved in EVERY session. | ||
| # These are merged with any session-specific approvals you make interactively. | ||
| # Examples: | ||
| # default_auto_approve_actions = ["mcp:obsidian_*"] | ||
| # default_auto_approve_actions = ["mcp:obsidian_*", "mcp:memory_*"] | ||
| default_auto_approve_actions = [] | ||
|
|
||
| # Workspace directory names (relative to the current work_dir) where file | ||
| # write/edit approvals are skipped automatically. Useful for skills, plans, | ||
| # notes, or other directories the agent routinely modifies. | ||
| # Examples: | ||
| # auto_approve_workspace_dirs = ["skills", "plans"] | ||
| # auto_approve_workspace_dirs = ["docs", "notes"] | ||
| auto_approve_workspace_dirs = [] | ||
|
|
||
| # Extra directories to discover skills from (absolute, ~-prefixed, or relative) | ||
| extra_skill_dirs = [] | ||
|
|
||
| # Enable anonymous telemetry to help improve kimi-cli. Set to false to disable. | ||
| telemetry = true | ||
|
|
||
| # Suppress the YOLO mode hint injected into the system prompt. | ||
| skip_yolo_prompt_injection = false | ||
|
|
||
| [loop_control] | ||
| max_steps_per_turn = 500 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new default TOML template hard-codes Useful? React with 👍 / 👎. |
||
| max_retries_per_step = 3 | ||
| max_ralph_iterations = 0 | ||
| reserved_context_size = 50000 | ||
| compaction_trigger_ratio = 0.85 | ||
|
|
||
| [background] | ||
| max_running_tasks = 4 | ||
| read_max_bytes = 30000 | ||
| notification_tail_lines = 20 | ||
| notification_tail_chars = 3000 | ||
| wait_poll_interval_ms = 500 | ||
| worker_heartbeat_interval_ms = 5000 | ||
| worker_stale_after_ms = 15000 | ||
| kill_grace_period_ms = 2000 | ||
| keep_alive_on_exit = false | ||
| agent_task_timeout_s = 900 | ||
| print_wait_ceiling_s = 3600 | ||
|
|
||
| [notifications] | ||
| claim_stale_after_ms = 15000 | ||
|
|
||
| [mcp.client] | ||
| tool_call_timeout_ms = 60000 | ||
| """ | ||
|
|
||
|
|
||
| def get_default_config() -> Config: | ||
| """Get the default configuration.""" | ||
| return Config( | ||
|
|
@@ -289,6 +382,12 @@ def get_default_config() -> Config: | |
| ) | ||
|
|
||
|
|
||
| def _write_default_config_file(config_file: Path) -> None: | ||
| """Write the default config file with comments and examples.""" | ||
| config_file.parent.mkdir(parents=True, exist_ok=True) | ||
| config_file.write_text(_DEFAULT_CONFIG_TEMPLATE, encoding="utf-8") | ||
|
|
||
|
|
||
| def load_config(config_file: Path | None = None) -> Config: | ||
| """ | ||
| Load configuration from config file. | ||
|
|
@@ -315,9 +414,30 @@ def load_config(config_file: Path | None = None) -> Config: | |
| _migrate_json_config_to_toml() | ||
|
|
||
| if not config_file.exists(): | ||
| config = get_default_config() | ||
| logger.debug("No config file found, creating default config: {config}", config=config) | ||
| save_config(config, config_file) | ||
| logger.debug("No config file found, creating default config at: {file}", file=config_file) | ||
| if config_file.suffix.lower() == ".json": | ||
| # Write a valid JSON default so subsequent loads via json.loads succeed. | ||
| default_config = get_default_config() | ||
| config_file.parent.mkdir(parents=True, exist_ok=True) | ||
| config_file.write_text( | ||
| json.dumps( | ||
| default_config.model_dump(mode="json", exclude_none=True), | ||
| ensure_ascii=False, | ||
| indent=2, | ||
| ), | ||
| encoding="utf-8", | ||
| ) | ||
| config = default_config | ||
| else: | ||
| _write_default_config_file(config_file) | ||
| try: | ||
| data = tomlkit.loads(_DEFAULT_CONFIG_TEMPLATE) | ||
| config = Config.model_validate(data) | ||
| except (TOMLKitError, ValidationError) as e: | ||
| # This should never happen because the template is static and tested, | ||
| # but fall back to the plain default config if it does. | ||
| logger.warning("Default config template failed validation: {error}", error=e) | ||
| config = get_default_config() | ||
| config.is_from_default_location = is_default_config_file | ||
| config.source_file = config_file | ||
| return config | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import fnmatch | ||
| import re | ||
| import uuid | ||
| from collections.abc import Callable | ||
| from typing import Literal | ||
|
|
@@ -52,6 +54,9 @@ def rejection_error(self) -> ToolRejectedError: | |
| return ToolRejectedError() | ||
|
|
||
|
|
||
| _GLOB_SPECIAL_RE = re.compile(r"[*?\[\]]") | ||
|
|
||
|
|
||
| class ApprovalState: | ||
| def __init__( | ||
| self, | ||
|
|
@@ -60,10 +65,32 @@ def __init__( | |
| on_change: Callable[[], None] | None = None, | ||
| ): | ||
| self.yolo = yolo | ||
| self.auto_approve_actions: set[str] = auto_approve_actions or set() | ||
| """Set of action names that should automatically be approved.""" | ||
| # Separate exact action names from glob patterns so that fnmatch is | ||
| # only applied to entries that actually contain glob-special characters. | ||
| self._auto_approve_exact: set[str] = set() | ||
| self._auto_approve_patterns: list[str] = [] | ||
| for entry in (auto_approve_actions or set()): | ||
| if _GLOB_SPECIAL_RE.search(entry): | ||
| self._auto_approve_patterns.append(entry) | ||
|
Comment on lines
+73
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This reclassification treats any persisted action containing Useful? React with 👍 / 👎. |
||
| else: | ||
| self._auto_approve_exact.add(entry) | ||
| self._on_change = on_change | ||
|
|
||
| def is_auto_approved(self, action: str) -> bool: | ||
| """Return True if *action* matches an exact name or a glob pattern.""" | ||
| if action in self._auto_approve_exact: | ||
| return True | ||
| return any(fnmatch.fnmatch(action, p) for p in self._auto_approve_patterns) | ||
|
|
||
| def add_auto_approve_action(self, action: str) -> None: | ||
| """Add *action* as an exact auto-approve name (not a glob pattern).""" | ||
| self._auto_approve_exact.add(action) | ||
|
|
||
| @property | ||
| def auto_approve_actions(self) -> set[str]: | ||
| """All auto-approve entries (exact names + glob patterns) for persistence.""" | ||
| return self._auto_approve_exact | set(self._auto_approve_patterns) | ||
|
|
||
| def notify_change(self) -> None: | ||
| if self._on_change is not None: | ||
| self._on_change() | ||
|
|
@@ -142,7 +169,7 @@ async def request( | |
| ) | ||
| return ApprovalResult(approved=True) | ||
|
|
||
| if action in self._state.auto_approve_actions: | ||
| if self._state.is_auto_approved(action): | ||
| from kimi_cli.telemetry import track | ||
|
|
||
| track( | ||
|
|
@@ -195,7 +222,7 @@ async def request( | |
| tool_name=tool_call.function.name, | ||
| approval_mode="manual", | ||
| ) | ||
| self._state.auto_approve_actions.add(action) | ||
| self._state.add_auto_approve_action(action) | ||
| self._state.notify_change() | ||
| for pending in self._runtime.list_pending(): | ||
| if pending.action == action: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Config template hardcodes stale
max_steps_per_turn = 500instead of the current default1000The
_DEFAULT_CONFIG_TEMPLATEatsrc/kimi_cli/config.py:348setsmax_steps_per_turn = 500, but theLoopControlmodel default was already bumped to1000atsrc/kimi_cli/config.py:79. The CHANGELOG even documents this change ("Raise defaultmax_steps_per_turnfrom 500 to 1000"), and the docs atdocs/en/configuration/config-files.md:73show1000. Every new user who gets a freshly generated config file will silently receive the old, lower limit. Additionally, the new testtest_load_config_creates_template_with_comments(tests/core/test_config.py:99-102) assertsconfig.model_dump(...) == get_default_config().model_dump(), which will fail because the template parses to500whileget_default_config()yields1000.Was this helpful? React with 👍 or 👎 to provide feedback.