mindcode 0.2.0__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.
Files changed (116) hide show
  1. mindcode/__init__.py +5 -0
  2. mindcode/__main__.py +4 -0
  3. mindcode/__pycache__/__init__.cpython-313.pyc +0 -0
  4. mindcode/__pycache__/__main__.cpython-313.pyc +0 -0
  5. mindcode/__pycache__/_version.cpython-313.pyc +0 -0
  6. mindcode/__pycache__/config.cpython-313.pyc +0 -0
  7. mindcode/__pycache__/policy.cpython-313.pyc +0 -0
  8. mindcode/__pycache__/remote.cpython-313.pyc +0 -0
  9. mindcode/__pycache__/render.cpython-313.pyc +0 -0
  10. mindcode/__pycache__/runtime.cpython-313.pyc +0 -0
  11. mindcode/__pycache__/tasking.cpython-313.pyc +0 -0
  12. mindcode/__pycache__/terminal_core.cpython-313.pyc +0 -0
  13. mindcode/_version.py +11 -0
  14. mindcode/approval.py +105 -0
  15. mindcode/bench/__init__.py +19 -0
  16. mindcode/bench/__pycache__/__init__.cpython-313.pyc +0 -0
  17. mindcode/bench/__pycache__/fake_tools.cpython-313.pyc +0 -0
  18. mindcode/bench/__pycache__/jsonutil.cpython-313.pyc +0 -0
  19. mindcode/bench/__pycache__/paths.cpython-313.pyc +0 -0
  20. mindcode/bench/__pycache__/predictions.cpython-313.pyc +0 -0
  21. mindcode/bench/__pycache__/report.cpython-313.pyc +0 -0
  22. mindcode/bench/__pycache__/runner.cpython-313.pyc +0 -0
  23. mindcode/bench/__pycache__/schema.cpython-313.pyc +0 -0
  24. mindcode/bench/__pycache__/trace.cpython-313.pyc +0 -0
  25. mindcode/bench/__pycache__/workspace.cpython-313.pyc +0 -0
  26. mindcode/bench/adapters/__init__.py +22 -0
  27. mindcode/bench/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
  28. mindcode/bench/adapters/__pycache__/base.cpython-313.pyc +0 -0
  29. mindcode/bench/adapters/__pycache__/mini_bfcl.cpython-313.pyc +0 -0
  30. mindcode/bench/adapters/__pycache__/mini_gaia.cpython-313.pyc +0 -0
  31. mindcode/bench/adapters/__pycache__/mini_terminal.cpython-313.pyc +0 -0
  32. mindcode/bench/adapters/__pycache__/swe_bench.cpython-313.pyc +0 -0
  33. mindcode/bench/adapters/__pycache__/terminal_bench.cpython-313.pyc +0 -0
  34. mindcode/bench/adapters/base.py +45 -0
  35. mindcode/bench/adapters/mini_bfcl.py +18 -0
  36. mindcode/bench/adapters/mini_gaia.py +18 -0
  37. mindcode/bench/adapters/mini_terminal.py +18 -0
  38. mindcode/bench/adapters/swe_bench.py +86 -0
  39. mindcode/bench/adapters/terminal_bench.py +107 -0
  40. mindcode/bench/fake_tools.py +73 -0
  41. mindcode/bench/jsonutil.py +20 -0
  42. mindcode/bench/paths.py +10 -0
  43. mindcode/bench/predictions.py +65 -0
  44. mindcode/bench/report.py +48 -0
  45. mindcode/bench/runner.py +250 -0
  46. mindcode/bench/schema.py +78 -0
  47. mindcode/bench/scorers/__pycache__/base.cpython-313.pyc +0 -0
  48. mindcode/bench/scorers/__pycache__/composite.cpython-313.pyc +0 -0
  49. mindcode/bench/scorers/__pycache__/exact.cpython-313.pyc +0 -0
  50. mindcode/bench/scorers/__pycache__/json_call.cpython-313.pyc +0 -0
  51. mindcode/bench/scorers/__pycache__/swe.cpython-313.pyc +0 -0
  52. mindcode/bench/scorers/__pycache__/terminal.cpython-313.pyc +0 -0
  53. mindcode/bench/scorers/__pycache__/terminal_bench.cpython-313.pyc +0 -0
  54. mindcode/bench/scorers/base.py +22 -0
  55. mindcode/bench/scorers/composite.py +57 -0
  56. mindcode/bench/scorers/exact.py +30 -0
  57. mindcode/bench/scorers/json_call.py +44 -0
  58. mindcode/bench/scorers/swe.py +29 -0
  59. mindcode/bench/scorers/terminal.py +43 -0
  60. mindcode/bench/scorers/terminal_bench.py +25 -0
  61. mindcode/bench/trace.py +34 -0
  62. mindcode/bench/workspace.py +127 -0
  63. mindcode/cli/__init__.py +112 -0
  64. mindcode/cli/__pycache__/__init__.cpython-313.pyc +0 -0
  65. mindcode/cli/__pycache__/_shared.cpython-313.pyc +0 -0
  66. mindcode/cli/_shared.py +43 -0
  67. mindcode/cli/commands/__init__.py +1 -0
  68. mindcode/cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  69. mindcode/cli/commands/__pycache__/bench.cpython-313.pyc +0 -0
  70. mindcode/cli/commands/__pycache__/chat.cpython-313.pyc +0 -0
  71. mindcode/cli/commands/__pycache__/config_cmd.cpython-313.pyc +0 -0
  72. mindcode/cli/commands/__pycache__/remote.cpython-313.pyc +0 -0
  73. mindcode/cli/commands/__pycache__/shell.cpython-313.pyc +0 -0
  74. mindcode/cli/commands/__pycache__/status.cpython-313.pyc +0 -0
  75. mindcode/cli/commands/__pycache__/task.cpython-313.pyc +0 -0
  76. mindcode/cli/commands/__pycache__/terminal.cpython-313.pyc +0 -0
  77. mindcode/cli/commands/bench.py +365 -0
  78. mindcode/cli/commands/chat.py +86 -0
  79. mindcode/cli/commands/config_cmd.py +197 -0
  80. mindcode/cli/commands/remote.py +146 -0
  81. mindcode/cli/commands/shell.py +108 -0
  82. mindcode/cli/commands/status.py +37 -0
  83. mindcode/cli/commands/task.py +417 -0
  84. mindcode/cli/commands/terminal.py +124 -0
  85. mindcode/cli/shell/__init__.py +5 -0
  86. mindcode/cli/shell/__pycache__/__init__.cpython-313.pyc +0 -0
  87. mindcode/cli/shell/__pycache__/completion.cpython-313.pyc +0 -0
  88. mindcode/cli/shell/__pycache__/menu.cpython-313.pyc +0 -0
  89. mindcode/cli/shell/__pycache__/repl.cpython-313.pyc +0 -0
  90. mindcode/cli/shell/__pycache__/slash.cpython-313.pyc +0 -0
  91. mindcode/cli/shell/__pycache__/startup.cpython-313.pyc +0 -0
  92. mindcode/cli/shell/completion.py +72 -0
  93. mindcode/cli/shell/menu.py +88 -0
  94. mindcode/cli/shell/repl.py +597 -0
  95. mindcode/cli/shell/slash.py +480 -0
  96. mindcode/cli/shell/startup.py +55 -0
  97. mindcode/cli/shell/tui.py +897 -0
  98. mindcode/config.py +173 -0
  99. mindcode/mcp.py +205 -0
  100. mindcode/policy.py +173 -0
  101. mindcode/remote.py +216 -0
  102. mindcode/render.py +458 -0
  103. mindcode/runtime.py +541 -0
  104. mindcode/skills.py +154 -0
  105. mindcode/subagents.py +43 -0
  106. mindcode/tasking.py +281 -0
  107. mindcode/terminal/__init__.py +1 -0
  108. mindcode/terminal/__main__.py +17 -0
  109. mindcode/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
  110. mindcode/terminal/__pycache__/__main__.cpython-313.pyc +0 -0
  111. mindcode/terminal_core.py +264 -0
  112. mindcode-0.2.0.dist-info/METADATA +244 -0
  113. mindcode-0.2.0.dist-info/RECORD +116 -0
  114. mindcode-0.2.0.dist-info/WHEEL +5 -0
  115. mindcode-0.2.0.dist-info/entry_points.txt +2 -0
  116. mindcode-0.2.0.dist-info/top_level.txt +1 -0
