mindcode 0.3.2__py3-none-any.whl → 0.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mindcode/_version.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """mindcode 版本与品牌信息。"""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.5.2"
4
4
 
5
5
  __logo__ = r"""
6
6
  [bold cyan] _ _[/bold cyan]
@@ -0,0 +1,5 @@
1
+ """mindcode ACP agent adapter (see docs/acp-layering.md)."""
2
+
3
+ from .agent import PROTOCOL_VERSION, build_harness
4
+
5
+ __all__ = ["PROTOCOL_VERSION", "build_harness"]
@@ -0,0 +1,70 @@
1
+ """ACP agent entry point: ``python -m mindcode.acp_agent``.
2
+
3
+ stdio transport only. stdout carries ACP JSON-RPC frames exclusively; all
4
+ logging is routed to stderr (see docs/acp-layering.md §5 rule 1).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import asyncio
11
+ import logging
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from acp import run_agent
16
+
17
+ from .agent import _AgentHarness
18
+
19
+
20
+ def _setup_logging(verbose: bool) -> None:
21
+ logging.basicConfig(
22
+ stream=sys.stderr,
23
+ level=logging.DEBUG if verbose else logging.INFO,
24
+ format="%(asctime)s %(name)s %(levelname)s %(message)s",
25
+ )
26
+ # 第三方库的 INFO 噪音也压到 stderr,绝不污染 stdout
27
+ logging.getLogger().setLevel(logging.DEBUG if verbose else logging.INFO)
28
+
29
+
30
+ def main(argv: list[str] | None = None) -> int:
31
+ parser = argparse.ArgumentParser(
32
+ prog="python -m mindcode.acp_agent",
33
+ description="Run the mindcode ACP agent over stdio (ACP v1).",
34
+ )
35
+ parser.add_argument(
36
+ "--cwd",
37
+ default=None,
38
+ help="Default workspace used when a client connects without a cwd",
39
+ )
40
+ parser.add_argument("--verbose", action="store_true", help="stderr debug logs")
41
+ args = parser.parse_args(argv)
42
+
43
+ _setup_logging(args.verbose)
44
+
45
+ async def _serve() -> int:
46
+ harness_holder: list[_AgentHarness] = []
47
+
48
+ def factory(conn: object) -> _AgentHarness:
49
+ harness = _AgentHarness(
50
+ conn, # type: ignore[arg-type]
51
+ cwd=Path(args.cwd).expanduser() if args.cwd else None,
52
+ )
53
+ harness_holder.append(harness)
54
+ return harness
55
+
56
+ try:
57
+ await run_agent(factory)
58
+ finally:
59
+ if harness_holder:
60
+ await harness_holder[0].close()
61
+ return 0
62
+
63
+ try:
64
+ return asyncio.run(_serve())
65
+ except KeyboardInterrupt:
66
+ return 130
67
+
68
+
69
+ if __name__ == "__main__":
70
+ raise SystemExit(main())
@@ -0,0 +1,469 @@
1
+ """ACP agent adapter — exposes mindcode's native session API over ACP v1.
2
+
3
+ Layering (see docs/acp-layering.md):
4
+
5
+ engine (mindagent-core) → native session API (this file's only bus)
6
+ → ACP stdio JSON-RPC (acp SDK, ``python -m mindcode.acp_agent``)
7
+ → TUI Shell (untouched, peer frontend)
8
+
9
+ Only the five native ports are used: create session / run turn / cancel /
10
+ approval callback / event stream. Hard rules honoured here:
11
+
12
+ - stdout carries protocol frames only; every log line goes to stderr.
13
+ - one run per session at a time; a second ``session/prompt`` is rejected.
14
+ - provider/model/approval configuration always comes from ``ConfigStore``
15
+ (fresh ``read()`` per session); clients cannot widen the SessionPolicy.
16
+ - unimplemented stable methods (``session/load``, ``session/set_mode``)
17
+ answer with a JSON-RPC error instead of pretending support.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import logging
24
+ import time
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from acp import AgentSideConnection, helpers, RequestError
30
+ from acp.schema import (
31
+ AgentCapabilities,
32
+ InitializeResponse,
33
+ NewSessionResponse,
34
+ PermissionOption,
35
+ PromptCapabilities,
36
+ PromptResponse,
37
+ RequestPermissionResponse,
38
+ TextContentBlock,
39
+ ToolCallStart,
40
+ )
41
+
42
+ from mindagent.core import (
43
+ AgentSession,
44
+ AgentRuntime,
45
+ EventType,
46
+ LocalConversationStore,
47
+ RunOutcome,
48
+ SessionEnvironment,
49
+ SessionEventType,
50
+ SessionPolicy,
51
+ )
52
+ from ..approval import describe_action
53
+ from ..config import ConfigError, ConfigStore, ProviderConfig
54
+ from ..policy import ApprovalMode
55
+ from ..runtime import build_runtime
56
+
57
+ logger = logging.getLogger("mindcode.acp_agent")
58
+
59
+ PROTOCOL_VERSION = 1 # stable ACP v1; see docs/acp-layering.md §5
60
+
61
+ _PERMISSION_TIMEOUT_S = 120.0
62
+
63
+
64
+ def _stop_reason(result: Any) -> str:
65
+ """Map a native run result to an ACP v1 stop reason (layering §3)."""
66
+ outcome = getattr(result, "outcome", None)
67
+ if outcome is RunOutcome.CANCELLED:
68
+ return "cancelled"
69
+ if outcome in (RunOutcome.ERROR, RunOutcome.PARTIAL):
70
+ return "refusal"
71
+ return "end_turn"
72
+
73
+
74
+ def _tool_kind(action_name: str) -> str:
75
+ if action_name.startswith("file_read"):
76
+ return "read"
77
+ if action_name in ("file_write", "file_edit"):
78
+ return "edit"
79
+ if action_name.startswith("exec_") or "command" in action_name:
80
+ return "execute"
81
+ if "search" in action_name or "glob" in action_name or "grep" in action_name:
82
+ return "search"
83
+ return "other"
84
+
85
+
86
+ def _extract_text(prompt: list[Any]) -> str:
87
+ parts: list[str] = []
88
+ for block in prompt:
89
+ if isinstance(block, TextContentBlock):
90
+ parts.append(block.text)
91
+ else:
92
+ raise RequestError.invalid_params(
93
+ "only text content blocks are supported"
94
+ )
95
+ return "".join(parts)
96
+
97
+
98
+ @dataclass
99
+ class _ToolCallTracker:
100
+ """ACTION_STARTED → OBSERVATION_CREATED bookkeeping per session."""
101
+
102
+ by_action: dict[str, ToolCallStart] = field(default_factory=dict)
103
+
104
+
105
+ class _ManagedSession:
106
+ """Native session plus the per-session serialisation state."""
107
+
108
+ def __init__(self, session_id: str, session: AgentSession, runtime: AgentRuntime):
109
+ self.session_id = session_id
110
+ self.session = session
111
+ self.runtime = runtime
112
+ self.turn_lock = asyncio.Lock()
113
+ self.tool_calls = _ToolCallTracker()
114
+
115
+
116
+ class _AgentHarness:
117
+ """Owns ACP sessions; each maps to one native ``AgentSession``."""
118
+
119
+ def __init__(
120
+ self,
121
+ conn: AgentSideConnection,
122
+ *,
123
+ config_store: ConfigStore | None = None,
124
+ cwd: Path | None = None,
125
+ subagents: bool = False,
126
+ permission_timeout_s: float = _PERMISSION_TIMEOUT_S,
127
+ ) -> None:
128
+ self._conn = conn
129
+ self._config_store = config_store or ConfigStore()
130
+ self._cwd = cwd or Path.cwd()
131
+ self._subagents = subagents
132
+ self._permission_timeout_s = permission_timeout_s
133
+ self._sessions: dict[str, _ManagedSession] = {}
134
+ self._new_session_lock = asyncio.Lock()
135
+
136
+ # ----- ACP agent methods (stable surface only) -----
137
+
138
+ async def initialize(
139
+ self,
140
+ protocol_version: int,
141
+ client_capabilities: Any | None = None,
142
+ client_info: Any | None = None,
143
+ **kwargs: Any,
144
+ ) -> InitializeResponse:
145
+ logger.info(
146
+ "initialize: client=%s protocol=%s",
147
+ getattr(client_info, "name", "unknown"),
148
+ protocol_version,
149
+ )
150
+ return InitializeResponse(
151
+ protocol_version=PROTOCOL_VERSION,
152
+ agent_capabilities=AgentCapabilities(
153
+ load_session=False,
154
+ prompt_capabilities=PromptCapabilities(),
155
+ ),
156
+ )
157
+
158
+ async def new_session(
159
+ self,
160
+ cwd: str,
161
+ mcp_servers: list[Any] | None = None,
162
+ **kwargs: Any,
163
+ ) -> NewSessionResponse:
164
+ if mcp_servers:
165
+ raise RequestError.invalid_request(
166
+ "mcpServers are not supported by mindcode yet"
167
+ )
168
+ async with self._new_session_lock:
169
+ workspace = Path(cwd).expanduser() if cwd else self._cwd
170
+ try:
171
+ resolved = self._config_store.resolve()
172
+ except ConfigError as exc:
173
+ raise RequestError.internal_error(str(exc)) from exc
174
+ store = LocalConversationStore(
175
+ self._config_store.root / "sessions"
176
+ )
177
+ runtime = build_runtime(
178
+ workspace,
179
+ provider_config=resolved,
180
+ approve_mode=ApprovalMode(resolved.approval_mode),
181
+ stream=True,
182
+ event_handler=None,
183
+ approval_callback=self._make_approval(resolved),
184
+ )
185
+ try:
186
+ session = runtime.create_session(
187
+ environment=SessionEnvironment(
188
+ workspace_root=workspace,
189
+ working_directory=workspace,
190
+ ),
191
+ policy=SessionPolicy(
192
+ allow_write=True,
193
+ allow_dangerous=False,
194
+ max_parallel_actions=1,
195
+ ),
196
+ conversation_store=store,
197
+ )
198
+ except Exception:
199
+ await runtime.close(close_dependencies=True)
200
+ raise
201
+ session_id = session.session_id
202
+ self._sessions[session_id] = _ManagedSession(
203
+ session_id, session, runtime
204
+ )
205
+ logger.info(
206
+ "session %s created (workspace=%s provider=%s model=%s approval=%s)",
207
+ session_id,
208
+ workspace,
209
+ resolved.name,
210
+ resolved.model,
211
+ resolved.approval_mode,
212
+ )
213
+ return NewSessionResponse(session_id=session_id)
214
+
215
+ async def prompt(
216
+ self,
217
+ session_id: str,
218
+ prompt: list[Any],
219
+ **kwargs: Any,
220
+ ) -> PromptResponse:
221
+ managed = self._managed(session_id)
222
+ text = _extract_text(prompt)
223
+ if not text.strip():
224
+ raise RequestError.invalid_params("prompt must contain text")
225
+ if managed.turn_lock.locked():
226
+ # 铁律 2:每 session 串行,第二轮直接拒收(参照 COMMAND_REJECTED 语义)。
227
+ raise RequestError.invalid_request(
228
+ f"session {session_id} already has an active run"
229
+ )
230
+ async with managed.turn_lock:
231
+ return await self._run_turn(managed, text)
232
+
233
+ async def cancel(self, session_id: str, **kwargs: Any) -> None:
234
+ managed = self._managed(session_id)
235
+ run_id = managed.session.active_run_id
236
+ if run_id:
237
+ logger.info("cancel session=%s run=%s", session_id, run_id)
238
+ await managed.session.cancel_run(run_id)
239
+
240
+ async def set_session_mode(
241
+ self, session_id: str, mode_id: str, **kwargs: Any
242
+ ) -> None:
243
+ raise RequestError.invalid_request(
244
+ "session/set_mode is not supported yet (planned P2)"
245
+ )
246
+
247
+ async def load_session(self, *args: Any, **kwargs: Any) -> None:
248
+ raise RequestError.invalid_request(
249
+ "session/load is not supported yet (planned P2)"
250
+ )
251
+
252
+ # ----- internals -----
253
+
254
+ def _managed(self, session_id: str) -> _ManagedSession:
255
+ managed = self._sessions.get(session_id)
256
+ if managed is None:
257
+ raise RequestError.resource_not_found(session_id)
258
+ return managed
259
+
260
+ async def close(self) -> None:
261
+ for managed in list(self._sessions.values()):
262
+ try:
263
+ await managed.runtime.close(close_dependencies=True)
264
+ except Exception:
265
+ logger.exception("failed closing session %s", managed.session_id)
266
+ self._sessions.clear()
267
+
268
+ # ----- approval bridge (native port 4 → session/request_permission) -----
269
+
270
+ def _make_approval(self, provider_config: ProviderConfig) -> Any:
271
+ async def approve(context: Any, action: Any) -> bool:
272
+ managed = self._managed(context.session_id)
273
+ summary = describe_action(action)
274
+ started = helpers.start_tool_call(
275
+ tool_call_id=f"perm-{int(time.time() * 1000)}",
276
+ title=summary,
277
+ kind=_tool_kind(getattr(action, "name", "")),
278
+ status="pending",
279
+ )
280
+ await self._notify_update(managed, started)
281
+ options = [
282
+ PermissionOption(
283
+ option_id="allow", name="Allow", kind="allow_once"
284
+ ),
285
+ PermissionOption(
286
+ option_id="reject", name="Reject", kind="reject_once"
287
+ ),
288
+ ]
289
+ try:
290
+ response = await asyncio.wait_for(
291
+ self._conn.request_permission(
292
+ session_id=managed.session_id,
293
+ tool_call=started,
294
+ options=options,
295
+ ),
296
+ timeout=self._permission_timeout_s,
297
+ )
298
+ except asyncio.TimeoutError:
299
+ logger.warning(
300
+ "permission timed out after %.0fs → deny (unattended)",
301
+ self._permission_timeout_s,
302
+ )
303
+ return False
304
+ outcome = response.outcome
305
+ option_id = getattr(outcome, "option_id", None)
306
+ if getattr(outcome, "outcome", None) == "cancelled":
307
+ return False
308
+ return option_id == "allow"
309
+
310
+ return approve
311
+
312
+ # ----- event-stream translation (native port 5 → session/update) -----
313
+
314
+ async def _notify_update(self, managed: _ManagedSession, update: Any) -> None:
315
+ await self._conn.session_update(
316
+ session_id=managed.session_id,
317
+ update=update,
318
+ )
319
+
320
+ async def _run_turn(self, managed: _ManagedSession, text: str) -> PromptResponse:
321
+ session = managed.session
322
+ checkpoint = session.pending_checkpoint
323
+ if checkpoint is None:
324
+ command_id = await session.submit_run(text)
325
+ else:
326
+ command_id = await session.continue_run(
327
+ checkpoint.checkpoint_id, user_input=text
328
+ )
329
+ while True:
330
+ event = await session.next_event()
331
+ if event.event_type == SessionEventType.RUN_ACCEPTED:
332
+ continue
333
+ if event.event_type == SessionEventType.AGENT_EVENT:
334
+ agent_event = event.agent_event
335
+ if agent_event is None:
336
+ continue
337
+ await self._on_agent_event(managed, agent_event)
338
+ continue
339
+ if event.event_type in (
340
+ SessionEventType.APPROVAL_DECIDED,
341
+ SessionEventType.USER_MESSAGE_APPENDED,
342
+ ):
343
+ continue
344
+ if event.event_type == SessionEventType.RUN_INTERRUPTED:
345
+ return PromptResponse(stop_reason="cancelled")
346
+ if event.event_type == SessionEventType.RUN_COMPLETED:
347
+ if event.correlation_id and event.correlation_id != command_id:
348
+ # 中断跟随:旧一轮收尾后新一轮接管,继续等新一轮。
349
+ command_id = event.correlation_id
350
+ continue
351
+ result = event.payload.get("result")
352
+ if result is None:
353
+ logger.error(
354
+ "run %s failed: %s",
355
+ event.run_id,
356
+ event.payload.get("error"),
357
+ )
358
+ return PromptResponse(stop_reason="refusal")
359
+ return PromptResponse(stop_reason=_stop_reason(result))
360
+ if event.event_type == SessionEventType.COMMAND_REJECTED:
361
+ logger.error(
362
+ "command rejected: %s",
363
+ event.payload.get("reason", "unknown"),
364
+ )
365
+ return PromptResponse(stop_reason="refusal")
366
+ if event.event_type == SessionEventType.USER_INPUT_REQUIRED:
367
+ logger.warning(
368
+ "USER_INPUT_REQUIRED without approval bridge; cancelling run"
369
+ )
370
+ run_id = event.run_id or session.active_run_id
371
+ if run_id:
372
+ await session.cancel_run(run_id)
373
+ continue
374
+
375
+ async def _on_agent_event(self, managed: _ManagedSession, agent_event: Any) -> None:
376
+ event_type = agent_event.event_type
377
+ payload = agent_event.payload or {}
378
+ if event_type == EventType.TEXT_DELTA:
379
+ delta = payload.get("delta", "")
380
+ if delta:
381
+ await self._notify_update(
382
+ managed, helpers.update_agent_message_text(delta)
383
+ )
384
+ elif event_type == EventType.ACTION_STARTED:
385
+ action_name = payload.get("action_name", "tool")
386
+ arguments = payload.get("arguments", {}) or {}
387
+ title = _describe_started(action_name, arguments)
388
+ started = helpers.start_tool_call(
389
+ tool_call_id=_tool_call_key(agent_event),
390
+ title=title,
391
+ kind=_tool_kind(action_name),
392
+ status="in_progress",
393
+ raw_input=arguments,
394
+ )
395
+ managed.tool_calls.by_action[_tool_call_key(agent_event)] = started
396
+ await self._notify_update(managed, started)
397
+ elif event_type == EventType.OBSERVATION_CREATED:
398
+ key = _tool_call_key(agent_event)
399
+ started = managed.tool_calls.by_action.pop(key, None)
400
+ ok = bool(payload.get("ok", False))
401
+ output = _observation_text(payload)
402
+ if started is not None:
403
+ progress = helpers.update_tool_call(
404
+ started.tool_call_id,
405
+ status="completed" if ok else "failed",
406
+ content=[helpers.tool_content(helpers.text_block(output))],
407
+ raw_output=payload.get("result"),
408
+ )
409
+ await self._notify_update(managed, progress)
410
+ elif event_type == EventType.ERROR_CREATED:
411
+ message = str(payload.get("error", "unknown error"))
412
+ logger.error("agent error: %s", message)
413
+ elif event_type == EventType.DECISION_CREATED:
414
+ # reasoning summary: forward as thought chunk when present
415
+ summary = payload.get("reasoning_summary")
416
+ if summary:
417
+ await self._notify_update(
418
+ managed, helpers.update_agent_thought_text(str(summary))
419
+ )
420
+ # 其他事件类型(COMPLETION_REJECTED 等)记日志即可
421
+ else:
422
+ logger.debug("untranslated agent event: %s", event_type)
423
+
424
+
425
+ def _tool_call_key(agent_event: Any) -> str:
426
+ action_id = getattr(agent_event, "action_id", None) or getattr(
427
+ agent_event, "action_batch_id", None
428
+ )
429
+ if action_id:
430
+ return str(action_id)
431
+ payload = agent_event.payload or {}
432
+ return str(payload.get("action_id") or payload.get("batch_id") or "tool")
433
+
434
+
435
+ def _describe_started(action_name: str, arguments: dict[str, Any]) -> str:
436
+ target = arguments.get("path") or arguments.get("command") or ""
437
+ title = f"{action_name} {target}".strip()
438
+ return title if title else action_name
439
+
440
+
441
+ def _observation_text(payload: dict[str, Any]) -> str:
442
+ if payload.get("ok", False):
443
+ result = payload.get("result")
444
+ if isinstance(result, dict):
445
+ diff = result.get("mindcode_diff")
446
+ if diff:
447
+ return str(diff)
448
+ output = result.get("output") or result.get("content")
449
+ if output:
450
+ return str(output)
451
+ return "ok"
452
+ error = payload.get("error") or "failed"
453
+ return str(error)
454
+
455
+
456
+ def build_harness(
457
+ conn: AgentSideConnection,
458
+ *,
459
+ config_store: ConfigStore | None = None,
460
+ cwd: Path | None = None,
461
+ subagents: bool = False,
462
+ ) -> _AgentHarness:
463
+ """Factory used by ``__main__`` and tests."""
464
+ return _AgentHarness(
465
+ conn,
466
+ config_store=config_store,
467
+ cwd=cwd,
468
+ subagents=subagents,
469
+ )
mindcode/approval.py CHANGED
@@ -90,14 +90,14 @@ def make_shell_approval(
90
90
  result = await chooser(
91
91
  f"允许执行? {summary}",
92
92
  [("allow", "允许"), ("deny", "拒绝")],
93
- "deny",
93
+ "allow",
94
94
  )
95
95
  return result == "allow"
96
96
  # Lazy import:非交互进程构造 policy 时不引入 prompt_toolkit。
97
97
  from .cli.shell.menu import prompt_yes_no
98
98
 
99
99
  return await prompt_yes_no(
100
- f"允许执行 {summary}?", default_yes=False
100
+ f"允许执行 {summary}?", default_yes=True
101
101
  )
102
102
 
103
103
  return _approve
@@ -119,12 +119,12 @@ def make_shell_file_access_approval(
119
119
  chooser = get_chooser()
120
120
  if chooser is not None:
121
121
  result = await chooser(
122
- title, [("allow", "允许此次只读访问"), ("deny", "拒绝")], "deny",
122
+ title, [("allow", "允许此次只读访问"), ("deny", "拒绝")], "allow",
123
123
  )
124
124
  approved = result == "allow"
125
125
  else:
126
126
  from .cli.shell.menu import prompt_yes_no
127
- approved = await prompt_yes_no(title, default_yes=False)
127
+ approved = await prompt_yes_no(title, default_yes=True)
128
128
  return ExternalFileReadDecision(request_id=request.request_id, approved=approved)
129
129
 
130
130
  return approve
mindcode/bench/runner.py CHANGED
@@ -12,7 +12,7 @@ from mindagent.core.artifact import (
12
12
  write_artifact_text,
13
13
  )
14
14
 
15
- from ..config import load_config
15
+ from ..config import ConfigStore
16
16
  from ..policy import ApprovalMode
17
17
  from ..runtime import build_runtime
18
18
  from .adapters import built_in_adapters
@@ -115,13 +115,15 @@ class MindcodeBenchmarkRunner:
115
115
  runtime = None
116
116
 
117
117
  try:
118
- mindcode_config = load_config()
119
- provider_config = mindcode_config.get_provider(config.provider)
118
+ provider_config = ConfigStore().resolve(
119
+ provider_override=config.provider,
120
+ model_override=config.model,
121
+ approval_override=config.approve_mode,
122
+ )
120
123
  runtime = build_runtime(
121
124
  workspace,
122
125
  provider_config=provider_config,
123
126
  approve_mode=ApprovalMode(config.approve_mode),
124
- model=config.model,
125
127
  stream=False,
126
128
  event_handler=recorder,
127
129
  max_steps=config.max_steps,
mindcode/cli/_shared.py CHANGED
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
 
8
8
  from rich.console import Console
9
9
 
10
- from ..config import CONFIG_FILE
10
+ from ..config import CONFIG_DIR, ConfigStore
11
11
 
12
12
 
13
13
  class _StdoutProxy:
@@ -36,8 +36,6 @@ def mask_key(key: str) -> str:
36
36
 
37
37
  def find_config_file() -> Path:
38
38
  """返回配置文件路径(首次访问时自动 bootstrap)。"""
39
- if not CONFIG_FILE.exists():
40
- from ..config import load_config
41
-
42
- load_config()
43
- return CONFIG_FILE
39
+ store = ConfigStore(CONFIG_DIR)
40
+ store.bootstrap()
41
+ return store.config_path
@@ -31,7 +31,7 @@ from ...bench.runner import (
31
31
  run_benchmark,
32
32
  )
33
33
  from ...bench.schema import BenchmarkRunConfig
34
- from ...config import load_config
34
+ from ...config import ConfigStore
35
35
  from ...policy import ApprovalMode
36
36
  from .._shared import console
37
37
 
@@ -70,8 +70,8 @@ def run(
70
70
  help="覆盖 default_provider",
71
71
  ),
72
72
  model: str | None = typer.Option(None, "--model", help="覆盖模型"),
73
- approve_mode: str = typer.Option(
74
- ApprovalMode.FULL_ACCEPT.value,
73
+ approve_mode: str | None = typer.Option(
74
+ None,
75
75
  "--approve-mode",
76
76
  case_sensitive=False,
77
77
  help="权限模式:ask_user 逐项询问,full_accept 全部接受",
@@ -93,18 +93,27 @@ def run(
93
93
  help="每 case 总超时秒数",
94
94
  ),
95
95
  ) -> None:
96
- config = load_config()
97
- provider_name = provider or config.default_provider
98
96
  run_id = f"{suite}-{time.strftime('%Y%m%d-%H%M%S')}"
99
97
  try:
98
+ approval_override = (
99
+ ApprovalMode(approve_mode).value
100
+ if approve_mode is not None
101
+ else None
102
+ )
103
+ resolved = ConfigStore().resolve(
104
+ provider_override=provider,
105
+ model_override=model,
106
+ approval_override=approval_override,
107
+ )
108
+ selected_approve_mode = resolved.approval_mode
100
109
  output_root = Path(output).expanduser()
101
110
  run_output_dir = _run_output_dir(output_root, run_id)
102
111
  run_config = BenchmarkRunConfig(
103
112
  suite=suite,
104
113
  run_id=run_id,
105
- provider=provider_name,
106
- model=model,
107
- approve_mode=ApprovalMode(approve_mode).value,
114
+ provider=resolved.name,
115
+ model=resolved.model,
116
+ approve_mode=selected_approve_mode,
108
117
  workspace_root=run_output_dir / "workspaces",
109
118
  output_root=output_root,
110
119
  limit=limit,