mindcode 0.5.2__tar.gz → 0.5.5__tar.gz

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.
Files changed (102) hide show
  1. {mindcode-0.5.2 → mindcode-0.5.5}/PKG-INFO +6 -6
  2. {mindcode-0.5.2 → mindcode-0.5.5}/README.md +5 -5
  3. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/_version.py +1 -1
  4. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/acp_agent/__main__.py +20 -0
  5. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/acp_agent/agent.py +89 -14
  6. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/approval.py +4 -4
  7. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/schema.py +5 -3
  8. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/__init__.py +10 -3
  9. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/commands/bench.py +15 -3
  10. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/commands/chat.py +36 -6
  11. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/commands/shell.py +42 -6
  12. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/commands/task.py +30 -12
  13. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/shell/input_pipeline.py +13 -0
  14. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/shell/repl.py +169 -44
  15. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/shell/slash.py +6 -24
  16. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/shell/startup.py +0 -8
  17. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/shell/tui.py +255 -83
  18. mindcode-0.5.5/mindcode/config/settings.toml +41 -0
  19. mindcode-0.5.5/mindcode/config/system.md +25 -0
  20. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/config.py +180 -4
  21. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/policy.py +7 -7
  22. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/render.py +17 -17
  23. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/runtime.py +146 -38
  24. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/tasking.py +8 -5
  25. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode.egg-info/PKG-INFO +6 -6
  26. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode.egg-info/SOURCES.txt +2 -0
  27. {mindcode-0.5.2 → mindcode-0.5.5}/pyproject.toml +4 -1
  28. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_acp_agent.py +139 -3
  29. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_approval.py +9 -9
  30. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_bench.py +3 -15
  31. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_cli_commands.py +95 -0
  32. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_config.py +43 -1
  33. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_diff_capture.py +2 -2
  34. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_exec_command_guard.py +1 -1
  35. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_external_file_approval.py +4 -4
  36. mindcode-0.5.5/tests/test_provider_retry_defaults.py +185 -0
  37. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_render_exec.py +3 -3
  38. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_shell_channels.py +61 -55
  39. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_shell_interrupt.py +72 -19
  40. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_shell_session.py +1 -1
  41. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_shell_tui.py +155 -24
  42. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_subagents.py +1 -1
  43. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_system_prompt.py +32 -0
  44. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_tasking.py +2 -0
  45. mindcode-0.5.2/tests/test_provider_retry_defaults.py +0 -80
  46. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/__init__.py +0 -0
  47. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/__main__.py +0 -0
  48. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/acp_agent/__init__.py +0 -0
  49. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/__init__.py +0 -0
  50. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/adapters/__init__.py +0 -0
  51. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/adapters/base.py +0 -0
  52. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/adapters/mini_bfcl.py +0 -0
  53. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/adapters/mini_gaia.py +0 -0
  54. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/adapters/mini_terminal.py +0 -0
  55. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/adapters/swe_bench.py +0 -0
  56. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/adapters/terminal_bench.py +0 -0
  57. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/fake_tools.py +0 -0
  58. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/jsonutil.py +0 -0
  59. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/paths.py +0 -0
  60. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/predictions.py +0 -0
  61. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/report.py +0 -0
  62. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/runner.py +0 -0
  63. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/scorers/base.py +0 -0
  64. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/scorers/composite.py +0 -0
  65. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/scorers/exact.py +0 -0
  66. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/scorers/json_call.py +0 -0
  67. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/scorers/swe.py +0 -0
  68. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/scorers/terminal.py +0 -0
  69. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/scorers/terminal_bench.py +0 -0
  70. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/trace.py +0 -0
  71. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/bench/workspace.py +0 -0
  72. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/_shared.py +0 -0
  73. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/commands/__init__.py +0 -0
  74. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/commands/config_cmd.py +0 -0
  75. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/commands/remote.py +0 -0
  76. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/commands/status.py +0 -0
  77. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/commands/terminal.py +0 -0
  78. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/shell/__init__.py +0 -0
  79. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/shell/completion.py +0 -0
  80. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/cli/shell/menu.py +0 -0
  81. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/mcp.py +0 -0
  82. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/remote.py +0 -0
  83. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/skills.py +0 -0
  84. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/subagents.py +0 -0
  85. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/terminal/__init__.py +0 -0
  86. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/terminal/__main__.py +0 -0
  87. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode/terminal_core.py +0 -0
  88. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode.egg-info/dependency_links.txt +0 -0
  89. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode.egg-info/entry_points.txt +0 -0
  90. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode.egg-info/requires.txt +0 -0
  91. {mindcode-0.5.2 → mindcode-0.5.5}/mindcode.egg-info/top_level.txt +0 -0
  92. {mindcode-0.5.2 → mindcode-0.5.5}/setup.cfg +0 -0
  93. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_policy_choice.py +0 -0
  94. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_policy_timeout.py +0 -0
  95. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_reasoner_compat.py +0 -0
  96. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_remote.py +0 -0
  97. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_shell_completion.py +0 -0
  98. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_shell_slash_priority.py +0 -0
  99. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_skills_mcp.py +0 -0
  100. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_slash_resume.py +0 -0
  101. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_task_resume.py +0 -0
  102. {mindcode-0.5.2 → mindcode-0.5.5}/tests/test_terminal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mindcode