mindcode/config.py ADDED
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tomllib
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+
9
+ CONFIG_DIR = Path.home() / ".cache" / "mindcode"
10
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
11
+ SESSIONS_DIR = CONFIG_DIR / "sessions"
12
+
13
+
14
+ CONFIG_TEMPLATE = """\
15
+ # mindcode configuration
16
+ # Located at: ~/.cache/mindcode/config.toml
17
+ #
18
+ # Edit this file to add or modify channels. API keys are stored locally
19
+ # and never sent anywhere except the channel endpoint.
20
+ #
21
+ # Inside the mindcode REPL you can also:
22
+ # /channels pick or add an API channel
23
+ # /models list and switch models of current channel
24
+
25
+ # Channel used when -p/--provider is not passed on the command line.
26
+ default_provider = "rotor"
27
+
28
+ # ────────────────────────────────────────────────────────────────────────
29
+ # Rotor gateway — OpenAI-compatible gateway endpoint
30
+ # ────────────────────────────────────────────────────────────────────────
31
+ [providers.rotor]
32
+ protocol = "openai"
33
+ api_key = ""
34
+ base_url = "http://127.0.0.1:8000/v1"
35
+ default_model = "glm-5.2"
36
+ models = [
37
+ "glm-5.2",
38
+ "glm-5.1",
39
+ "glm-4.7",
40
+ ]
41
+ """
42
+
43
+
44
+ @dataclass
45
+ class ProviderConfig:
46
+ name: str
47
+ api_key: str
48
+ base_url: str
49
+ default_model: str
50
+ protocol: str = "openai"
51
+ models: list[str] = field(default_factory=list)
52
+
53
+ def resolve_model(self, override: str | None) -> str:
54
+ if override:
55
+ return override
56
+ if self.default_model:
57
+ return self.default_model
58
+ if self.models:
59
+ return self.models[0]
60
+ raise ValueError(f"provider {self.name} 没有可用 model")
61
+
62
+ def validate(self) -> list[str]:
63
+ problems: list[str] = []
64
+ if self.protocol not in {"openai", "anthropic"}:
65
+ problems.append(
66
+ f"providers.{self.name}.protocol 不支持: {self.protocol}"
67
+ )
68
+ if not self.api_key:
69
+ problems.append(f"providers.{self.name}.api_key 未配置")
70
+ if not self.base_url:
71
+ problems.append(f"providers.{self.name}.base_url 未配置")
72
+ try:
73
+ self.resolve_model(None)
74
+ except ValueError as exc:
75
+ problems.append(str(exc))
76
+ return problems
77
+
78
+
79
+ @dataclass
80
+ class MindcodeConfig:
81
+ default_provider: str
82
+ providers: dict[str, ProviderConfig]
83
+ config_path: Path
84
+
85
+ def get_provider(self, name: str | None) -> ProviderConfig:
86
+ target = name or self.default_provider
87
+ if not target:
88
+ raise ValueError(
89
+ "config.toml 中未设置 default_provider,"
90
+ "请用 -p/--provider 显式指定"
91
+ )
92
+ if target not in self.providers:
93
+ available = ", ".join(sorted(self.providers)) or "(无)"
94
+ raise ValueError(
95
+ f"未知 provider: {target};可用: {available}"
96
+ )
97
+ return self.providers[target]
98
+
99
+
100
+ def _render_toml(
101
+ default_provider: str,
102
+ providers: dict[str, ProviderConfig],
103
+ ) -> str:
104
+ lines: list[str] = [
105
+ "# mindcode configuration",
106
+ f"# Located at: {CONFIG_FILE}",
107
+ "# Edit this file to add or modify providers.",
108
+ "",
109
+ f'default_provider = "{default_provider}"',
110
+ "",
111
+ ]
112
+ for name, p in providers.items():
113
+ lines.append(f"[providers.{name}]")
114
+ lines.append(f'protocol = "{p.protocol}"')
115
+ lines.append(f'api_key = "{p.api_key}"')
116
+ lines.append(f'base_url = "{p.base_url}"')
117
+ lines.append(f'default_model = "{p.default_model}"')
118
+ if len(p.models) <= 1:
119
+ inner = ", ".join(f'"{m}"' for m in p.models)
120
+ lines.append(f"models = [{inner}]")
121
+ else:
122
+ lines.append("models = [")
123
+ for m in p.models:
124
+ lines.append(f' "{m}",')
125
+ lines.append("]")
126
+ lines.append("")
127
+ return "\n".join(lines).rstrip() + "\n"
128
+
129
+
130
+ def load_config(path: Path | None = None) -> MindcodeConfig:
131
+ """Load config from path; create with template if missing."""
132
+ config_path = path or CONFIG_FILE
133
+ if not config_path.exists():
134
+ config_path.parent.mkdir(parents=True, exist_ok=True)
135
+ config_path.write_text(CONFIG_TEMPLATE, encoding="utf-8")
136
+ os.chmod(config_path, 0o600)
137
+ return _read_config(config_path)
138
+
139
+
140
+ def save_config(config: MindcodeConfig) -> None:
141
+ """Write config back to its config_path atomically."""
142
+ path = config.config_path
143
+ path.parent.mkdir(parents=True, exist_ok=True)
144
+ text = _render_toml(config.default_provider, config.providers)
145
+ tmp = path.with_suffix(".tmp")
146
+ tmp.write_text(text, encoding="utf-8")
147
+ os.chmod(tmp, 0o600)
148
+ tmp.replace(path)
149
+
150
+
151
+ def _read_config(path: Path) -> MindcodeConfig:
152
+ with path.open("rb") as f:
153
+ data = tomllib.load(f)
154
+ providers_raw = data.get("providers", {})
155
+ if not isinstance(providers_raw, dict):
156
+ providers_raw = {}
157
+ providers: dict[str, ProviderConfig] = {}
158
+ for name, raw in providers_raw.items():
159
+ if not isinstance(raw, dict):
160
+ continue
161
+ providers[name] = ProviderConfig(
162
+ name=name,
163
+ protocol=str(raw.get("protocol", "openai")),
164
+ api_key=str(raw.get("api_key", "")),
165
+ base_url=str(raw.get("base_url", "")),
166
+ default_model=str(raw.get("default_model", "")),
167
+ models=list(raw.get("models", []) or []),
168
+ )
169
+ return MindcodeConfig(
170
+ default_provider=str(data.get("default_provider", "")),
171
+ providers=providers,
172
+ config_path=path,
173
+ )
mindcode/mcp.py ADDED
@@ -0,0 +1,205 @@
1
+ """Minimal MindCode-side MCP client and tool adapter.
2
+
3
+ This module deliberately defines a transport-neutral client protocol. A host
4
+ can provide an MCP implementation, while tests can use a tiny in-memory fake;
5
+ MindCode itself does not add a network dependency.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import inspect
11
+ from collections.abc import Iterable, Mapping
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Protocol, runtime_checkable
14
+
15
+ from mindagent.core import ActionRisk
16
+ from mindagent.tools.base import BaseTool, ToolContext, ToolDefinition
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class MCPToolSpec:
21
+ """Transport-independent description returned by an MCP server."""
22
+
23
+ name: str
24
+ description: str = ""
25
+ input_schema: dict[str, Any] = field(
26
+ default_factory=lambda: {
27
+ "type": "object",
28
+ "properties": {},
29
+ "additionalProperties": False,
30
+ }
31
+ )
32
+ risk: ActionRisk = ActionRisk.WRITE
33
+
34
+ def __post_init__(self) -> None:
35
+ name = self.name.strip()
36
+ if not name:
37
+ raise ValueError("MCP tool name 不能为空")
38
+ if not isinstance(self.input_schema, dict):
39
+ raise TypeError("MCP tool input_schema 必须是 object schema")
40
+ object.__setattr__(self, "name", name)
41
+ object.__setattr__(self, "description", self.description.strip())
42
+ object.__setattr__(self, "input_schema", dict(self.input_schema))
43
+ if not isinstance(self.risk, ActionRisk):
44
+ object.__setattr__(self, "risk", _coerce_risk(self.risk))
45
+
46
+ @classmethod
47
+ def from_mapping(cls, value: Mapping[str, Any]) -> "MCPToolSpec":
48
+ annotations = value.get("annotations")
49
+ if not isinstance(annotations, Mapping):
50
+ annotations = {}
51
+ risk = value.get("risk", annotations.get("risk"))
52
+ if risk is None and annotations.get("readOnlyHint") is True:
53
+ risk = ActionRisk.READ_ONLY
54
+ if risk is None and annotations.get("destructiveHint") is True:
55
+ risk = ActionRisk.DANGEROUS
56
+ return cls(
57
+ name=str(value.get("name", "")),
58
+ description=str(value.get("description", "")),
59
+ input_schema=dict(
60
+ value.get("input_schema", value.get("inputSchema", {}))
61
+ or {}
62
+ ),
63
+ risk=_coerce_risk(risk),
64
+ )
65
+
66
+
67
+ @runtime_checkable
68
+ class MCPClient(Protocol):
69
+ """Small client contract required by :func:`adapt_mcp_tools`.
70
+
71
+ ``list_tools`` is synchronous so runtime construction remains synchronous;
72
+ ``call_tool`` may return a value or an awaitable.
73
+ """
74
+
75
+ def list_tools(self) -> Iterable[MCPToolSpec | Mapping[str, Any]]: ...
76
+
77
+ def call_tool(self, name: str, arguments: dict[str, Any]) -> Any: ...
78
+
79
+
80
+ class MCPToolAdapter(BaseTool):
81
+ """Expose one MCP tool through MindAgent's ordinary ``BaseTool`` API."""
82
+
83
+ def __init__(
84
+ self,
85
+ client: MCPClient,
86
+ spec: MCPToolSpec | Mapping[str, Any],
87
+ *,
88
+ namespace: str | None = None,
89
+ close_client: bool = False,
90
+ ):
91
+ self.client = client
92
+ self.spec = _coerce_spec(spec)
93
+ self._close_client = close_client
94
+ public_name = self.spec.name
95
+ if namespace:
96
+ public_name = f"{namespace}_{public_name}"
97
+ self.definition = ToolDefinition(
98
+ name=public_name,
99
+ description=self.spec.description or f"MCP tool {self.spec.name}",
100
+ parameters=self.spec.input_schema,
101
+ risk=self.spec.risk,
102
+ )
103
+
104
+ async def execute(
105
+ self,
106
+ arguments: dict[str, Any],
107
+ context: ToolContext,
108
+ ) -> Any:
109
+ result = self.client.call_tool(self.spec.name, arguments)
110
+ if inspect.isawaitable(result):
111
+ return await result
112
+ return result
113
+
114
+ async def close(self) -> None:
115
+ if not self._close_client:
116
+ return
117
+ self._close_client = False
118
+ close = getattr(self.client, "close", None)
119
+ if close is None:
120
+ close = getattr(self.client, "aclose", None)
121
+ if callable(close):
122
+ result = close()
123
+ if inspect.isawaitable(result):
124
+ await result
125
+
126
+
127
+ def adapt_mcp_tools(
128
+ client: MCPClient,
129
+ *,
130
+ namespace: str | None = None,
131
+ ) -> list[MCPToolAdapter]:
132
+ """Discover a synchronous MCP client and return ordinary BaseTool adapters."""
133
+ listed = client.list_tools()
134
+ if inspect.isawaitable(listed):
135
+ raise TypeError(
136
+ "MCPClient.list_tools() 必须同步;异步客户端请先在宿主侧发现工具"
137
+ )
138
+ if isinstance(listed, Mapping):
139
+ listed = listed.get("tools", ())
140
+ specs = [_coerce_spec(value) for value in listed]
141
+ return [
142
+ MCPToolAdapter(
143
+ client,
144
+ spec,
145
+ namespace=namespace,
146
+ close_client=index == 0,
147
+ )
148
+ for index, spec in enumerate(specs)
149
+ ]
150
+
151
+
152
+ async def aload_mcp_tools(
153
+ client: MCPClient,
154
+ *,
155
+ namespace: str | None = None,
156
+ ) -> list[MCPToolAdapter]:
157
+ """Async counterpart for clients whose tool discovery is awaitable."""
158
+ listed = client.list_tools()
159
+ if inspect.isawaitable(listed):
160
+ listed = await listed
161
+ if isinstance(listed, Mapping):
162
+ listed = listed.get("tools", ())
163
+ specs = [_coerce_spec(value) for value in listed]
164
+ return [
165
+ MCPToolAdapter(
166
+ client,
167
+ spec,
168
+ namespace=namespace,
169
+ close_client=index == 0,
170
+ )
171
+ for index, spec in enumerate(specs)
172
+ ]
173
+
174
+
175
+ def _coerce_spec(value: MCPToolSpec | Mapping[str, Any]) -> MCPToolSpec:
176
+ if isinstance(value, MCPToolSpec):
177
+ return value
178
+ if isinstance(value, Mapping):
179
+ return MCPToolSpec.from_mapping(value)
180
+ raise TypeError("MCP tools 必须是 MCPToolSpec 或 mapping")
181
+
182
+
183
+ def _coerce_risk(value: Any) -> ActionRisk:
184
+ if isinstance(value, ActionRisk):
185
+ return value
186
+ if value is None:
187
+ return ActionRisk.WRITE
188
+ try:
189
+ return ActionRisk(str(value).lower())
190
+ except ValueError as exc:
191
+ raise ValueError(f"未知 MCP tool risk: {value}") from exc
192
+
193
+
194
+ # Common spelling for callers that model MCP's tool description directly.
195
+ MCPToolDefinition = MCPToolSpec
196
+
197
+
198
+ __all__ = [
199
+ "MCPClient",
200
+ "MCPToolAdapter",
201
+ "MCPToolDefinition",
202
+ "MCPToolSpec",
203
+ "adapt_mcp_tools",
204
+ "aload_mcp_tools",
205
+ ]
mindcode/policy.py ADDED
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from enum import Enum
5
+
6
+ from mindagent.core import (
7
+ ActionRequest,
8
+ ActionRisk,
9
+ AgentContext,
10
+ ValidationResult,
11
+ )
12
+
13
+
14
+ class ApprovalMode(str, Enum):
15
+ """The two supported MindCode permission modes.
16
+
17
+ ``ASK`` and ``AUTO`` remain enum aliases so sessions created by older
18
+ MindCode versions keep working. Their values are the new canonical
19
+ ``ask_user`` and ``full_accept`` values. Short CLI spellings are handled
20
+ by ``_missing_`` for the same migration reason.
21
+ """
22
+
23
+ ASK_USER = "ask_user"
24
+ FULL_ACCEPT = "full_accept"
25
+
26
+ # Source compatibility for callers that imported the old names.
27
+ ASK = ASK_USER
28
+ AUTO = FULL_ACCEPT
29
+
30
+ @classmethod
31
+ def _missing_(cls, value: object) -> "ApprovalMode | None":
32
+ if not isinstance(value, str):
33
+ return None
34
+ aliases = {
35
+ "ask": cls.ASK_USER,
36
+ "full": cls.FULL_ACCEPT,
37
+ "full-access": cls.FULL_ACCEPT,
38
+ "auto": cls.FULL_ACCEPT,
39
+ }
40
+ return aliases.get(value.strip().lower())
41
+
42
+
43
+ def approval_mode_label(mode: ApprovalMode | str) -> str:
44
+ """Return the user-facing name for an approval mode."""
45
+ selected = ApprovalMode(mode)
46
+ if selected is ApprovalMode.ASK_USER:
47
+ return "Ask User"
48
+ return "Full Accept"
49
+
50
+
51
+ ApprovalCallback = Callable[[AgentContext, ActionRequest], Awaitable[bool]]
52
+
53
+
54
+ def _validate_session_policy(
55
+ context: AgentContext,
56
+ action: ActionRequest,
57
+ ) -> ValidationResult | None:
58
+ """Enforce session-scoped capability limits before mode approval.
59
+
60
+ MindCode's policy is the first extension invoked by ``AgentRuntime`` when
61
+ a custom policy is installed, so it must retain the same capability
62
+ checks as MindAgent's default policy instead of relying on a separate
63
+ ``AgentSession`` wrapper.
64
+ """
65
+ session_policy = context.session_policy
66
+ if session_policy is None:
67
+ return None
68
+ if (
69
+ session_policy.allowed_action_types is not None
70
+ and action.action_type not in session_policy.allowed_action_types
71
+ ):
72
+ return ValidationResult(False, "当前 session 不允许该 action 类型")
73
+ if (
74
+ session_policy.allowed_tools is not None
75
+ and action.name not in session_policy.allowed_tools
76
+ ):
77
+ return ValidationResult(
78
+ False,
79
+ f"当前 session 不允许工具: {action.name}",
80
+ )
81
+ if action.risk == ActionRisk.WRITE and not session_policy.allow_write:
82
+ return ValidationResult(False, "当前 session 不允许写操作")
83
+ if (
84
+ action.risk == ActionRisk.DANGEROUS
85
+ and not session_policy.allow_dangerous
86
+ ):
87
+ return ValidationResult(False, "当前 session 不允许危险操作")
88
+ return None
89
+
90
+
91
+ async def _default_approval_callback(
92
+ context: AgentContext,
93
+ action: ActionRequest,
94
+ ) -> bool:
95
+ """Ask in the MindCode shell when no host callback was supplied."""
96
+ # Lazy import avoids importing the CLI (and prompt_toolkit) for library
97
+ # users that construct a policy in a non-interactive process.
98
+ from .approval import describe_action
99
+ from .cli.shell.menu import prompt_yes_no
100
+
101
+ summary = describe_action(action)
102
+ return await prompt_yes_no(f"允许执行 {summary}?", default_yes=False)
103
+
104
+
105
+ class AskUserPolicy:
106
+ """Allow reads and ask the user before writes or dangerous actions.
107
+
108
+ 注意:validate_action 内阻塞式 confirm 是产品层授权等待(用户决策),
109
+ 已豁免 step 超时、受 total_timeout 监督。未来 Policy 解耦(deferred +
110
+ 挂起 validation 待审批后恢复)需 core 支持挂起/恢复机制,本次不做。
111
+ """
112
+
113
+ def __init__(self, confirm: ApprovalCallback | None = None):
114
+ self._confirm = confirm or _default_approval_callback
115
+
116
+ async def validate_action(
117
+ self,
118
+ context: AgentContext,
119
+ action: ActionRequest,
120
+ ) -> ValidationResult:
121
+ session_result = _validate_session_policy(context, action)
122
+ if session_result is not None:
123
+ return session_result
124
+ if action.risk == ActionRisk.READ_ONLY:
125
+ return ValidationResult(allowed=True, reason="只读动作已授权")
126
+ approved = await self._confirm(context, action)
127
+ if approved:
128
+ return ValidationResult(
129
+ allowed=True,
130
+ reason=f"用户已批准 {action.risk.value} 操作",
131
+ )
132
+ return ValidationResult(
133
+ allowed=False,
134
+ reason=f"用户拒绝执行 {action.risk.value} 操作",
135
+ )
136
+
137
+
138
+ class FullAcceptPolicy:
139
+ """Allow every action selected by the current session policy."""
140
+
141
+ async def validate_action(
142
+ self,
143
+ context: AgentContext,
144
+ action: ActionRequest,
145
+ ) -> ValidationResult:
146
+ session_result = _validate_session_policy(context, action)
147
+ if session_result is not None:
148
+ return session_result
149
+ return ValidationResult(allowed=True, reason="full access 已授权")
150
+
151
+
152
+ def make_policy(
153
+ mode: ApprovalMode | str,
154
+ *,
155
+ approval_callback: ApprovalCallback | None = None,
156
+ ) -> AskUserPolicy | FullAcceptPolicy:
157
+ """Build the product policy for ``mode``.
158
+
159
+ ``approval_callback`` is useful for a host UI or deterministic tests. The
160
+ interactive shell gets the default callback automatically.
161
+ """
162
+ selected = ApprovalMode(mode)
163
+ if selected is ApprovalMode.ASK_USER:
164
+ return AskUserPolicy(approval_callback)
165
+ if selected is ApprovalMode.FULL_ACCEPT:
166
+ return FullAcceptPolicy()
167
+ raise ValueError(f"未知 ApprovalMode: {mode}")
168
+
169
+
170
+ # Compatibility names for callers that imported the old policy classes. They
171
+ # intentionally map to the new two-mode semantics; there is no suggest mode.
172
+ AskPolicy = AskUserPolicy
173
+ AutoPolicy = FullAcceptPolicy