fix(aibox): batch H audit fixes - subagent cancel propagation, scheduler timeout, exec process-group reaping

Fourth audit round (see docs/reports/aibox_audit_20260817.md §11):
- H1: shared parent cancel event no longer cleared on subagent first think
  (N5 regression); parent-already-cancelled skips subagent start
- M1: /reload_prompts moved off event loop (run_in_executor, N3 sibling)
- M2: skill --help scan prewarmed before serving connections
- M3: scheduler task-level timeout (scheduler.task_timeout, default 600s)
- M4: get_recent_memories(None) capped instead of full-table load
- M5: memo delete now physically removes rows (was complete-only)
- M6: PromptManager.reload builds new list then atomically swaps
- L1-L12: read_file arg coercion, proxy save failure reported, SSE
  utf-8 tolerance, persona realpath check, truncation char boundary,
  compressor div0, skill path quoting, ask_user required, POSIX
  process-group kill (pgid captured at Popen), web scheme whitelist,
  registry redact substring match, Bearer redaction in failover logs
This commit is contained in:
qing committed 2026-08-18 02:33:17 +00:00
1 parent d1f780e381
commit d3acccd146
14 files changed
+643 -79

No files matched your search

+16
View File
@@ -96,6 +96,16 @@ class ReadFileTool(FileToolBase):
# 限制max_lines上限,防止请求超大范围
max_lines = min(max_lines, 500)
# L1(审计 2026-08-18):start/end_line 为 LLM 可控参数,此前未强转
# 直拼 sed 单引号模板——LLM 传含引号的字符串可闭合引号注入命令。
# 强转 int(非法输入在此拦截)并夹取合理范围。
try:
start_line = int(start_line)
end_line = int(end_line)
except (TypeError, ValueError):
return "❌ 参数错误: start_line/end_line 必须为整数"
start_line = max(1, start_line)
end_line = max(0, min(end_line, 10_000_000))
quoted = shlex.quote(abs_path)
# 构造等价的命令行读取命令
@@ -320,6 +330,12 @@ class SimpleWebCrawlerTool(Tool):
if action not in ("get", "text", "title", "links", "search"):
return f"❌ 未知操作: {action}"
# L10(审计 2026-08-18):url 为 LLM 可控输入(可被网页内容诱导),
# 此前不限 scheme——file:// 可经生成的 curl 指引读取本地文件(SSRF)。
# 白名单 http/https 后其余协议直接拒绝。
if not url or not str(url).strip().lower().startswith(("http://", "https://")):
return "❌ 仅支持 http/https 协议地址(已拦截 file:// 等非网络协议)"
quoted_url = shlex.quote(url)
timeout = constants.WEB_TIMEOUT
user_agent = shlex.quote(constants.WEB_USER_AGENT)
+3 -1
View File
@@ -752,7 +752,9 @@ class MemoTool(Tool):
memo_id = kwargs.get("memo_id")
if memo_id:
if self.memo_db.complete_memo(memo_id, force_complete=True):
# M5:真实物理删除(此前调 complete_memo 只标记完成,行不删除,
# 重复任务被推进而非移除,stats 计数失真)
if self.memo_db.delete_memo(memo_id):
return self.format_result(True, f"任务 #{memo_id} 已删除")
else:
return f"❌ 无法删除任务 #{memo_id}"
+5 -2
View File
@@ -39,10 +39,13 @@ class AskUserTool(Tool):
)
}
},
"required": []
# L8:此前 required 为空,LLM 不传 question 时 str + None 抛 TypeError
"required": ["question"]
}
def execute(self, question: str, **kwargs) -> str:
def execute(self, question: str = None, **kwargs) -> str:
if not question:
return "❌ 需要提供问题内容(question 参数)"
return (
"你遇到了想搞清楚的事情: " + question + "\n你现在可以询问用户。"
)
+5 -2
View File
@@ -146,7 +146,7 @@ class SetProxyTool(Tool):
check_detail = detail
config = ConfigManager()
config.set_proxy(host, port, username, password, password_env, enabled, save)
saved = config.set_proxy(host, port, username, password, password_env, enabled, save)
proxy_info = config.get_proxy_info()
if proxy_info.get("password_env"):
pw_source = "环境变量 " + proxy_info["password_env"]
@@ -159,10 +159,13 @@ class SetProxyTool(Tool):
"服务器": f"{proxy_info['host']}:{proxy_info['port']}",
"认证": "有" if proxy_info.get("username") else "无",
"密码来源": pw_source,
"持久化": "已保存" if save else "仅内存"
"持久化": "已保存" if saved else "保存失败(仅本次会话有效,重启后丢失)"
}
if check_detail:
data["连通性"] = check_detail
# L2:落盘失败(磁盘只读等)如实报告,不再静默报"已保存"
if save and not saved:
return self.format_result(False, "代理已配置但保存失败(仅本次会话有效,重启后丢失)", data)
return self.format_result(True, "代理设置成功", data)
+7 -2
View File
@@ -16,13 +16,18 @@ from common import Logger, constants, i18n
from .base import Tool
# 工具参数名命中这些关键词时,debug 日志里打码(防密码/密钥进日志文件)
_SENSITIVE_PARAM_KEYS = {"password", "api_key", "token", "secret", "authorization"}
# L11(审计 2026-08-18):精确匹配漏掉 apiKey/access_token/passwd 等变体,
# 改包含匹配——日志场景下过度打码可接受,漏打码才是问题。
_SENSITIVE_PARAM_KEYS = (
"password", "passwd", "api_key", "apikey", "token", "secret",
"authorization", "credential",
)
def _sanitize_kwargs(kwargs: Dict) -> Dict:
"""复制一份参数并把敏感键的值替换为 ***(仅用于日志输出)"""
return {
k: ("***" if str(k).lower() in _SENSITIVE_PARAM_KEYS else v)
k: ("***" if any(s in str(k).lower() for s in _SENSITIVE_PARAM_KEYS) else v)
for k, v in kwargs.items()
}
+8 -3
View File
@@ -16,6 +16,7 @@ tools.skill_tools - 技能相关工具
import os
import queue
import re
import shlex
from typing import Dict, Optional
from common import constants, i18n, Logger
@@ -253,10 +254,14 @@ class InstallSkillTool(Tool):
skills_dir = constants.SKILLS_DIR
# 如果给了具体路径,在说明里带入,否则用占位符
src = skill_path.strip() if skill_path else "/path/to/your_skill.md"
raw_src = skill_path.strip() if skill_path else ""
# 去掉扩展名作为技能名称,再统一补 .md 作为目标文件名
base = os.path.splitext(os.path.basename(src))[0] if src else "your_skill"
base = os.path.splitext(os.path.basename(raw_src))[0] if raw_src else "your_skill"
dst = os.path.join(skills_dir, base + ".md")
# L7(审计 2026-08-18):src 为 LLM 可控输入,含空格/元字符时
# 未经引用的命令会被 shell 拆词/注入(指引由子代理经 run_command 执行)
src = shlex.quote(raw_src) if raw_src else "/path/to/your_skill.md"
dst_q = shlex.quote(dst)
guide = f"""📦 扩展技能安装指引
{"=" * 55}
@@ -282,7 +287,7 @@ class InstallSkillTool(Tool):
调用 create_subagent 派生子代理执行复制命令(目标文件名统一补 .md 扩展名):
cp {src} {dst}
cp {src} {dst_q}
⚠️ 注意:目标文件名必须带 .md 扩展名,
系统以 .md 文件作为技能加载,且技能名称取文件名(不含 .md)。