3
- Version: 0.5.2
3
+ Version: 0.5.5
4
4
  Summary: codex-style interactive coding agent on top of mindagent
5
5
  Author: mindcode
6
6
  License: MIT
@@ -35,7 +35,7 @@ Requires-Dist: prompt-toolkit>=3.0.30
35
35
  - **可选 Subagent 模式**:`mindcode --subagents` 启用受限的 master/coder 协作;master 仅能委派,coder 才能访问 workspace
36
36
  - 全屏 TUI:顶部 logo/会话信息、可滚动 Markdown 消息区、固定带边框多行输入框与状态栏
37
37
  - 顶部信息栏显示本会话累计 token 消耗;provider 未返回 usage 时显示 `tokens —`
38
- - Rich 富渲染:Markdown、彩色工具面板、file_edit diff 高亮和流式 LLM 输出
38
+ - Rich 富渲染:Markdown、彩色工具面板、edit diff 高亮和流式 LLM 输出
39
39
  - TUI 内选择菜单:`/models`、`/permissions` 用方向键选择
40
40
  - REPL 权限模式:`ask_user`(默认,写入和危险操作逐项询问)/ `full_accept`(接受权限上限内的操作风险)
41
41
  - 会话持久化与恢复:基于 `LocalConversationStore`,存到 `~/.cache/mindcode/sessions/`
@@ -168,7 +168,7 @@ mindcode --subagents "审查当前改动并运行相关测试"
168
168
  ```bash
169
169
  mindcode # 默认进 shell
170
170
  › 在 workspace 创建 hello.txt 写入 hi
171
- › file_edit (create) hello.txt [WRITE]
171
+ › edit (create) hello.txt [WRITE]
172
172
  ask_user 将在执行前询问;也可切换到 /permissions full_accept
173
173
  ┌─ diff: hello.txt ──────────────────────┐
174
174
  │ +hi │
@@ -333,15 +333,15 @@ mindcode/
333
333
  持久化;运行结束会清理未消费的授权,不自动新增磁盘审计文件。
334
334
  - 普通问答使用 `USER_INPUT_REQUIRED` 与 `submit_user_input()`;旧 `approve_run()`
335
335
  因未绑定授权请求而被拒绝。
336
- - `exec_command` 只限制 `cwd`,不会隔离子进程访问的文件。`sed`、`find`、`git diff`
336
+ - `command` 只限制 `cwd`,不会隔离子进程访问的文件。`sed`、`find`、`git diff`
337
337
  等命令保守按写风险审批,具体范围见
338
338
  [策略与权限](../../docs/guides/policies-and-approval.md)。
339
339
 
340
340
  ## 局限
341
341
 
342
- - 不支持 `/undo`:`exec_command` 的副作用可能不可逆,回滚复杂。
342
+ - 不支持 `/undo`:`command` 的副作用可能不可逆,回滚复杂。
343
343
  - 会话恢复只续聊 user/assistant 历史,不恢复 TaskState / Evidence / pending continuation。
344
344
  - `task resume` 是重新观察 workspace 后创建新 Run,不会重放未确认的旧 Action。
345
345
  - 后台任务目前依赖本机 PID;不提供跨主机监督或精确进程身份恢复。
346
- - `bench run` 第一版仅支持串行(concurrency=1);`exec_command` 无沙箱,
346
+ - `bench run` 第一版仅支持串行(concurrency=1);`command` 无沙箱,
347
347
  SWE-bench / Terminal-Bench 评测依赖本机对应的评测环境。
@@ -19,7 +19,7 @@
19
19
  - **可选 Subagent 模式**:`mindcode --subagents` 启用受限的 master/coder 协作;master 仅能委派,coder 才能访问 workspace
20
20
  - 全屏 TUI:顶部 logo/会话信息、可滚动 Markdown 消息区、固定带边框多行输入框与状态栏
21
21
  - 顶部信息栏显示本会话累计 token 消耗;provider 未返回 usage 时显示 `tokens —`
22
- - Rich 富渲染:Markdown、彩色工具面板、file_edit diff 高亮和流式 LLM 输出
22
+ - Rich 富渲染:Markdown、彩色工具面板、edit diff 高亮和流式 LLM 输出
23
23
  - TUI 内选择菜单:`/models`、`/permissions` 用方向键选择
24
24
  - REPL 权限模式:`ask_user`(默认,写入和危险操作逐项询问)/ `full_accept`(接受权限上限内的操作风险)
25
25
  - 会话持久化与恢复:基于 `LocalConversationStore`,存到 `~/.cache/mindcode/sessions/`
@@ -152,7 +152,7 @@ mindcode --subagents "审查当前改动并运行相关测试"
152
152
  ```bash
