deepy-cli 0.2.23__tar.gz → 0.2.25__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.
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/PKG-INFO +9 -1
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/README.md +8 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/pyproject.toml +1 -1
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/__init__.py +1 -1
- deepy_cli-0.2.25/src/deepy/audit.py +158 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/config/__init__.py +4 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/config/settings.py +34 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/agent.py +6 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/runner.py +202 -43
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/mcp.py +12 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/status.py +8 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tools/agents.py +48 -3
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tui/app.py +58 -1
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tui/screens.py +183 -1
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tui/widgets.py +42 -5
- deepy_cli-0.2.25/src/deepy/ui/audit_approval_panel.py +401 -0
- deepy_cli-0.2.25/src/deepy/ui/audit_approval_picker.py +295 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/file_mentions.py +1 -2
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/message_view.py +25 -3
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/prompt_input.py +9 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/terminal.py +395 -7
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/background_tasks.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/cli.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/Read.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/Search.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/Update.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/Write.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/shell.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/task_list.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/task_output.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/task_stop.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/test_shell.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/data/tools/todo_write.md +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/errors.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/input_suggestions.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/cache_context.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/prompts/init_agents.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/prompts/system.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/prompts/tool_docs.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/session_cost.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/sessions/index.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/sessions/session.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/sessions/store_helpers.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/skill_market.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/skills.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/subagents.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/todos.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tools/builtin.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tools/file_state.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tools/search.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tools/shell_output.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tools/test_shell.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tui/__init__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tui/commands.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tui/compat.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tui/diff.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tui/runner.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/tui/state.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/types/__init__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/types/sdk.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/types/tool_payloads.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/local_command.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/skill_picker.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/status_footer.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/styles.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/usage.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.2.23 → deepy_cli-0.2.25}/src/deepy/utils/notify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: deepy-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.25
|
|
4
4
|
Summary: Deepy - Vibe coding for DeepSeek models in your terminal
|
|
5
5
|
Keywords: deepseek,coding-agent,terminal,cli,agents
|
|
6
6
|
Author: kirineko
|
|
@@ -327,10 +327,18 @@ compact_trigger_ratio = 0.8
|
|
|
327
327
|
reserved_context_tokens = 50000
|
|
328
328
|
compact_preserve_recent_messages = 2
|
|
329
329
|
|
|
330
|
+
[audit]
|
|
331
|
+
mode = "yolo" # normal, auto, or yolo
|
|
332
|
+
|
|
330
333
|
[ui]
|
|
331
334
|
theme = "dark" # dark or light
|
|
332
335
|
```
|
|
333
336
|
|
|
337
|
+
Audit modes control side-effect approval. `normal` asks before managed text
|
|
338
|
+
writes, shell commands, background task stops, and MCP tool calls. `auto`
|
|
339
|
+
auto-approves managed text writes but still asks for commands and untrusted MCP
|
|
340
|
+
tools. `yolo` preserves the high-autonomy default.
|
|
341
|
+
|
|
334
342
|
Manual configuration commands:
|
|
335
343
|
|
|
336
344
|
```bash
|
|
@@ -295,10 +295,18 @@ compact_trigger_ratio = 0.8
|
|
|
295
295
|
reserved_context_tokens = 50000
|
|
296
296
|
compact_preserve_recent_messages = 2
|
|
297
297
|
|
|
298
|
+
[audit]
|
|
299
|
+
mode = "yolo" # normal, auto, or yolo
|
|
300
|
+
|
|
298
301
|
[ui]
|
|
299
302
|
theme = "dark" # dark or light
|
|
300
303
|
```
|
|
301
304
|
|
|
305
|
+
Audit modes control side-effect approval. `normal` asks before managed text
|
|
306
|
+
writes, shell commands, background task stops, and MCP tool calls. `auto`
|
|
307
|
+
auto-approves managed text writes but still asks for commands and untrusted MCP
|
|
308
|
+
tools. `yolo` preserves the high-autonomy default.
|
|
309
|
+
|
|
302
310
|
Manual configuration commands:
|
|
303
311
|
|
|
304
312
|
```bash
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Any, Literal, cast
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuditMode(StrEnum):
|
|
10
|
+
NORMAL = "normal"
|
|
11
|
+
AUTO = "auto"
|
|
12
|
+
YOLO = "yolo"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
AUDIT_MODES = {mode.value for mode in AuditMode}
|
|
16
|
+
DEFAULT_AUDIT_MODE = AuditMode.YOLO
|
|
17
|
+
AuditAction = Literal["text_write", "command", "background_task_control", "mcp_tool"]
|
|
18
|
+
ApprovalOutcome = Literal["approve", "reject"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_audit_mode(value: object, *, default: AuditMode = DEFAULT_AUDIT_MODE) -> AuditMode:
|
|
22
|
+
if isinstance(value, AuditMode):
|
|
23
|
+
return value
|
|
24
|
+
if isinstance(value, str):
|
|
25
|
+
normalized = value.strip().lower()
|
|
26
|
+
if normalized in AUDIT_MODES:
|
|
27
|
+
return AuditMode(normalized)
|
|
28
|
+
return default
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_valid_audit_mode(value: str) -> bool:
|
|
32
|
+
return value in AUDIT_MODES
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def next_audit_mode(mode: AuditMode | str) -> AuditMode:
|
|
36
|
+
current = parse_audit_mode(mode)
|
|
37
|
+
order = (AuditMode.NORMAL, AuditMode.AUTO, AuditMode.YOLO)
|
|
38
|
+
index = order.index(current)
|
|
39
|
+
return order[(index + 1) % len(order)]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class AuditModeState:
|
|
44
|
+
mode: AuditMode = DEFAULT_AUDIT_MODE
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_value(cls, value: object) -> "AuditModeState":
|
|
48
|
+
return cls(parse_audit_mode(value))
|
|
49
|
+
|
|
50
|
+
def set(self, value: AuditMode | str) -> AuditMode:
|
|
51
|
+
self.mode = parse_audit_mode(value, default=self.mode)
|
|
52
|
+
return self.mode
|
|
53
|
+
|
|
54
|
+
def cycle(self) -> AuditMode:
|
|
55
|
+
self.mode = next_audit_mode(self.mode)
|
|
56
|
+
return self.mode
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class McpSafeTool:
|
|
61
|
+
server: str
|
|
62
|
+
tool: str
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_mapping(cls, value: Mapping[str, Any]) -> "McpSafeTool | None":
|
|
66
|
+
server = value.get("server")
|
|
67
|
+
tool = value.get("tool")
|
|
68
|
+
if not isinstance(server, str) or not server.strip():
|
|
69
|
+
return None
|
|
70
|
+
if not isinstance(tool, str) or not tool.strip():
|
|
71
|
+
return None
|
|
72
|
+
return cls(server=server.strip(), tool=tool.strip())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class AuditConfig:
|
|
77
|
+
mode: AuditMode = DEFAULT_AUDIT_MODE
|
|
78
|
+
mcp_safe_tools: tuple[McpSafeTool, ...] = ()
|
|
79
|
+
invalid_mode: str | None = None
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_mapping(cls, raw: Mapping[str, Any]) -> "AuditConfig":
|
|
83
|
+
raw_mode = raw.get("mode")
|
|
84
|
+
mode = parse_audit_mode(raw_mode)
|
|
85
|
+
invalid_mode = (
|
|
86
|
+
str(raw_mode)
|
|
87
|
+
if isinstance(raw_mode, str) and raw_mode.strip() and raw_mode.strip().lower() not in AUDIT_MODES
|
|
88
|
+
else None
|
|
89
|
+
)
|
|
90
|
+
safe_tools: list[McpSafeTool] = []
|
|
91
|
+
for item in _safe_tool_entries(raw.get("mcp_safe_tools")):
|
|
92
|
+
parsed = McpSafeTool.from_mapping(item)
|
|
93
|
+
if parsed is not None:
|
|
94
|
+
safe_tools.append(parsed)
|
|
95
|
+
return cls(mode=mode, mcp_safe_tools=tuple(safe_tools), invalid_mode=invalid_mode)
|
|
96
|
+
|
|
97
|
+
def is_mcp_tool_safe(self, server: str, tool: str) -> bool:
|
|
98
|
+
return any(entry.server == server and entry.tool == tool for entry in self.mcp_safe_tools)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class AuditPolicy:
|
|
103
|
+
mode_getter: Callable[[], AuditMode]
|
|
104
|
+
config: AuditConfig = field(default_factory=AuditConfig)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_mode(cls, mode: AuditMode | str, config: AuditConfig | None = None) -> "AuditPolicy":
|
|
108
|
+
parsed = parse_audit_mode(mode)
|
|
109
|
+
return cls(lambda: parsed, config=config or AuditConfig(mode=parsed))
|
|
110
|
+
|
|
111
|
+
def active_mode(self) -> AuditMode:
|
|
112
|
+
return parse_audit_mode(self.mode_getter())
|
|
113
|
+
|
|
114
|
+
def needs_approval(self, action: AuditAction) -> bool:
|
|
115
|
+
mode = self.active_mode()
|
|
116
|
+
if mode == AuditMode.YOLO:
|
|
117
|
+
return False
|
|
118
|
+
if mode == AuditMode.NORMAL:
|
|
119
|
+
return action in {"text_write", "command", "background_task_control", "mcp_tool"}
|
|
120
|
+
if mode == AuditMode.AUTO:
|
|
121
|
+
return action in {"command", "background_task_control", "mcp_tool"}
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def needs_mcp_approval(self, *, server: str, tool: str) -> bool:
|
|
125
|
+
mode = self.active_mode()
|
|
126
|
+
if mode == AuditMode.YOLO:
|
|
127
|
+
return False
|
|
128
|
+
if mode == AuditMode.AUTO and self.config.is_mcp_tool_safe(server, tool):
|
|
129
|
+
return False
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class PendingApproval:
|
|
135
|
+
index: int
|
|
136
|
+
name: str
|
|
137
|
+
tool_name: str
|
|
138
|
+
arguments: str
|
|
139
|
+
agent_name: str = ""
|
|
140
|
+
action_kind: str = "tool"
|
|
141
|
+
server_name: str = ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class ApprovalDecision:
|
|
146
|
+
outcome: ApprovalOutcome
|
|
147
|
+
always: bool = False
|
|
148
|
+
rejection_message: str | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _safe_tool_entries(value: object) -> list[Mapping[str, Any]]:
|
|
152
|
+
if not isinstance(value, list):
|
|
153
|
+
return []
|
|
154
|
+
entries: list[Mapping[str, Any]] = []
|
|
155
|
+
for item in value:
|
|
156
|
+
if isinstance(item, Mapping):
|
|
157
|
+
entries.append(cast(Mapping[str, Any], item))
|
|
158
|
+
return entries
|
|
@@ -47,6 +47,7 @@ from .settings import (
|
|
|
47
47
|
is_valid_ui_theme,
|
|
48
48
|
is_valid_ui_view_mode,
|
|
49
49
|
is_valid_reasoning_mode,
|
|
50
|
+
is_valid_config_audit_mode,
|
|
50
51
|
load_settings,
|
|
51
52
|
mask_secret,
|
|
52
53
|
provider_info_for,
|
|
@@ -54,6 +55,7 @@ from .settings import (
|
|
|
54
55
|
settings_to_toml_dict,
|
|
55
56
|
thinking_modes_for_provider,
|
|
56
57
|
update_config_model_settings,
|
|
58
|
+
update_config_audit_mode,
|
|
57
59
|
update_config_input_suggestions_enabled,
|
|
58
60
|
update_config_theme,
|
|
59
61
|
update_config_view_mode,
|
|
@@ -109,6 +111,7 @@ __all__ = [
|
|
|
109
111
|
"is_valid_ui_theme",
|
|
110
112
|
"is_valid_ui_view_mode",
|
|
111
113
|
"is_valid_reasoning_mode",
|
|
114
|
+
"is_valid_config_audit_mode",
|
|
112
115
|
"load_settings",
|
|
113
116
|
"mask_secret",
|
|
114
117
|
"provider_info_for",
|
|
@@ -116,6 +119,7 @@ __all__ = [
|
|
|
116
119
|
"settings_to_toml_dict",
|
|
117
120
|
"thinking_modes_for_provider",
|
|
118
121
|
"update_config_model_settings",
|
|
122
|
+
"update_config_audit_mode",
|
|
119
123
|
"update_config_input_suggestions_enabled",
|
|
120
124
|
"update_config_theme",
|
|
121
125
|
"update_config_view_mode",
|
|
@@ -9,6 +9,8 @@ from typing import Any, Mapping, Self
|
|
|
9
9
|
|
|
10
10
|
import tomli_w
|
|
11
11
|
|
|
12
|
+
from deepy.audit import AuditConfig, AuditMode, DEFAULT_AUDIT_MODE, is_valid_audit_mode
|
|
13
|
+
|
|
12
14
|
DEFAULT_MODEL = "deepseek-v4-pro"
|
|
13
15
|
DEFAULT_BASE_URL = "https://api.deepseek.com"
|
|
14
16
|
DEFAULT_CONTEXT_WINDOW_TOKENS = 1_048_576
|
|
@@ -595,6 +597,7 @@ class UiConfig:
|
|
|
595
597
|
|
|
596
598
|
@dataclass(frozen=True)
|
|
597
599
|
class Settings:
|
|
600
|
+
audit: AuditConfig = field(default_factory=AuditConfig)
|
|
598
601
|
model: ModelConfig = field(default_factory=ModelConfig)
|
|
599
602
|
context: ContextConfig = field(default_factory=ContextConfig)
|
|
600
603
|
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
|
@@ -613,6 +616,7 @@ class Settings:
|
|
|
613
616
|
env: Mapping[str, str] | None = None,
|
|
614
617
|
) -> Self:
|
|
615
618
|
return cls(
|
|
619
|
+
audit=AuditConfig.from_mapping(_as_mapping(raw.get("audit"))),
|
|
616
620
|
model=ModelConfig.from_mapping(_as_mapping(raw.get("model")), env=env),
|
|
617
621
|
context=ContextConfig.from_mapping(_as_mapping(raw.get("context"))),
|
|
618
622
|
logging=LoggingConfig.from_mapping(_as_mapping(raw.get("logging"))),
|
|
@@ -646,6 +650,14 @@ def settings_to_toml_dict(settings: Settings, *, reveal_secret: bool = False) ->
|
|
|
646
650
|
data.pop("path", None)
|
|
647
651
|
if "ui" in data:
|
|
648
652
|
data["ui"].pop("theme_configured", None)
|
|
653
|
+
if "audit" in data:
|
|
654
|
+
data["audit"].pop("invalid_mode", None)
|
|
655
|
+
if "mode" in data["audit"] and isinstance(settings.audit.mode, AuditMode):
|
|
656
|
+
data["audit"]["mode"] = settings.audit.mode.value
|
|
657
|
+
if "mcp_safe_tools" in data["audit"]:
|
|
658
|
+
data["audit"]["mcp_safe_tools"] = [
|
|
659
|
+
{"server": item.server, "tool": item.tool} for item in settings.audit.mcp_safe_tools
|
|
660
|
+
]
|
|
649
661
|
api_key = settings.model.api_key
|
|
650
662
|
if api_key:
|
|
651
663
|
data["model"]["api_key"] = api_key if reveal_secret else mask_secret(api_key)
|
|
@@ -661,6 +673,10 @@ def is_valid_ui_view_mode(value: str) -> bool:
|
|
|
661
673
|
return value in UI_VIEW_MODES
|
|
662
674
|
|
|
663
675
|
|
|
676
|
+
def is_valid_config_audit_mode(value: str) -> bool:
|
|
677
|
+
return is_valid_audit_mode(value)
|
|
678
|
+
|
|
679
|
+
|
|
664
680
|
def is_supported_deepseek_model(value: str) -> bool:
|
|
665
681
|
return value in SUPPORTED_DEEPSEEK_MODELS
|
|
666
682
|
|
|
@@ -734,6 +750,10 @@ def write_config(
|
|
|
734
750
|
"thinking": thinking_enabled_for_mode(mode, provider),
|
|
735
751
|
"reasoning_effort": reasoning_effort_for_mode(mode, provider),
|
|
736
752
|
},
|
|
753
|
+
"audit": {
|
|
754
|
+
"mode": DEFAULT_AUDIT_MODE.value,
|
|
755
|
+
"mcp_safe_tools": [],
|
|
756
|
+
},
|
|
737
757
|
"context": {
|
|
738
758
|
"window_tokens": DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
739
759
|
"compact_trigger_ratio": DEFAULT_COMPACT_TRIGGER_RATIO,
|
|
@@ -873,6 +893,20 @@ def update_config_view_mode(config_path: Path, view_mode: str) -> None:
|
|
|
873
893
|
_write_private_toml(path, raw)
|
|
874
894
|
|
|
875
895
|
|
|
896
|
+
def update_config_audit_mode(config_path: Path, audit_mode: str) -> None:
|
|
897
|
+
if not is_valid_config_audit_mode(audit_mode):
|
|
898
|
+
raise ValueError("Audit mode must be one of: normal, auto, yolo.")
|
|
899
|
+
path = config_path.expanduser()
|
|
900
|
+
if path.suffix == ".json":
|
|
901
|
+
raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
|
|
902
|
+
raw = _read_toml_mapping(path)
|
|
903
|
+
audit = raw.get("audit")
|
|
904
|
+
audit_map = dict(audit) if isinstance(audit, Mapping) else {}
|
|
905
|
+
audit_map["mode"] = audit_mode
|
|
906
|
+
raw["audit"] = audit_map
|
|
907
|
+
_write_private_toml(path, raw)
|
|
908
|
+
|
|
909
|
+
|
|
876
910
|
def _read_toml_mapping(path: Path) -> dict[str, Any]:
|
|
877
911
|
if not path.exists():
|
|
878
912
|
return {}
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
|
+
from deepy.audit import AuditPolicy
|
|
6
7
|
from deepy.config import Settings
|
|
7
8
|
from deepy.mcp import sdk_mcp_tool_name
|
|
8
9
|
from deepy.prompts import build_system_prompt
|
|
@@ -28,6 +29,7 @@ def build_deepy_agent(
|
|
|
28
29
|
mcp_servers: list[MCPServer] | None = None,
|
|
29
30
|
preferred_mcp_web_search_tools: list[str] | None = None,
|
|
30
31
|
emit_event: Any | None = None,
|
|
32
|
+
audit_policy: AuditPolicy | None = None,
|
|
31
33
|
):
|
|
32
34
|
from agents import Agent
|
|
33
35
|
|
|
@@ -39,6 +41,7 @@ def build_deepy_agent(
|
|
|
39
41
|
settings.model.name,
|
|
40
42
|
),
|
|
41
43
|
preferred_mcp_web_search_tools=preferred_mcp_web_search_tools,
|
|
44
|
+
audit_policy=audit_policy,
|
|
42
45
|
)
|
|
43
46
|
subagent_tools = build_subagent_tools(
|
|
44
47
|
settings,
|
|
@@ -52,6 +55,7 @@ def build_deepy_agent(
|
|
|
52
55
|
settings.model.name,
|
|
53
56
|
),
|
|
54
57
|
emit_event=emit_event,
|
|
58
|
+
audit_policy=audit_policy,
|
|
55
59
|
)
|
|
56
60
|
return Agent(
|
|
57
61
|
name="Deepy",
|
|
@@ -89,6 +93,7 @@ def build_subagent_tools(
|
|
|
89
93
|
preferred_mcp_web_search_tools: list[str],
|
|
90
94
|
mimo_schema_compatibility: bool = False,
|
|
91
95
|
emit_event: Any | None = None,
|
|
96
|
+
audit_policy: AuditPolicy | None = None,
|
|
92
97
|
) -> list[Any]:
|
|
93
98
|
from agents import Agent
|
|
94
99
|
|
|
@@ -105,6 +110,7 @@ def build_subagent_tools(
|
|
|
105
110
|
mimo_schema_compatibility=mimo_schema_compatibility,
|
|
106
111
|
preferred_mcp_web_search_tools=preferred_mcp_web_search_tools,
|
|
107
112
|
include_tools=set(definition.tools),
|
|
113
|
+
audit_policy=audit_policy,
|
|
108
114
|
),
|
|
109
115
|
mcp_servers=_search_mcp_servers_for_subagent(
|
|
110
116
|
definition,
|
|
@@ -2,12 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import contextlib
|
|
5
|
+
import inspect
|
|
5
6
|
import time
|
|
6
|
-
from collections.abc import Callable
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
7
8
|
from dataclasses import dataclass, field
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Any, Literal
|
|
10
|
+
from typing import Any, Literal, cast
|
|
10
11
|
|
|
12
|
+
from deepy.audit import ApprovalDecision, AuditMode, AuditModeState, AuditPolicy, PendingApproval, parse_audit_mode
|
|
11
13
|
from deepy.config import Settings, load_settings
|
|
12
14
|
from deepy.sessions import DeepySession
|
|
13
15
|
from deepy.skills import find_skill
|
|
@@ -65,6 +67,12 @@ async def run_prompt_once(
|
|
|
65
67
|
background_tasks: BackgroundTaskManager | None = None,
|
|
66
68
|
should_interrupt: Callable[[], bool] | None = None,
|
|
67
69
|
cancel_mode: Literal["immediate", "after_turn"] = "immediate",
|
|
70
|
+
audit_mode: AuditMode | str | AuditModeState | None = None,
|
|
71
|
+
approval_resolver: Callable[
|
|
72
|
+
[list[PendingApproval]],
|
|
73
|
+
list[ApprovalDecision] | Awaitable[list[ApprovalDecision]],
|
|
74
|
+
]
|
|
75
|
+
| None = None,
|
|
68
76
|
) -> RunSummary:
|
|
69
77
|
from agents import RunConfig, Runner
|
|
70
78
|
from agents.exceptions import MaxTurnsExceeded, ModelBehaviorError
|
|
@@ -73,6 +81,10 @@ async def run_prompt_once(
|
|
|
73
81
|
root = (project_root or Path.cwd()).resolve()
|
|
74
82
|
resolved_settings = settings or load_settings()
|
|
75
83
|
resolved_provider = provider or build_provider_bundle(resolved_settings)
|
|
84
|
+
audit_state = audit_mode if isinstance(audit_mode, AuditModeState) else AuditModeState(
|
|
85
|
+
parse_audit_mode(audit_mode, default=resolved_settings.audit.mode)
|
|
86
|
+
)
|
|
87
|
+
audit_policy = AuditPolicy(lambda: audit_state.mode, resolved_settings.audit)
|
|
76
88
|
session = DeepySession.open(root, session_id) if session_id else DeepySession.create(root)
|
|
77
89
|
initial_todos, _ = normalize_todo_items(session.todo_state())
|
|
78
90
|
runtime = ToolRuntime(
|
|
@@ -84,7 +96,11 @@ async def run_prompt_once(
|
|
|
84
96
|
)
|
|
85
97
|
created_mcp_runtime: DeepyMcpRuntime | None = None
|
|
86
98
|
if mcp_runtime is None:
|
|
87
|
-
created_mcp_runtime = DeepyMcpRuntime(
|
|
99
|
+
created_mcp_runtime = DeepyMcpRuntime(
|
|
100
|
+
resolved_settings,
|
|
101
|
+
project_root=root,
|
|
102
|
+
audit_policy=audit_policy,
|
|
103
|
+
)
|
|
88
104
|
mcp_runtime = created_mcp_runtime
|
|
89
105
|
await mcp_runtime.connect()
|
|
90
106
|
loaded_skills = _resolve_loaded_skills(root, prompt, skill_names)
|
|
@@ -97,6 +113,7 @@ async def run_prompt_once(
|
|
|
97
113
|
mcp_servers=mcp_runtime.active_servers,
|
|
98
114
|
preferred_mcp_web_search_tools=mcp_runtime.preferred_web_search_tools,
|
|
99
115
|
emit_event=emit_event,
|
|
116
|
+
audit_policy=audit_policy,
|
|
100
117
|
)
|
|
101
118
|
prefix_snapshot = build_cache_prefix_snapshot(
|
|
102
119
|
resolved_settings,
|
|
@@ -158,49 +175,74 @@ async def run_prompt_once(
|
|
|
158
175
|
prefix_token: Any | None = None
|
|
159
176
|
try:
|
|
160
177
|
prefix_token = set_current_cache_prefix_snapshot(prefix_snapshot)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
interrupt_task = asyncio.create_task(
|
|
170
|
-
_watch_stream_interrupt(
|
|
171
|
-
result,
|
|
172
|
-
should_interrupt=should_interrupt,
|
|
173
|
-
cancel_mode=cancel_mode,
|
|
174
|
-
)
|
|
178
|
+
run_input: Any = prompt
|
|
179
|
+
while True:
|
|
180
|
+
result = Runner.run_streamed(
|
|
181
|
+
agent,
|
|
182
|
+
input=run_input,
|
|
183
|
+
max_turns=max_turns,
|
|
184
|
+
run_config=run_config,
|
|
185
|
+
session=session, # ty: ignore[invalid-argument-type] - DeepySession matches the SDK Session protocol at runtime.
|
|
175
186
|
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if normalized.kind == "tool_output":
|
|
189
|
-
questions = _pending_questions_from_tool_output(normalized.text)
|
|
190
|
-
if questions:
|
|
191
|
-
pending_questions = questions
|
|
192
|
-
waiting_for_user = True
|
|
193
|
-
_cancel_stream_result(result, mode="after_turn")
|
|
187
|
+
if should_interrupt is not None:
|
|
188
|
+
interrupt_task = asyncio.create_task(
|
|
189
|
+
_watch_stream_interrupt(
|
|
190
|
+
result,
|
|
191
|
+
should_interrupt=should_interrupt,
|
|
192
|
+
cancel_mode=cancel_mode,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
async for event in result.stream_events():
|
|
196
|
+
if should_interrupt is not None and should_interrupt():
|
|
197
|
+
_cancel_stream_result(result, mode=cancel_mode)
|
|
198
|
+
interrupted = True
|
|
194
199
|
break
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
normalized = normalize_stream_event(event)
|
|
201
|
+
if normalized is None:
|
|
202
|
+
continue
|
|
203
|
+
if normalized.kind == "usage":
|
|
204
|
+
usage = merge_usage(usage, normalize_usage(normalized.payload.get("usage")))
|
|
205
|
+
if emit_event is not None:
|
|
206
|
+
emit_event(normalized)
|
|
207
|
+
if normalized.kind == "tool_output":
|
|
208
|
+
questions = _pending_questions_from_tool_output(normalized.text)
|
|
209
|
+
if questions:
|
|
210
|
+
pending_questions = questions
|
|
211
|
+
waiting_for_user = True
|
|
212
|
+
_cancel_stream_result(result, mode="after_turn")
|
|
213
|
+
break
|
|
214
|
+
if normalized.kind != "text_delta" or not normalized.text:
|
|
215
|
+
continue
|
|
216
|
+
chunks.append(normalized.text)
|
|
217
|
+
if emit is not None:
|
|
218
|
+
emit(normalized.text)
|
|
219
|
+
if should_interrupt is not None and should_interrupt():
|
|
220
|
+
_cancel_stream_result(result, mode=cancel_mode)
|
|
221
|
+
interrupted = True
|
|
222
|
+
break
|
|
223
|
+
interrupted = interrupted or await _finish_interrupt_task(interrupt_task)
|
|
224
|
+
interrupt_task = None
|
|
225
|
+
if interrupted or waiting_for_user:
|
|
203
226
|
break
|
|
227
|
+
interruptions = list(getattr(result, "interruptions", []) or [])
|
|
228
|
+
if not interruptions:
|
|
229
|
+
break
|
|
230
|
+
state = result.to_state()
|
|
231
|
+
decisions = await _approval_decisions(
|
|
232
|
+
interruptions,
|
|
233
|
+
approval_resolver=approval_resolver,
|
|
234
|
+
)
|
|
235
|
+
for interruption, decision in zip(interruptions, decisions, strict=False):
|
|
236
|
+
if decision.outcome == "approve":
|
|
237
|
+
state.approve(interruption, always_approve=decision.always)
|
|
238
|
+
else:
|
|
239
|
+
state.reject(
|
|
240
|
+
interruption,
|
|
241
|
+
always_reject=decision.always,
|
|
242
|
+
rejection_message=decision.rejection_message
|
|
243
|
+
or "Tool execution was rejected by the user audit approval decision.",
|
|
244
|
+
)
|
|
245
|
+
run_input = state
|
|
204
246
|
if prefix_token is not None:
|
|
205
247
|
reset_current_cache_prefix_snapshot(prefix_token)
|
|
206
248
|
prefix_token = None
|
|
@@ -375,6 +417,123 @@ async def _cleanup_created_mcp(mcp_runtime: DeepyMcpRuntime | None) -> None:
|
|
|
375
417
|
await mcp_runtime.cleanup()
|
|
376
418
|
|
|
377
419
|
|
|
420
|
+
async def _approval_decisions(
|
|
421
|
+
interruptions: list[Any],
|
|
422
|
+
*,
|
|
423
|
+
approval_resolver: Callable[
|
|
424
|
+
[list[PendingApproval]],
|
|
425
|
+
list[ApprovalDecision] | Awaitable[list[ApprovalDecision]],
|
|
426
|
+
]
|
|
427
|
+
| None,
|
|
428
|
+
) -> list[ApprovalDecision]:
|
|
429
|
+
pending = [_pending_approval_from_interruption(index, item) for index, item in enumerate(interruptions)]
|
|
430
|
+
if approval_resolver is None:
|
|
431
|
+
return [
|
|
432
|
+
ApprovalDecision(
|
|
433
|
+
outcome="reject",
|
|
434
|
+
rejection_message="Tool execution requires audit approval, but no approval UI is available.",
|
|
435
|
+
)
|
|
436
|
+
for _ in pending
|
|
437
|
+
]
|
|
438
|
+
resolved = approval_resolver(pending)
|
|
439
|
+
if inspect.isawaitable(resolved):
|
|
440
|
+
resolved = await resolved
|
|
441
|
+
decisions = list(cast(list[ApprovalDecision], resolved))
|
|
442
|
+
if len(decisions) < len(pending):
|
|
443
|
+
decisions = [
|
|
444
|
+
*decisions,
|
|
445
|
+
*[
|
|
446
|
+
ApprovalDecision(
|
|
447
|
+
outcome="reject",
|
|
448
|
+
rejection_message="Tool execution was rejected because no audit decision was provided.",
|
|
449
|
+
)
|
|
450
|
+
for _ in range(len(pending) - len(decisions))
|
|
451
|
+
],
|
|
452
|
+
]
|
|
453
|
+
return decisions[: len(pending)]
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _pending_approval_from_interruption(index: int, item: Any) -> PendingApproval:
|
|
457
|
+
raw_item = getattr(item, "raw_item", None)
|
|
458
|
+
tool_name = _approval_tool_name(item, raw_item)
|
|
459
|
+
arguments = _approval_arguments(item, raw_item)
|
|
460
|
+
agent = getattr(item, "agent", None)
|
|
461
|
+
agent_name = str(getattr(agent, "name", "") or "")
|
|
462
|
+
server_name = _approval_server_name(raw_item, tool_name)
|
|
463
|
+
return PendingApproval(
|
|
464
|
+
index=index,
|
|
465
|
+
name=str(getattr(item, "name", "") or tool_name or "tool"),
|
|
466
|
+
tool_name=tool_name,
|
|
467
|
+
arguments=arguments,
|
|
468
|
+
agent_name=agent_name,
|
|
469
|
+
action_kind="mcp_tool" if server_name else _approval_action_kind(tool_name),
|
|
470
|
+
server_name=server_name,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _approval_tool_name(item: Any, raw_item: Any) -> str:
|
|
475
|
+
for value in (
|
|
476
|
+
getattr(item, "tool_name", None),
|
|
477
|
+
getattr(item, "name", None),
|
|
478
|
+
getattr(raw_item, "name", None),
|
|
479
|
+
):
|
|
480
|
+
if isinstance(value, str) and value:
|
|
481
|
+
return value
|
|
482
|
+
if isinstance(raw_item, dict):
|
|
483
|
+
value = raw_item.get("name")
|
|
484
|
+
if isinstance(value, str):
|
|
485
|
+
return value
|
|
486
|
+
function = raw_item.get("function")
|
|
487
|
+
if isinstance(function, dict) and isinstance(function.get("name"), str):
|
|
488
|
+
return function["name"]
|
|
489
|
+
return ""
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _approval_arguments(item: Any, raw_item: Any) -> str:
|
|
493
|
+
for value in (getattr(item, "arguments", None), getattr(raw_item, "arguments", None)):
|
|
494
|
+
if isinstance(value, str):
|
|
495
|
+
return value
|
|
496
|
+
if isinstance(raw_item, dict):
|
|
497
|
+
value = raw_item.get("arguments")
|
|
498
|
+
if isinstance(value, str):
|
|
499
|
+
return value
|
|
500
|
+
function = raw_item.get("function")
|
|
501
|
+
if isinstance(function, dict) and isinstance(function.get("arguments"), str):
|
|
502
|
+
return function["arguments"]
|
|
503
|
+
arguments = raw_item.get("arguments_json") or raw_item.get("input")
|
|
504
|
+
if arguments is not None:
|
|
505
|
+
return json_utils.dumps(arguments)
|
|
506
|
+
arguments = getattr(raw_item, "arguments_json", None)
|
|
507
|
+
if arguments is not None:
|
|
508
|
+
return json_utils.dumps(arguments)
|
|
509
|
+
return ""
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _approval_server_name(raw_item: Any, tool_name: str) -> str:
|
|
513
|
+
for attr in ("server_label", "server_name"):
|
|
514
|
+
value = getattr(raw_item, attr, None)
|
|
515
|
+
if isinstance(value, str) and value:
|
|
516
|
+
return value
|
|
517
|
+
if isinstance(raw_item, dict):
|
|
518
|
+
for key in ("server_label", "server_name"):
|
|
519
|
+
value = raw_item.get(key)
|
|
520
|
+
if isinstance(value, str) and value:
|
|
521
|
+
return value
|
|
522
|
+
if "__" in tool_name:
|
|
523
|
+
return tool_name.split("__", 1)[0]
|
|
524
|
+
return ""
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _approval_action_kind(tool_name: str) -> str:
|
|
528
|
+
if tool_name in {"Write", "Update"}:
|
|
529
|
+
return "text_write"
|
|
530
|
+
if tool_name == "shell":
|
|
531
|
+
return "command"
|
|
532
|
+
if tool_name == "task_stop":
|
|
533
|
+
return "background_task_control"
|
|
534
|
+
return "tool"
|
|
535
|
+
|
|
536
|
+
|
|
378
537
|
def _resolve_loaded_skills(
|
|
379
538
|
root: Path,
|
|
380
539
|
prompt: str,
|