) => {
+ setLocalSettings((prev) => {
+ if (prev === null) {
+ return null;
+ }
+ return { ...prev, ...update };
+ });
+ handleUpdateSettings(update);
+ };
+
+ return (
+
+
+ {localSettings === null ? (
+
+ ) : (
+ handleValueChange({ smart_approve: checked })}
+ disabled={disabled}
+ />
+ )}
+
+
+
+ {localSettings === null ? (
+
+ ) : (
+ handleValueChange({ smart_approve_threshold: Number(e.target.value) })}
+ disabled={disabled}
+ />
+ )}
+
+
+ );
+}
diff --git a/src-frontend/src/features/SideBar/views/SettingsView/index.tsx b/src-frontend/src/features/SideBar/views/SettingsView/index.tsx
index ab8412d1..14c05324 100644
--- a/src-frontend/src/features/SideBar/views/SettingsView/index.tsx
+++ b/src-frontend/src/features/SideBar/views/SettingsView/index.tsx
@@ -3,11 +3,12 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { SIDEBAR_NAMESPACE } from "@/i18n/resources";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
-import { SideBarHeader } from "../../components/SideBarHeader";
-import { DevSettings } from "./DevSettings";
import { GeneralSettings } from "./GeneralSettings";
-import { HelperModelSettings } from "./HelperModelSettings";
import { ProviderSettings } from "./ProviderSettings";
+import { HelperModelSettings } from "./HelperModelSettings";
+import { AgentSettings } from "./AgentSettings";
+import { DevSettings } from "./DevSettings";
+import { SideBarHeader } from "../../components/SideBarHeader";
export function SettingsView() {
const { t } = useTranslation(SIDEBAR_NAMESPACE);
@@ -28,6 +29,11 @@ export function SettingsView() {
title: t("settings.tabs.helper_model"),
content: ,
},
+ {
+ id: "agents",
+ title: t("settings.tabs.agents"),
+ content: ,
+ },
{
id: "dev",
title: t("settings.tabs.dev"),
diff --git a/src-frontend/src/features/Tabs/TaskPanel/components/messages/GeneralToolMessage.tsx b/src-frontend/src/features/Tabs/TaskPanel/components/messages/GeneralToolMessage.tsx
index 2e3533bd..e45245df 100644
--- a/src-frontend/src/features/Tabs/TaskPanel/components/messages/GeneralToolMessage.tsx
+++ b/src-frontend/src/features/Tabs/TaskPanel/components/messages/GeneralToolMessage.tsx
@@ -7,6 +7,7 @@ import { useAgentTaskAction } from "../../hooks/use-agent-task";
import { useToolName } from "../../hooks/use-tool-name";
import { useToolState } from "../../hooks/use-tool-state";
import { shouldShowConfirmation, ToolConfirmation } from "./BuiltInToolMessage/components/ToolConfirmation";
+import { ToolMessageMetadata } from "@/api/generated/schemas";
export function GeneralToolMessage({ message }: ToolMessageProps) {
const { reviewTool } = useAgentTaskAction();
@@ -21,6 +22,7 @@ export function GeneralToolMessage({ message }: ToolMessageProps) {
toolName={toolName}
toolsetName={toolsetName}
state={toolState}
+ riskLevel={(message.metadata as ToolMessageMetadata).risk_level}
/>
diff --git a/src-frontend/src/i18n/locales/en/sidebar.json b/src-frontend/src/i18n/locales/en/sidebar.json
index cb49e227..589de27c 100644
--- a/src-frontend/src/i18n/locales/en/sidebar.json
+++ b/src-frontend/src/i18n/locales/en/sidebar.json
@@ -27,6 +27,14 @@
"placeholder": "Plugins view"
},
"settings": {
+ "agents": {
+ "smart_approve": {
+ "title": "Smart approve"
+ },
+ "smart_approve_threshold": {
+ "title": "Smart approve threshold"
+ }
+ },
"dev": {
"devtools": {
"open_button": "Open",
@@ -85,6 +93,7 @@
}
},
"tabs": {
+ "agents": "Agents",
"dev": "Development",
"general": "General",
"helper_model": "Helper model",
diff --git a/src-frontend/src/i18n/locales/zh_CN/sidebar.json b/src-frontend/src/i18n/locales/zh_CN/sidebar.json
index e65640a3..6de481b7 100644
--- a/src-frontend/src/i18n/locales/zh_CN/sidebar.json
+++ b/src-frontend/src/i18n/locales/zh_CN/sidebar.json
@@ -27,6 +27,14 @@
"placeholder": "插件视图"
},
"settings": {
+ "agents": {
+ "smart_approve": {
+ "title": "智能批准"
+ },
+ "smart_approve_threshold": {
+ "title": "智能批准阈值"
+ }
+ },
"dev": {
"devtools": {
"open_button": "打开",
@@ -85,6 +93,7 @@
}
},
"tabs": {
+ "agents": "Agents",
"dev": "开发",
"general": "通用",
"helper_model": "助手模型",
diff --git a/src-frontend/src/stores/server-settings-store.ts b/src-frontend/src/stores/server-settings-store.ts
index 24e2cccc..5eac2e6e 100644
--- a/src-frontend/src/stores/server-settings-store.ts
+++ b/src-frontend/src/stores/server-settings-store.ts
@@ -6,7 +6,7 @@ type ServerSettingsStore = {
current: AppSettings | null;
currentPromise: Promise;
isLoading: boolean;
- setPartial: (settings: Partial) => void;
+ setPartial: (settings: Partial) => Promise | null;
};
export const useServerSettingsStore = create()((set, get) => ({
@@ -28,5 +28,6 @@ export const useServerSettingsStore = create()((set, get) =
return settings;
});
set({ currentPromise: updatePromise });
+ return updatePromise;
},
}));
diff --git a/src-frontend/src/styles/base.css b/src-frontend/src/styles/base.css
index 5df4de68..ef101d9a 100644
--- a/src-frontend/src/styles/base.css
+++ b/src-frontend/src/styles/base.css
@@ -11,3 +11,7 @@ body {
input[type="password"]::-ms-reveal {
display: none !important;
}
+
+.dark input[type="number"] {
+ color-scheme: dark;
+}
diff --git a/src-server/src/api/routes/settings.py b/src-server/src/api/routes/settings.py
index 0182d900..99ac22bf 100644
--- a/src-server/src/api/routes/settings.py
+++ b/src-server/src/api/routes/settings.py
@@ -1,4 +1,3 @@
-import asyncio
from fastapi import APIRouter
from ...settings import AppSettings, use_app_setting_manager
From 99ce8c913918b6f7e564b9141154e5e67ffe8400 Mon Sep 17 00:00:00 2001
From: BHznJNs <441768875@qq.com>
Date: Mon, 16 Mar 2026 20:07:33 +0800
Subject: [PATCH 3/3] backend tool call safety audit
---
src-server/pyproject.toml | 2 +-
.../one_turns/tool_call_safety_audit.py | 43 +++---
src-server/src/agent/task/__init__.py | 6 +-
.../src/agent/task/tool_call_dispatcher.py | 122 ++++++++++++------
.../src/agent/task/tool_call_reviewer.py | 59 ++++++---
.../agent/tool/builtin_tools/file_system.py | 2 +
.../src/api/routes/task/context_file.py | 4 +-
src-server/uv.lock | 8 +-
8 files changed, 158 insertions(+), 88 deletions(-)
diff --git a/src-server/pyproject.toml b/src-server/pyproject.toml
index 5d417bf3..a047a695 100644
--- a/src-server/pyproject.toml
+++ b/src-server/pyproject.toml
@@ -3,7 +3,7 @@ name = "dais-server"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
- "dais-sdk==0.8.15",
+ "dais-sdk==0.8.16",
"dais-shell==0.1.2",
"alembic==1.18.4",
diff --git a/src-server/src/agent/prompts/one_turns/tool_call_safety_audit.py b/src-server/src/agent/prompts/one_turns/tool_call_safety_audit.py
index 2a126b12..2077c4bb 100644
--- a/src-server/src/agent/prompts/one_turns/tool_call_safety_audit.py
+++ b/src-server/src/agent/prompts/one_turns/tool_call_safety_audit.py
@@ -1,5 +1,5 @@
INSTRUCTION = """\
-You are a security review module for an AI agent system. Your sole responsibility is to assess the danger level of pending tool calls before they are executed.
+You are a security review module for an AI agent system. Your sole responsibility is to assess the risk level of pending tool calls before they are executed.
## Inputs
@@ -41,8 +41,8 @@
```
- {unique id}
- {name of the tool}
+ {unique id}
+ {name of the tool}
{tool arguments, typically a JSON object}
...
@@ -54,17 +54,22 @@
## Your Task
-For each pending tool call, assign a **danger level** from 0 to 100 (multiples of 10 only: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).
-Output a single JSON object array, with each `tool_call_id` and its danger level string. No explanation, no additional text - only the JSON.
+For each pending tool call, assign a **risk level** from 0 to 100 (multiples of 10 only: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).
+Output a single JSON object array, with each `tool_call_id` and its risk level string. No explanation, no additional text - only the JSON.
-**Output format:**
-[
- {"call_id": "tool_call_id1", "danger_level": 10},
- {"call_id": "tool_call_id2", "danger_level": 50},
- ...
-]
+### Output format
+
+```
+{
+ "results": [
+ {"call_id": "tool_call_id1", "risk_level": 10},
+ {"call_id": "tool_call_id2", "risk_level": 50},
+ ...
+ ]
+}
+```
-## Danger Level Reference
+## Risk Level Reference
| Level | Meaning | Typical Examples |
|-------|---------|-----------------|
@@ -74,10 +79,10 @@
| 30 | Low risk, reversible with minor effort | Modify a non-critical config, send an internal notification |
| 40 | Moderate risk, reversible but with some effort | Overwrite an existing file, restart a non-critical service, fetch a public URL (read-only GET, no sensitive data in request) |
| 50 | Medium risk, partial or uncertain reversibility | Batch-modify records, send an external email, install a package |
-| 60 | Elevated risk, difficult to reverse | Delete non-critical data, modify production config |
+| 60 | Elevated risk, difficult to reverse | Delete non-critical data, modify production config, write to or edit files outside the working directory |
| 70 | High risk, hard to reverse, potential for significant impact | Delete important files, stop a critical service, bulk data changes |
| 80 | Very high risk, likely irreversible or severe impact | Drop a database table, revoke access credentials, mass-delete records |
-| 90 | Extremely high risk OR danger level cannot be determined (see Rule 5) | Destructive system commands, unknown/opaque commands, missing critical context |
+| 90 | Extremely high risk OR risk level cannot be determined (see Rule 5) | Destructive system commands, unknown/opaque commands, missing critical context |
| 100 | Catastrophic, certainly irreversible, existential impact | Format a disk, wipe all data, terminate all system processes |
## Scoring Rules
@@ -170,14 +175,14 @@ def context_xml(context: list[Message]) -> ET.Element:
case _: ... # do nothing for other message types
return root
- def pending_tool_calls_xml(pending_tool_calls: list[ToolMessage]) -> ET.Element:
+ def pending_tool_calls_xml(pending_tool_messages: list[ToolMessage]) -> ET.Element:
root = ET.Element("pending_tool_calls")
- for tc in pending_tool_calls:
+ for message in pending_tool_messages:
tool_call_elem = ET.SubElement(root, "tool_call")
- ET.SubElement(tool_call_elem, "tool_call_id").text = tc.id
- ET.SubElement(tool_call_elem, "name").text = tc.name
- ET.SubElement(tool_call_elem, "arguments").text = json.dumps(tc.arguments, ensure_ascii=False)
+ ET.SubElement(tool_call_elem, "call_id").text = message.call_id
+ ET.SubElement(tool_call_elem, "name").text = message.name
+ ET.SubElement(tool_call_elem, "arguments").text = json.dumps(message.arguments, ensure_ascii=False)
return root
return "".join([ET.tostring(el, encoding="unicode") for el in (
diff --git a/src-server/src/agent/task/__init__.py b/src-server/src/agent/task/__init__.py
index 76c75769..99a4473a 100644
--- a/src-server/src/agent/task/__init__.py
+++ b/src-server/src/agent/task/__init__.py
@@ -118,7 +118,7 @@ async def approve_tool_call(self, call_id: str, approved: bool) -> AsyncGenerato
case ToolCallBlocked(event):
yield event
case ToolCallApproved():
- yield await self._tool_call_dispatcher.execute(tool, message=target_message)
+ yield await self._tool_call_dispatcher.execute(tool, target_message)
yield MessageReplaceEvent(message=target_message)
async def run(self) -> AgentGenerator:
@@ -161,7 +161,9 @@ async def run(self) -> AgentGenerator:
self._tool_call_dispatcher.dispatch(tool_call_messages)
async for event in dispatch_stream:
yield event
- if dispatch_result.has_finished_task or len(dispatch_result.pendings) > 0:
+ if (dispatch_result.has_finished_task or
+ dispatch_result.has_blocked_tool_calls):
+ self._is_running = False
break
except GeneratorExit:
_exited_by_generator_close = True
diff --git a/src-server/src/agent/task/tool_call_dispatcher.py b/src-server/src/agent/task/tool_call_dispatcher.py
index e4f545af..33f64c6c 100644
--- a/src-server/src/agent/task/tool_call_dispatcher.py
+++ b/src-server/src/agent/task/tool_call_dispatcher.py
@@ -4,10 +4,13 @@
from typing import AsyncGenerator
from dais_sdk.tool import ToolCallExecutor
from loguru import logger
-from dais_sdk.types import ToolDef, ToolLike, ToolMessage, ToolDoesNotExistError
+from dais_sdk.types import ToolDef, ToolMessage, ToolDoesNotExistError
from .tool_call_reviewer import ToolCallReviewer, ToolCallBlocked, ToolCallApproved
from ..context import AgentContext
-from ..types import ToolEvent, ToolExecutedEvent, MessageReplaceEvent, ToolCallEndEvent, ErrorEvent
+from ..types import (
+ ToolEvent, ToolExecutedEvent, MessageReplaceEvent, ErrorEvent,
+ ToolRequirePermissionEvent,
+)
from ..exception_handlers import handle_tool_does_not_exist_error
from ..tool import ExecutionControlToolset
@@ -15,7 +18,12 @@
@dataclass
class ToolCallDispatchResult:
has_finished_task: bool
- pendings: list[ToolMessage]
+ has_blocked_tool_calls: bool
+
+@dataclass
+class ToolCallDispatch:
+ message: ToolMessage
+ tool: ToolDef
class ToolCallDispatcher:
_logger = logger.bind(name="ToolCallDispatcher")
@@ -25,7 +33,7 @@ def __init__(self, ctx: AgentContext, tool_call_executor: ToolCallExecutor, tool
self._tool_call_executor = tool_call_executor
self._tool_call_reviewer = tool_call_reviewer
- async def execute(self, tool: ToolDef, message: ToolMessage) -> ToolEvent:
+ async def execute(self, tool: ToolDef, message: ToolMessage) -> ToolExecutedEvent:
"""
Execute tool call and attach the result to the corresponding message.
This method should not throw any exceptions.
@@ -38,56 +46,92 @@ async def execute(self, tool: ToolDef, message: ToolMessage) -> ToolEvent:
call_id=message.call_id,
result=result if error is None else None)
+ async def _classify(self,
+ dispatches: list[ToolCallDispatch]
+ ) -> tuple[
+ list[tuple[ToolCallApproved, ToolCallDispatch]],
+ list[tuple[ToolCallBlocked, ToolCallDispatch]]
+ ]:
+ approved: list[tuple[ToolCallApproved, ToolCallDispatch]] = []
+ blocked: list[tuple[ToolCallBlocked, ToolCallDispatch]] = []
+
+ for dispatch in dispatches:
+ tool, message = dispatch.tool, dispatch.message
+ permission_check_result =\
+ await self._tool_call_reviewer.check_permission(tool, message)
+ match permission_check_result:
+ case ToolCallApproved() as approved_event:
+ approved.append((approved_event, dispatch))
+ case ToolCallBlocked() as blocked_event:
+ blocked.append((blocked_event, dispatch))
+
+ if len(blocked) == 0:
+ return approved, blocked
+
+ waiting_audit: list[ToolCallDispatch] = []
+ remaining_blocked: list[tuple[ToolCallBlocked, ToolCallDispatch]] = []
+ for blocked_event, dispatch in blocked:
+ if isinstance(blocked_event.event, ToolRequirePermissionEvent):
+ waiting_audit.append(dispatch)
+ else:
+ remaining_blocked.append((blocked_event, dispatch))
+
+ audit_result =\
+ await self._tool_call_reviewer.audit_tool_calls(waiting_audit)
+ if audit_result is None:
+ return approved, blocked
+
+ high_risk, low_risk = audit_result
+ approved.extend((ToolCallApproved(), dispatch) for dispatch in low_risk)
+ blocked = remaining_blocked + [
+ (ToolCallBlocked(ToolRequirePermissionEvent(
+ call_id=dispatch.message.call_id,
+ tool_name=dispatch.tool.name,
+ )), dispatch)
+ for dispatch in high_risk
+ ]
+ return approved, blocked
+
async def _dispatch_stream(self,
tool_call_messages: list[ToolMessage],
result: ToolCallDispatchResult,
) -> AsyncGenerator[ToolEvent
| MessageReplaceEvent
| ErrorEvent, None]:
- executables = list[tuple[ToolDef, ToolMessage]]()
+ dispatches: list[ToolCallDispatch] = []
for message in tool_call_messages:
tool = self._ctx.find_tool(message.name)
if tool is None:
message.error = handle_tool_does_not_exist_error(ToolDoesNotExistError(message.name))
yield MessageReplaceEvent(message=message)
continue
-
- # Since the toolsets only contain ToolDefs, and the tools are all under toolsets,
- # so we can safely assert the type of tool_def to ToolDef here.
- assert isinstance(tool, ToolDef)
-
if tool.executes(ExecutionControlToolset.finish_task):
result.has_finished_task = True
+ dispatches.append(ToolCallDispatch(message=message, tool=tool))
- permission_check_result = await self._tool_call_reviewer.check_permission(tool, message)
- match permission_check_result:
- case ToolCallBlocked(event):
- yield event
- yield MessageReplaceEvent(message=message)
- result.pendings.append(message)
- case ToolCallApproved():
- executables.append((tool, message))
-
- if len(result.pendings) > 0:
- audit_result = await self._tool_call_reviewer.audit_tool_calls(result.pendings)
- if audit_result is not None:
- high_risk, low_risk = audit_result
- result.pendings = high_risk
- for message in low_risk:
- tool = self._ctx.find_tool(message.name)
- if tool is None: continue
- executables.append((tool, message))
-
- if len(executables) > 0:
- execute_tasks = [self.execute(tool, message) for tool, message in executables]
- events = await asyncio.gather(*execute_tasks, return_exceptions=True)
- for event, (_, message) in zip(events, executables):
- if isinstance(event, BaseException):
- self._logger.exception(f"Error in tool call {message.call_id}")
- continue
- yield event
- yield MessageReplaceEvent(message=message)
+ approved, blocked = await self._classify(dispatches)
+
+ result.has_blocked_tool_calls = len(blocked) > 0
+ for blocked_event, dispatch in blocked:
+ yield blocked_event.event
+ yield MessageReplaceEvent(message=dispatch.message)
+
+ async def execute_wrapper(dispatch: ToolCallDispatch):
+ executed_event = await self.execute(dispatch.tool, dispatch.message)
+ return executed_event, MessageReplaceEvent(message=dispatch.message)
+
+ execute_tasks = [execute_wrapper(dispatch) for _, dispatch in approved]
+ for item in await asyncio.gather(*execute_tasks, return_exceptions=True):
+ if isinstance(item, BaseException):
+ self._logger.exception(f"Tool call execution error: ", exc_info=item)
+ continue
+ executed_event, replace_event = item
+ yield executed_event
+ yield replace_event
def dispatch(self, tool_call_messages: list[ToolMessage]) -> tuple[AsyncGenerator[ToolEvent | MessageReplaceEvent | ErrorEvent, None], ToolCallDispatchResult]:
- result = ToolCallDispatchResult(has_finished_task=False, pendings=[])
+ result = ToolCallDispatchResult(
+ has_finished_task=False,
+ has_blocked_tool_calls=False,
+ )
return self._dispatch_stream(tool_call_messages, result), result
diff --git a/src-server/src/agent/task/tool_call_reviewer.py b/src-server/src/agent/task/tool_call_reviewer.py
index 6c39dff6..368ec551 100644
--- a/src-server/src/agent/task/tool_call_reviewer.py
+++ b/src-server/src/agent/task/tool_call_reviewer.py
@@ -1,3 +1,4 @@
+from typing import TYPE_CHECKING
from dataclasses import dataclass
from loguru import logger
from dais_sdk.types import ToolDef, ToolMessage
@@ -6,7 +7,7 @@
from ..prompts import (
create_one_turn_llm,
USER_DENIED_TOOL_CALL_RESULT,
- ToolCallSafetyAudit, ToolCallSafetyAuditInput, ToolCallSafetyAuditOutput,
+ ToolCallSafetyAudit, ToolCallSafetyAuditInput,
)
from ..context import AgentContext
from ..types import (
@@ -16,6 +17,10 @@
from ..types.metadata import ToolMessageMetadata, UserApprovalStatus, is_agent_tool_metadata
from ...settings import use_app_setting_manager
+if TYPE_CHECKING:
+ from .tool_call_dispatcher import ToolCallDispatch
+
+
@dataclass
class ToolCallBlocked:
event: ToolEvent
@@ -29,7 +34,12 @@ class ToolCallReviewer:
def __init__(self, ctx: AgentContext):
self._ctx = ctx
- async def audit_tool_calls(self, messages: list[ToolMessage]) -> tuple[list[ToolMessage], list[ToolMessage]] | None:
+ async def audit_tool_calls(self,
+ dispatches: list[ToolCallDispatch]
+ ) -> tuple[
+ list[ToolCallDispatch],
+ list[ToolCallDispatch]
+ ] | None:
"""
Side effect: The risk level will be attached to the metadata of each message.
@@ -37,6 +47,9 @@ async def audit_tool_calls(self, messages: list[ToolMessage]) -> tuple[list[Tool
- Tuple of (high_risk, low_risk)
- None if smart approve is disabled or no flash model is configured.
"""
+ if len(dispatches) == 0:
+ return [], []
+
settings = use_app_setting_manager().settings
if not settings.smart_approve:
self._logger.info("Smart approve is disabled, skipping smart approve")
@@ -45,18 +58,20 @@ async def audit_tool_calls(self, messages: list[ToolMessage]) -> tuple[list[Tool
self._logger.warning("No flash model configured, skipping smart approve")
return None
- llm = await create_one_turn_llm(settings.flash_model)
+ try:
+ llm = await create_one_turn_llm(settings.flash_model)
+ except Exception:
+ self._logger.exception("Failed to create LLM for smart approve")
+ return None
+
audit_context_size = 5
context = self._ctx.messages[-audit_context_size:]
safety_audit = ToolCallSafetyAudit(llm)
- tooldefs: list[ToolDef] = []
- for message in messages:
- tool = self._ctx.find_tool(message.name)
- if tool is not None: tooldefs.append(tool)
-
+ tools = [dispatch.tool for dispatch in dispatches]
+ messages = [dispatch.message for dispatch in dispatches]
input = ToolCallSafetyAuditInput(
- tool_definitions=prepare_tools(tooldefs),
+ tool_definitions=prepare_tools(tools),
context=context,
pending_tool_calls=messages
)
@@ -64,25 +79,27 @@ async def audit_tool_calls(self, messages: list[ToolMessage]) -> tuple[list[Tool
# attach risk level to each message
for item in output.results:
- for message in messages:
- if message.call_id == item.call_id:
- assert is_agent_tool_metadata(message.metadata)
- message.metadata["risk_level"] = item.risk_level
+ for dispatch in dispatches:
+ if dispatch.message.call_id == item.call_id:
+ assert is_agent_tool_metadata(dispatch.message.metadata)
+ dispatch.message.metadata["risk_level"] = item.risk_level
break
else:
self._logger.warning(f"Tool call {item.call_id} not found")
continue
# split messages into two groups: high risk and low risk
- high_risk = []
- low_risk = []
- for message in messages:
- assert is_agent_tool_metadata(message.metadata)
- assert "risk_level" in message.metadata
- if message.metadata["risk_level"] > settings.smart_approve_threshold:
- high_risk.append(message)
+ high_risk: list[ToolCallDispatch] = []
+ low_risk: list[ToolCallDispatch] = []
+ for dispatch in dispatches:
+ assert is_agent_tool_metadata(dispatch.message.metadata)
+ if "risk_level" not in dispatch.message.metadata:
+ self._logger.warning(f"Tool call {dispatch.message.call_id} has no risk level")
+ continue
+ if dispatch.message.metadata["risk_level"] > settings.smart_approve_threshold:
+ high_risk.append(dispatch)
else:
- low_risk.append(message)
+ low_risk.append(dispatch)
return high_risk, low_risk
def apply_user_approval(self,
diff --git a/src-server/src/agent/tool/builtin_tools/file_system.py b/src-server/src/agent/tool/builtin_tools/file_system.py
index ec79c355..18501c7a 100644
--- a/src-server/src/agent/tool/builtin_tools/file_system.py
+++ b/src-server/src/agent/tool/builtin_tools/file_system.py
@@ -10,6 +10,8 @@
from ....utils.scandir_recursive import scandir_recursive_bfs
from ....utils.ignore_rules import load_gitignore_spec, should_exclude
+# TODO: use to_thread to prevent blocking
+
class FileSystemToolset(BuiltInToolset):
def __init__(self,
ctx: BuiltInToolsetContext,
diff --git a/src-server/src/api/routes/task/context_file.py b/src-server/src/api/routes/task/context_file.py
index c19ad4b5..b93dfcb3 100644
--- a/src-server/src/api/routes/task/context_file.py
+++ b/src-server/src/api/routes/task/context_file.py
@@ -56,7 +56,7 @@ def _list_directory(workspace_root: Path, path: str) -> list[task_schemas.Contex
type SearchCandidate = tuple[str, str, Literal["folder", "file"]]
@lru_cache(maxsize=8)
def _scan_cached(root: Path, scan_limit: int) -> list[SearchCandidate]:
- candidates = list[SearchCandidate]()
+ candidates: list[SearchCandidate] = []
for entry in scandir_recursive_bfs(root, scan_limit):
rel_path = Path(entry.path).relative_to(root).as_posix()
candidates.append((entry.name, rel_path, "folder" if entry.is_dir() else "file"))
@@ -67,7 +67,7 @@ def _search_file(query: str, workspace_root: Path, match_limit: int) -> list[tas
SCORE_CUTOFF = 60
candidates = _scan_cached(workspace_root, MAX_SCAN_LIMIT)
- results = list[tuple[float, task_schemas.ContextFileItem]]()
+ results: list[tuple[float, task_schemas.ContextFileItem]] = []
for basename, rel_path, node_type in candidates:
name_score = fuzz.WRatio(query, basename)
path_score = fuzz.WRatio(query, rel_path)
diff --git a/src-server/uv.lock b/src-server/uv.lock
index 3a1a9676..307f3a77 100644
--- a/src-server/uv.lock
+++ b/src-server/uv.lock
@@ -298,7 +298,7 @@ wheels = [
[[package]]
name = "dais-sdk"
-version = "0.8.15"
+version = "0.8.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -308,9 +308,9 @@ dependencies = [
{ name = "starlette" },
{ name = "uvicorn" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/26/b3/2d5a5e19eb2d562dc0abf0dd66716b5fe59eff403e12e2ed97e13e280c9b/dais_sdk-0.8.15.tar.gz", hash = "sha256:3bd3ad24444c2bd86590aeaa5d0eec99a2b5ac53e4bab0743d0793c76f23286d", size = 19352, upload-time = "2026-03-15T13:02:43.862Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/c0/44208da7d77661bb206a66610da5b8a7e9eb981876abee8f24b41876f2a9/dais_sdk-0.8.16.tar.gz", hash = "sha256:b91daba337738d854cd5fcfdc7a8681ff468cd791f48fa51a06c61e9b0e12239", size = 19547, upload-time = "2026-03-16T03:23:11.601Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/78/6d/005d37f07da3847939da1597fdd97831c230f600a583f95987986729e38d/dais_sdk-0.8.15-py3-none-any.whl", hash = "sha256:d35486ea9982b5e5ff6c3bc9190bb2e77216d2f63426562849bb409c601a3a8e", size = 29193, upload-time = "2026-03-15T13:02:42.81Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/b7/eab632242fa26df0e4aa837f554024f71421b99f3311c87314b59b7f2e7b/dais_sdk-0.8.16-py3-none-any.whl", hash = "sha256:4103f6420093e9fbc971ac0d98e106351afd7038248d1e646c1452099b81e98c", size = 29492, upload-time = "2026-03-16T03:23:10.031Z" },
]
[[package]]
@@ -358,7 +358,7 @@ requires-dist = [
{ name = "aiosqlite", specifier = "==0.22.1" },
{ name = "alembic", specifier = "==1.18.4" },
{ name = "binaryornot", specifier = "==0.4.4" },
- { name = "dais-sdk", specifier = "==0.8.15" },
+ { name = "dais-sdk", specifier = "==0.8.16" },
{ name = "dais-shell", specifier = "==0.1.2" },
{ name = "fastapi", specifier = "==0.135.1" },
{ name = "fastapi-pagination", specifier = "==0.15.10" },