153
153
  mindcode # 默认进 shell
154
154
  › 在 workspace 创建 hello.txt 写入 hi
155
- › file_edit (create) hello.txt [WRITE]
155
+ › edit (create) hello.txt [WRITE]
156
156
  ask_user 将在执行前询问;也可切换到 /permissions full_accept
157
157
  ┌─ diff: hello.txt ──────────────────────┐
158
158
  │ +hi │
@@ -317,15 +317,15 @@ mindcode/
317
317
  持久化;运行结束会清理未消费的授权,不自动新增磁盘审计文件。
318
318
  - 普通问答使用 `USER_INPUT_REQUIRED` 与 `submit_user_input()`;旧 `approve_run()`
319
319
  因未绑定授权请求而被拒绝。
320
- - `exec_command` 只限制 `cwd`,不会隔离子进程访问的文件。`sed`、`find`、`git diff`
320
+ - `command` 只限制 `cwd`,不会隔离子进程访问的文件。`sed`、`find`、`git diff`
321
321
  等命令保守按写风险审批,具体范围见
322
322
  [策略与权限](../../docs/guides/policies-and-approval.md)。
323
323
 
324
324
  ## 局限
325
325
 
326
- - 不支持 `/undo`:`exec_command` 的副作用可能不可逆,回滚复杂。
326
+ - 不支持 `/undo`:`command` 的副作用可能不可逆,回滚复杂。
327
327
  - 会话恢复只续聊 user/assistant 历史,不恢复 TaskState / Evidence / pending continuation。
328
328
  - `task resume` 是重新观察 workspace 后创建新 Run,不会重放未确认的旧 Action。
329
329
  - 后台任务目前依赖本机 PID;不提供跨主机监督或精确进程身份恢复。
330
- - `bench run` 第一版仅支持串行(concurrency=1);`exec_command` 无沙箱,
330
+ - `bench run` 第一版仅支持串行(concurrency=1);`command` 无沙箱,
331
331
  SWE-bench / Terminal-Bench 评测依赖本机对应的评测环境。
@@ -1,6 +1,6 @@
1
1
  """mindcode 版本与品牌信息。"""
2
2
 
3
- __version__ = "0.5.2"
3
+ __version__ = "0.5.5"
4
4
 
5
5
  __logo__ = r"""
6
6
  [bold cyan] _ _[/bold cyan]
@@ -14,6 +14,7 @@ from pathlib import Path
14
14
 
15
15
  from acp import run_agent
16
16
 
17
+ from ..policy import ApprovalMode
17
18
  from .agent import _AgentHarness
18
19
 
19
20
 
@@ -37,6 +38,23 @@ def main(argv: list[str] | None = None) -> int:
37
38
  default=None,
38
39
  help="Default workspace used when a client connects without a cwd",
39
40
  )
41
+ parser.add_argument(
42
+ "--approval-mode",
43
+ choices=[mode.value for mode in ApprovalMode],
44
+ default=None,
45
+ help=(
46
+ "Override the approval mode read from config.toml. This is a "
47
+ "host-side launch decision; ACP requests can never change it."
48
+ ),
49
+ )
50
+ parser.add_argument(
51
+ "--allow-dangerous",
52
+ action="store_true",
53
+ help=(
54
+ "Allow ActionRisk.DANGEROUS tools in created sessions. Only for "
55
+ "trusted, attended hosts: it lifts the SessionPolicy ceiling."
56
+ ),
57
+ )
40
58
  parser.add_argument("--verbose", action="store_true", help="stderr debug logs")
41
59
  args = parser.parse_args(argv)
42
60
 
@@ -49,6 +67,8 @@ def main(argv: list[str] | None = None) -> int:
49
67
  harness = _AgentHarness(
50
68
  conn, # type: ignore[arg-type]
51
69
  cwd=Path(args.cwd).expanduser() if args.cwd else None,
70
+ approval_mode=args.approval_mode,
71
+ allow_dangerous=args.allow_dangerous,
52
72
  )
53
73
  harness_holder.append(harness)
54
74
  return harness
@@ -11,8 +11,10 @@ approval callback / event stream. Hard rules honoured here:
11
11
 
12
12
  - stdout carries protocol frames only; every log line goes to stderr.
13
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.
14
+ - provider/model/approval configuration comes from ``ConfigStore`` (fresh
15
+ ``read()`` per session); an ACP *request* can never widen it. Only the host
16
+ that launched this process may override approval/session limits, through
17
+ explicit CLI flags (``--approval-mode``, ``--allow-dangerous``).
16
18
  - unimplemented stable methods (``session/load``, ``session/set_mode``)
17
19
  answer with a JSON-RPC error instead of pretending support.
18
20
  """
@@ -34,7 +36,6 @@ from acp.schema import (
34
36
  PermissionOption,
35
37
  PromptCapabilities,
36
38
  PromptResponse,
37
- RequestPermissionResponse,
38
39
  TextContentBlock,
39
40
  ToolCallStart,
40
41
  )
@@ -43,6 +44,8 @@ from mindagent.core import (
43
44
  AgentSession,
44
45
  AgentRuntime,
45
46
  EventType,
47
+ ExternalFileReadDecision,
48
+ ExternalFileReadRequest,
46
49
  LocalConversationStore,
47
50
  RunOutcome,
48
51
  SessionEnvironment,
@@ -50,7 +53,7 @@ from mindagent.core import (
50
53
  SessionPolicy,
51
54
  )
52
55
  from ..approval import describe_action
53
- from ..config import ConfigError, ConfigStore, ProviderConfig
56
+ from ..config import BUILTIN_SETTINGS, ConfigError, ConfigStore, ProviderConfig
54
57
  from ..policy import ApprovalMode
55
58
  from ..runtime import build_runtime
56
59
 
@@ -58,9 +61,6 @@ logger = logging.getLogger("mindcode.acp_agent")
58
61
 
59
62
  PROTOCOL_VERSION = 1 # stable ACP v1; see docs/acp-layering.md §5
60
63
 
61
- _PERMISSION_TIMEOUT_S = 120.0
62
-
63
-
64
64
  def _stop_reason(result: Any) -> str:
65
65
  """Map a native run result to an ACP v1 stop reason (layering §3)."""
66
66
  outcome = getattr(result, "outcome", None)
@@ -72,9 +72,9 @@ def _stop_reason(result: Any) -> str:
72
72
 
73
73
 
74
74
  def _tool_kind(action_name: str) -> str:
75
- if action_name.startswith("file_read"):
75
+ if action_name.startswith("read"):
76
76
  return "read"
77
- if action_name in ("file_write", "file_edit"):
77
+ if action_name in ("write", "edit"):
78
78
  return "edit"
79
79
  if action_name.startswith("exec_") or "command" in action_name:
80
80
  return "execute"
@@ -123,12 +123,29 @@ class _AgentHarness:
123
123
  config_store: ConfigStore | None = None,
124
124
  cwd: Path | None = None,
125
125
  subagents: bool = False,
126
- permission_timeout_s: float = _PERMISSION_TIMEOUT_S,
126
+ approval_mode: str | None = None,
127
+ allow_dangerous: bool = False,
128
+ permission_timeout_s: float | None = None,
127
129
  ) -> None:
128
130
  self._conn = conn
129
131
  self._config_store = config_store or ConfigStore()
130
132
  self._cwd = cwd or Path.cwd()
131
133
  self._subagents = subagents
134
+ # Explicit launch-time policy, always from the *host* that started
135
+ # this process (CLI flags, i.e. the user's own settings) — never from
136
+ # an ACP request. ``None`` keeps the ConfigStore value.
137
+ self._approval_mode = approval_mode
138
+ self._allow_dangerous = allow_dangerous
139
+ self._permission_timeout_from_config = permission_timeout_s is None
140
+ if permission_timeout_s is None:
141
+ try:
142
+ permission_timeout_s = (
143
+ self._config_store.read()
144
+ .runtime_settings.permission_timeout_s
145
+ )
146
+ except AttributeError:
147
+ # Lightweight test/fake stores may only implement resolve().
148
+ permission_timeout_s = BUILTIN_SETTINGS.permission_timeout_s
132
149
  self._permission_timeout_s = permission_timeout_s
133
150
  self._sessions: dict[str, _ManagedSession] = {}
134
151
  self._new_session_lock = asyncio.Lock()
@@ -171,16 +188,23 @@ class _AgentHarness:
171
188
  resolved = self._config_store.resolve()
172
189
  except ConfigError as exc:
173
190
  raise RequestError.internal_error(str(exc)) from exc
191
+ if self._permission_timeout_from_config:
192
+ self._permission_timeout_s = (
193
+ resolved.runtime_settings.permission_timeout_s
194
+ )
174
195
  store = LocalConversationStore(
175
196
  self._config_store.root / "sessions"
176
197
  )
198
+ effective_mode = self._approval_mode or resolved.approval_mode
199
+ effective_approval = ApprovalMode(effective_mode)
177
200
  runtime = build_runtime(
178
201
  workspace,
179
202
  provider_config=resolved,
180
- approve_mode=ApprovalMode(resolved.approval_mode),
203
+ approve_mode=effective_approval,
181
204
  stream=True,
182
205
  event_handler=None,
183
206
  approval_callback=self._make_approval(resolved),
207
+ file_access_callback=self._make_file_access_approval(),
184
208
  )
185
209
  try:
186
210
  session = runtime.create_session(
@@ -190,7 +214,7 @@ class _AgentHarness:
190
214
  ),
191
215
  policy=SessionPolicy(
192
216
  allow_write=True,
193
- allow_dangerous=False,
217
+ allow_dangerous=self._allow_dangerous,
194
218
  max_parallel_actions=1,
195
219
  ),
196
220
  conversation_store=store,
@@ -203,12 +227,13 @@ class _AgentHarness:
203
227
  session_id, session, runtime
204
228
  )
205
229
  logger.info(
206
- "session %s created (workspace=%s provider=%s model=%s approval=%s)",
230
+ "session %s created (workspace=%s provider=%s model=%s approval=%s allow_dangerous=%s)",
207
231
  session_id,
208
232
  workspace,
209
233
  resolved.name,
210
234
  resolved.model,
211
- resolved.approval_mode,
235
+ effective_mode,
236
+ self._allow_dangerous,
212
237
  )
213
238
  return NewSessionResponse(session_id=session_id)
214
239
 
@@ -309,6 +334,52 @@ class _AgentHarness:
309
334
 
310
335
  return approve
311
336
 
337
+ def _make_file_access_approval(self) -> Any:
338
+ async def approve(
339
+ context: Any,
340
+ request: ExternalFileReadRequest,
341
+ ) -> ExternalFileReadDecision:
342
+ managed = self._managed(context.session_id)
343
+ started = helpers.start_tool_call(
344
+ tool_call_id=f"external-read-{request.request_id}",
345
+ title=f"Read external file {request.path}",
346
+ kind="read",
347
+ status="pending",
348
+ )
349
+ await self._notify_update(managed, started)
350
+ options = [
351
+ PermissionOption(
352
+ option_id="allow", name="Allow once", kind="allow_once"
353
+ ),
354
+ PermissionOption(
355
+ option_id="reject", name="Reject", kind="reject_once"
356
+ ),
357
+ ]
358
+ approved = False
359
+ try:
360
+ response = await asyncio.wait_for(
361
+ self._conn.request_permission(
362
+ session_id=managed.session_id,
363
+ tool_call=started,
364
+ options=options,
365
+ ),
366
+ timeout=self._permission_timeout_s,
367
+ )
368
+ except asyncio.TimeoutError:
369
+ logger.warning(
370
+ "external read permission timed out after %.0fs → deny",
371
+ self._permission_timeout_s,
372
+ )
373
+ else:
374
+ outcome = response.outcome
375
+ approved = (
376
+ getattr(outcome, "outcome", None) != "cancelled"
377
+ and getattr(outcome, "option_id", None) == "allow"
378
+ )
379
+ return ExternalFileReadDecision(request.request_id, approved)
380
+
381
+ return approve
382
+
312
383
  # ----- event-stream translation (native port 5 → session/update) -----
313
384
 
314
385
  async def _notify_update(self, managed: _ManagedSession, update: Any) -> None:
@@ -459,6 +530,8 @@ def build_harness(
459
530
  config_store: ConfigStore | None = None,
460
531
  cwd: Path | None = None,
461
532
  subagents: bool = False,
533
+ approval_mode: str | None = None,
534
+ allow_dangerous: bool = False,
462
535
  ) -> _AgentHarness:
463
536
  """Factory used by ``__main__`` and tests."""
464
537
  return _AgentHarness(
@@ -466,4 +539,6 @@ def build_harness(
466
539
  config_store=config_store,
467
540
  cwd=cwd,
468
541
  subagents=subagents,
542
+ approval_mode=approval_mode,
543
+ allow_dangerous=allow_dangerous,
469
544
  )
@@ -42,17 +42,17 @@ def describe_action(action: ActionRequest) -> str:
42
42
  arguments = (
43
43
  action.arguments if isinstance(action.arguments, dict) else {}
44
44
  )
45
- if name in ("file_edit", "file_write"):
46
- verb = "Edit" if name == "file_edit" else "Write"
45
+ if name in ("edit", "write"):
46
+ verb = "Edit" if name == "edit" else "Write"
47
47
  path = _one_line(arguments.get("path", "?"))
48
48
  return f"{verb} {path} [{risk_label(action.risk)}]"
49
- if name == "exec_command":
49
+ if name == "command":
50
50
  command = arguments.get("command", "")
51
51
  preview = _one_line(command) if isinstance(command, str) else ""
52
52
  if len(preview) > 120:
53
53
  preview = preview[:117] + "..."
54
54
  return f"Command {preview} [{risk_label(action.risk)}]"
55
- if name in ("file_read", "memory_read"):
55
+ if name in ("read", "memory_read"):
56
56
  path = _one_line(arguments.get("path", "?"))
57
57
  return f"Read {path} [{risk_label(action.risk)}]"
58
58
  payload = json.dumps(arguments, ensure_ascii=False, sort_keys=True)
@@ -4,6 +4,8 @@ from dataclasses import dataclass, field
4
4
  from pathlib import Path
5
5
  from typing import Any
6
6
 
7
+ from ..config import BUILTIN_SETTINGS
8
+
7
9
 
8
10
  @dataclass(frozen=True)
9
11
  class BenchmarkFile:
@@ -59,9 +61,9 @@ class BenchmarkRunConfig:
59
61
  limit: int | None = None
60
62
  case_ids: tuple[str, ...] = ()
61
63
  concurrency: int = 1
62
- max_steps: int = 50
63
- step_timeout_s: float | None = 300.0
64
- total_timeout_s: float | None = 1800.0
64
+ max_steps: int = BUILTIN_SETTINGS.benchmark.max_steps
65
+ step_timeout_s: float | None = BUILTIN_SETTINGS.benchmark.step_timeout_s
66
+ total_timeout_s: float | None = BUILTIN_SETTINGS.benchmark.total_timeout_s
65
67
  enable_subagents: bool = False
66
68
  enable_background_tasks: bool = False
67
69
  seed: int | None = None
@@ -20,7 +20,13 @@
20
20
  from __future__ import annotations
21
21
 
22
22
  import typer
23
- from click import UsageError
23
+
24
+ try:
25
+ # Typer 0.27 vendors Click to keep its exception types aligned with its
26
+ # renderer; older Typer releases use the public Click package directly.
27
+ from typer.core import _click
28
+ except ImportError: # pragma: no cover - compatibility with older Typer
29
+ import click as _click
24
30
 
25
31
  from .._version import __version__
26
32
  from ._shared import console
@@ -46,9 +52,10 @@ class DefaultShellGroup(typer.core.TyperGroup):
46
52
  if args:
47
53
  first = args[0]
48
54
  if first == "shell":
49
- raise UsageError(
55
+ raise _click.exceptions.UsageError(
50
56
  "`mindcode shell` 已移除;请改用 "
51
- "`mindcode [PROMPT] [OPTIONS]`"
57
+ "`mindcode [PROMPT] [OPTIONS]`",
58
+ ctx,
52
59
  )
53
60
  if (
54
61
  first in PUBLIC_SUBCOMMANDS
@@ -86,15 +86,22 @@ def run(
86
86
  "--benchmark-root",
87
87
  help="benchmark case 根目录",
88
88
  ),
89
- max_steps: int = typer.Option(50, "--max-steps", help="每 case 最大 step"),
89
+ max_steps: int | None = typer.Option(
90
+ None, "--max-steps", help="每 case 最大 step"
91
+ ),
90
92
  total_timeout: float | None = typer.Option(
91
- 600.0,
93
+ None,
92
94
  "--total-timeout",
93
- help="每 case 总超时秒数",
95
+ help="每 case 总超时秒数,0 表示禁用",
94
96
  ),
95
97
  ) -> None:
96
98
  run_id = f"{suite}-{time.strftime('%Y%m%d-%H%M%S')}"
97
99
  try:
100
+ total_timeout_disabled = total_timeout == 0
101
+ if total_timeout is not None and total_timeout < 0:
102
+ raise ValueError("total timeout 必须为 0 或大于 0")
103
+ if total_timeout_disabled:
104
+ total_timeout = None
98
105
  approval_override = (
99
106
  ApprovalMode(approve_mode).value
100
107
  if approve_mode is not None
@@ -106,6 +113,11 @@ def run(
106
113
  approval_override=approval_override,
107
114
  )
108
115
  selected_approve_mode = resolved.approval_mode
116
+ benchmark_defaults = resolved.runtime_settings.benchmark
117
+ if max_steps is None:
118
+ max_steps = benchmark_defaults.max_steps
119
+ if total_timeout is None and not total_timeout_disabled:
120
+ total_timeout = benchmark_defaults.total_timeout_s
109
121
  output_root = Path(output).expanduser()
110
122
  run_output_dir = _run_output_dir(output_root, run_id)
111
123
  run_config = BenchmarkRunConfig(
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import typer
8
8
 
9
+ from ...config import BUILTIN_SETTINGS
9
10
  from ...policy import ApprovalMode
10
11
  from .._shared import console
11
12
  from ..shell import run_shell
@@ -41,15 +42,25 @@ def chat(
41
42
  False, "--no-stream", help="关闭流式输出"
42
43
  ),
43
44
  max_steps: int | None = typer.Option(
44
- None, "--max-steps", help="最大 step 数(默认 50)"
45
+ None,
46
+ "--max-steps",
47
+ help=f"最大 step 数(默认 {BUILTIN_SETTINGS.one_shot.max_steps})",
45
48
  ),
46
49
  step_timeout: float | None = typer.Option(
47
50
  None,
48
51
  "--step-timeout",
49
- help="单步超时秒数(默认 300,必须大于 0)",
52
+ help=(
53
+ "单步超时秒数(默认 "
54
+ f"{BUILTIN_SETTINGS.one_shot.step_timeout_s:g},必须大于 0)"
55
+ ),
50
56
  ),
51
57
  total_timeout: float | None = typer.Option(
52
- None, "--total-timeout", help="总超时秒数,0 表示禁用(默认 1800)"
58
+ None,
59
+ "--total-timeout",
60
+ help=(
61
+ "总超时秒数,0 表示禁用(默认 "
62
+ f"{BUILTIN_SETTINGS.one_shot.total_timeout_s:g})"
63
+ ),
53
64
  ),
54
65
  ) -> None:
55
66
  """发送单条消息,不进入交互模式。"""
@@ -78,13 +89,32 @@ def chat(
78
89
  resume=resume,
79
90
  stream=not no_stream,
80
91
  one_shot_prompt=message,
81
- max_steps=max_steps if max_steps is not None else 50,
92
+ max_steps=(
93
+ max_steps
94
+ if max_steps is not None
95
+ else BUILTIN_SETTINGS.one_shot.max_steps
96
+ ),
82
97
  step_timeout_s=(
83
- step_timeout if step_timeout is not None else 300.0
98
+ step_timeout
99
+ if step_timeout is not None
100
+ else BUILTIN_SETTINGS.one_shot.step_timeout_s
84
101
  ),
85
102
  total_timeout_s=None
86
103
  if total_timeout == 0
87
- else (total_timeout if total_timeout is not None else 1800.0),
104
+ else (
105
+ total_timeout
106
+ if total_timeout is not None
107
+ else BUILTIN_SETTINGS.one_shot.total_timeout_s
108
+ ),
109
+ settings_defaults=frozenset(
110
+ name
111
+ for name, value in (
112
+ ("max_steps", max_steps),
113
+ ("step_timeout_s", step_timeout),
114
+ ("total_timeout_s", total_timeout),
115
+ )
116
+ if value is None
117
+ ),
88
118
  )
89
119
  except ValueError as exc:
90
120
  console.print(f"[red]配置错误:[/red] {exc}")
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import typer
8
8
 
9
+ from ...config import BUILTIN_SETTINGS
9
10
  from ...policy import ApprovalMode
10
11
  from .._shared import console
11
12
  from ..shell import run_shell
@@ -46,23 +47,34 @@ def shell(
46
47
  no_stream: bool = typer.Option(
47
48
  False, "--no-stream", help="关闭流式输出"
48
49
  ),
50
+ no_markdown: bool = typer.Option(
51
+ False, "--no-markdown", help="关闭交互消息的 Markdown 渲染"
52
+ ),
49
53
  subagents: bool = typer.Option(
50
54
  False,
51
55
  "--subagents",
52
56
  help="使用 master/coder 委派模式(await_result)",
53
57
  ),
54
58
  max_steps: int | None = typer.Option(
55
- None, "--max-steps", help="最大 step 数(默认 50)"
59
+ None,
60
+ "--max-steps",
61
+ help=f"最大 step 数(默认 {BUILTIN_SETTINGS.one_shot.max_steps})",
56
62
  ),
57
63
  step_timeout: float | None = typer.Option(
58
64
  None,
59
65
  "--step-timeout",
60
- help="单步超时秒数(交互默认不限制;one-shot 默认 300,必须大于 0)",
66
+ help=(
67
+ "单步超时秒数(交互默认不限制;one-shot 默认 "
68
+ f"{BUILTIN_SETTINGS.one_shot.step_timeout_s:g},必须大于 0)"
69
+ ),
61
70
  ),
62
71
  total_timeout: float | None = typer.Option(
63
72
  None,
64
73
  "--total-timeout",
65
- help="总超时秒数,0 表示禁用(交互默认禁用;一次性/子代理默认 1800)",
74
+ help=(
75
+ "总超时秒数,0 表示禁用(交互默认禁用;一次性/子代理默认 "
76
+ f"{BUILTIN_SETTINGS.one_shot.total_timeout_s:g})"
77
+ ),
66
78
  ),
67
79
  ) -> None:
68
80
  """启动顶层 `mindcode` 交互或 one-shot 模式。"""
@@ -95,7 +107,9 @@ def shell(
95
107
  selected_total_timeout = None
96
108
  else:
97
109
  selected_total_timeout = (
98
- total_timeout if total_timeout is not None else 1800.0
110
+ total_timeout
111
+ if total_timeout is not None
112
+ else BUILTIN_SETTINGS.one_shot.total_timeout_s
99
113
  )
100
114
 
101
115
  try:
@@ -107,15 +121,37 @@ def shell(
107
121
  session_id=session,
108
122
  resume=resume,
109
123
  stream=not no_stream,
124
+ markdown_enabled=False if no_markdown else None,
110
125
  subagents=subagents,
111
126
  one_shot_prompt=prompt,
112
- max_steps=max_steps if max_steps is not None else 50,
127
+ max_steps=(
128
+ max_steps
129
+ if max_steps is not None
130
+ else (
131
+ BUILTIN_SETTINGS.interactive.max_steps
132
+ if prompt is None
133
+ else BUILTIN_SETTINGS.one_shot.max_steps
134
+ )
135
+ ),
113
136
  step_timeout_s=(
114
137
  step_timeout
115
138
  if step_timeout is not None
116
- else (None if prompt is None else 300.0)
139
+ else (
140
+ BUILTIN_SETTINGS.interactive.step_timeout_s
141
+ if prompt is None
142
+ else BUILTIN_SETTINGS.one_shot.step_timeout_s
143
+ )
117
144
  ),
118
145
  total_timeout_s=selected_total_timeout,
146
+ settings_defaults=frozenset(
147
+ name
148
+ for name, value in (
149
+ ("max_steps", max_steps),
150
+ ("step_timeout_s", step_timeout),
151
+ ("total_timeout_s", total_timeout),
152
+ )
153
+ if value is None
154
+ ),
119
155
  )
120
156
  except ValueError as exc:
121
157
  console.print(f"[red]配置错误:[/red] {exc}")