feat(agents): Phase 7-10 hook system, plugins, skills, orchestration
Phase 7: Built-in Hooks (audit_log, dangerous_confirmation, security_scan) Phase 8: Plugin system (PluginManager, PluginSandbox, PluginManifest) Phase 9: Skills registry (SkillRegistry, local/plugin/MCP loaders) Phase 10: TeamLeader, RemoteTransport, BackgroundTaskManager
This commit is contained in:
11
backend/app/agents/tools/hooks/builtins/__init__.py
Normal file
11
backend/app/agents/tools/hooks/builtins/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""内置 Hook 集合 - Phase 7"""
|
||||
|
||||
from app.agents.tools.hooks.builtins.audit_log import AuditLogHook
|
||||
from app.agents.tools.hooks.builtins.dangerous_confirmation import DangerousConfirmationHook
|
||||
from app.agents.tools.hooks.builtins.security_scan import SecurityScanHook
|
||||
|
||||
__all__ = [
|
||||
"AuditLogHook",
|
||||
"DangerousConfirmationHook",
|
||||
"SecurityScanHook",
|
||||
]
|
||||
115
backend/app/agents/tools/hooks/builtins/audit_log.py
Normal file
115
backend/app/agents/tools/hooks/builtins/audit_log.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""审计日志 Hook - Phase 7.2
|
||||
|
||||
记录所有工具调用到审计日志。
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.agents.tools.hooks.types import (
|
||||
ExecutionContext,
|
||||
HookResult,
|
||||
HookType,
|
||||
)
|
||||
from app.agents.tools.manifest import ToolCategory
|
||||
|
||||
|
||||
class AuditLogHook:
|
||||
"""审计日志 Hook
|
||||
|
||||
记录所有工具调用的详细信息,包括:
|
||||
- 调用时间
|
||||
- 工具名称
|
||||
- 输入参数
|
||||
- 执行结果
|
||||
- 执行时长
|
||||
- 用户 ID
|
||||
"""
|
||||
|
||||
def __init__(self, log_path: str | None = None):
|
||||
"""
|
||||
Args:
|
||||
log_path: 日志文件路径,None 则输出到 stdout
|
||||
"""
|
||||
self.log_path = log_path
|
||||
self._logs: list[dict[str, Any]] = []
|
||||
|
||||
async def pre_tool_use(self, context: ExecutionContext) -> HookResult:
|
||||
"""工具执行前记录"""
|
||||
log_entry = {
|
||||
"event": "pre_tool",
|
||||
"tool_name": context.tool_name,
|
||||
"input": context.tool_input,
|
||||
"user_id": context.user_id,
|
||||
"session_id": context.session_id,
|
||||
}
|
||||
self._logs.append(log_entry)
|
||||
self._write_log(log_entry)
|
||||
return HookResult(
|
||||
hook_name="audit_log",
|
||||
success=True,
|
||||
continue_execution=True,
|
||||
)
|
||||
|
||||
async def post_tool_use(self, context: ExecutionContext, result: Any) -> HookResult:
|
||||
"""工具执行后记录"""
|
||||
log_entry = {
|
||||
"event": "post_tool",
|
||||
"tool_name": context.tool_name,
|
||||
"result": str(result)[:500] if result else None,
|
||||
"duration_ms": (
|
||||
(context.end_time - context.start_time) * 1000
|
||||
if context.start_time and context.end_time
|
||||
else None
|
||||
),
|
||||
}
|
||||
self._logs.append(log_entry)
|
||||
self._write_log(log_entry)
|
||||
return HookResult(
|
||||
hook_name="audit_log",
|
||||
success=True,
|
||||
continue_execution=True,
|
||||
modified_output=result,
|
||||
)
|
||||
|
||||
async def tool_error(self, context: ExecutionContext, error: Exception) -> HookResult:
|
||||
"""工具出错时记录"""
|
||||
log_entry = {
|
||||
"event": "tool_error",
|
||||
"tool_name": context.tool_name,
|
||||
"error": str(error),
|
||||
"error_type": type(error).__name__,
|
||||
}
|
||||
self._logs.append(log_entry)
|
||||
self._write_log(log_entry)
|
||||
return HookResult(
|
||||
hook_name="audit_log",
|
||||
success=False,
|
||||
continue_execution=True,
|
||||
error=str(error),
|
||||
)
|
||||
|
||||
def _write_log(self, entry: dict[str, Any]) -> None:
|
||||
"""写入日志"""
|
||||
import json
|
||||
import datetime
|
||||
|
||||
entry["timestamp"] = datetime.datetime.now().isoformat()
|
||||
|
||||
if self.log_path:
|
||||
try:
|
||||
with open(self.log_path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
except Exception:
|
||||
# 日志写入失败不影响主流程
|
||||
pass
|
||||
else:
|
||||
# 输出到 stdout
|
||||
print(f"[AUDIT] {json.dumps(entry, ensure_ascii=False)}")
|
||||
|
||||
def get_logs(self) -> list[dict[str, Any]]:
|
||||
"""获取所有日志"""
|
||||
return self._logs.copy()
|
||||
|
||||
def clear_logs(self) -> None:
|
||||
"""清空日志"""
|
||||
self._logs.clear()
|
||||
@@ -0,0 +1,142 @@
|
||||
"""危险操作确认 Hook - Phase 7.2
|
||||
|
||||
对危险操作要求用户确认。
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.agents.tools.hooks.types import (
|
||||
ExecutionContext,
|
||||
HookResult,
|
||||
)
|
||||
from app.agents.tools.manifest import SideEffectScope
|
||||
|
||||
|
||||
# 危险操作关键词
|
||||
DANGEROUS_PATTERNS = [
|
||||
# 文件操作
|
||||
"delete",
|
||||
"remove",
|
||||
"rm ",
|
||||
"rmdir",
|
||||
"unlink",
|
||||
"format",
|
||||
"truncate",
|
||||
# 系统操作
|
||||
"shutdown",
|
||||
"reboot",
|
||||
"kill",
|
||||
"pkill",
|
||||
"sudo",
|
||||
"chmod",
|
||||
"chown",
|
||||
# 数据操作
|
||||
"drop",
|
||||
"truncate",
|
||||
"delete from",
|
||||
"delete.*where",
|
||||
"insert into.*select",
|
||||
"update.*set",
|
||||
# 网络操作
|
||||
"curl",
|
||||
"wget",
|
||||
"nc ",
|
||||
"netcat",
|
||||
"ssh ",
|
||||
"scp ",
|
||||
"sftp ",
|
||||
# 环境变量
|
||||
"export.*secret",
|
||||
"export.*key",
|
||||
"export.*token",
|
||||
]
|
||||
|
||||
|
||||
class DangerousConfirmationHook:
|
||||
"""危险操作确认 Hook
|
||||
|
||||
检查工具调用是否包含危险操作,如是则要求确认。
|
||||
"""
|
||||
|
||||
def __init__(self, auto_block: bool = False):
|
||||
"""
|
||||
Args:
|
||||
auto_block: True 表示自动拦截危险操作,False 表示仅警告
|
||||
"""
|
||||
self.auto_block = auto_block
|
||||
self._pending_confirmations: dict[str, bool] = {}
|
||||
|
||||
async def pre_tool_use(self, context: ExecutionContext) -> HookResult:
|
||||
"""检查是否为危险操作"""
|
||||
is_dangerous = self._check_dangerous(context.tool_name, context.tool_input)
|
||||
|
||||
if is_dangerous:
|
||||
if self.auto_block:
|
||||
return HookResult(
|
||||
hook_name="dangerous_confirmation",
|
||||
success=False,
|
||||
continue_execution=False,
|
||||
error=f"危险操作被自动拦截: {context.tool_name}",
|
||||
metadata={"dangerous": True, "auto_blocked": True},
|
||||
)
|
||||
else:
|
||||
# 标记需要确认
|
||||
context.metadata["requires_confirmation"] = True
|
||||
context.metadata["dangerous_operation"] = True
|
||||
return HookResult(
|
||||
hook_name="dangerous_confirmation",
|
||||
success=True,
|
||||
continue_execution=True,
|
||||
metadata={"dangerous": True, "requires_confirmation": True},
|
||||
)
|
||||
|
||||
return HookResult(
|
||||
hook_name="dangerous_confirmation",
|
||||
success=True,
|
||||
continue_execution=True,
|
||||
)
|
||||
|
||||
def _check_dangerous(self, tool_name: str, tool_input: dict[str, Any]) -> bool:
|
||||
"""检查是否为危险操作"""
|
||||
# 检查工具名称
|
||||
dangerous_tools = [
|
||||
"delete",
|
||||
"remove",
|
||||
"drop",
|
||||
"truncate",
|
||||
"kill",
|
||||
"shutdown",
|
||||
"reboot",
|
||||
"bash",
|
||||
"powershell",
|
||||
"shell",
|
||||
]
|
||||
|
||||
if tool_name.lower() in dangerous_tools:
|
||||
return True
|
||||
|
||||
# 检查输入参数
|
||||
input_str = str(tool_input).lower()
|
||||
|
||||
for pattern in DANGEROUS_PATTERNS:
|
||||
if pattern.lower() in input_str:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def confirm(self, session_id: str, confirmed: bool) -> None:
|
||||
"""确认危险操作
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
confirmed: True 表示用户确认,False 表示取消
|
||||
"""
|
||||
self._pending_confirmations[session_id] = confirmed
|
||||
|
||||
def is_confirmed(self, session_id: str) -> bool:
|
||||
"""检查是否已确认"""
|
||||
return self._pending_confirmations.get(session_id, False)
|
||||
|
||||
def clear_confirmation(self, session_id: str) -> None:
|
||||
"""清除确认状态"""
|
||||
self._pending_confirmations.pop(session_id, None)
|
||||
183
backend/app/agents/tools/hooks/builtins/security_scan.py
Normal file
183
backend/app/agents/tools/hooks/builtins/security_scan.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""安全扫描 Hook - Phase 7.2
|
||||
|
||||
扫描工具调用和结果中的敏感信息。
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from app.agents.tools.hooks.types import (
|
||||
ExecutionContext,
|
||||
HookResult,
|
||||
)
|
||||
|
||||
|
||||
# 敏感信息模式
|
||||
SENSITIVE_PATTERNS = {
|
||||
"api_key": [
|
||||
r"api[_-]?key['\"]?\s*[:=]\s*['\"]?[a-zA-Z0-9_\-]{20,}",
|
||||
r"apikey['\"]?\s*[:=]\s*['\"]?[a-zA-Z0-9_\-]{20,}",
|
||||
],
|
||||
"password": [
|
||||
r"password['\"]?\s*[:=]\s*['\"]?[^\s'\"]{8,}",
|
||||
r"passwd['\"]?\s*[:=]\s*['\"]?[^\s'\"]{8,}",
|
||||
r"secret['\"]?\s*[:=]\s*['\"]?[a-zA-Z0-9_\-]{20,}",
|
||||
],
|
||||
"token": [
|
||||
r"token['\"]?\s*[:=]\s*['\"]?[a-zA-Z0-9_\-\.]{20,}",
|
||||
r"bearer\s+[a-zA-Z0-9_\-\.]+",
|
||||
r"ghp_[a-zA-Z0-9]{36}",
|
||||
r"sk-[a-zA-Z0-9]{48}",
|
||||
],
|
||||
"private_key": [
|
||||
r"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----",
|
||||
r"-----END (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----",
|
||||
],
|
||||
"ip_address": [
|
||||
r"\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b",
|
||||
],
|
||||
"email": [
|
||||
r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class SecurityScanHook:
|
||||
"""安全扫描 Hook
|
||||
|
||||
扫描工具输入和输出中的敏感信息,进行脱敏处理。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redact: bool = True,
|
||||
block_on_detect: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
redact: 是否对敏感信息进行脱敏
|
||||
block_on_detect: 检测到敏感信息时是否阻止执行
|
||||
"""
|
||||
self.redact = redact
|
||||
self.block_on_detect = block_on_detect
|
||||
self._compiled_patterns = {
|
||||
name: [re.compile(p, re.IGNORECASE) for p in patterns]
|
||||
for name, patterns in SENSITIVE_PATTERNS.items()
|
||||
}
|
||||
|
||||
async def pre_tool_use(self, context: ExecutionContext) -> HookResult:
|
||||
"""扫描输入参数"""
|
||||
detected = self._scan_dict(context.tool_input)
|
||||
|
||||
if detected:
|
||||
context.metadata["security_detected"] = detected
|
||||
|
||||
if self.block_on_detect:
|
||||
return HookResult(
|
||||
hook_name="security_scan",
|
||||
success=False,
|
||||
continue_execution=False,
|
||||
error=f"检测到敏感信息: {', '.join(detected.keys())}",
|
||||
metadata={"detected": detected, "blocked": True},
|
||||
)
|
||||
|
||||
if self.redact:
|
||||
redacted_input = self._redact_dict(context.tool_input.copy())
|
||||
return HookResult(
|
||||
hook_name="security_scan",
|
||||
success=True,
|
||||
continue_execution=True,
|
||||
modified_input=redacted_input,
|
||||
metadata={"detected": detected, "redacted": True},
|
||||
)
|
||||
|
||||
return HookResult(
|
||||
hook_name="security_scan",
|
||||
success=True,
|
||||
continue_execution=True,
|
||||
)
|
||||
|
||||
async def post_tool_use(self, context: ExecutionContext, result: Any) -> HookResult:
|
||||
"""扫描输出结果"""
|
||||
if isinstance(result, dict):
|
||||
detected = self._scan_dict(result)
|
||||
|
||||
if detected:
|
||||
context.metadata["security_detected_output"] = detected
|
||||
|
||||
if self.redact:
|
||||
redacted_result = self._redact_dict(result.copy())
|
||||
return HookResult(
|
||||
hook_name="security_scan",
|
||||
success=True,
|
||||
continue_execution=True,
|
||||
modified_output=redacted_result,
|
||||
metadata={"detected": detected, "redacted": True},
|
||||
)
|
||||
|
||||
elif isinstance(result, str):
|
||||
detected = self._scan_string(result)
|
||||
if detected:
|
||||
context.metadata["security_detected_output"] = detected
|
||||
|
||||
if self.redact:
|
||||
redacted_result = self._redact_string(result)
|
||||
return HookResult(
|
||||
hook_name="security_scan",
|
||||
success=True,
|
||||
continue_execution=True,
|
||||
modified_output=redacted_result,
|
||||
metadata={"detected": detected, "redacted": True},
|
||||
)
|
||||
|
||||
return HookResult(
|
||||
hook_name="security_scan",
|
||||
success=True,
|
||||
continue_execution=True,
|
||||
modified_output=result,
|
||||
)
|
||||
|
||||
def _scan_dict(self, data: dict[str, Any]) -> dict[str, list[str]]:
|
||||
"""扫描字典中的敏感信息"""
|
||||
result: dict[str, list[str]] = {}
|
||||
|
||||
for key, value in data.items():
|
||||
if isinstance(value, str):
|
||||
found = self._scan_string(value)
|
||||
if found:
|
||||
result[key] = found
|
||||
|
||||
return result
|
||||
|
||||
def _scan_string(self, text: str) -> list[str]:
|
||||
"""扫描字符串中的敏感信息"""
|
||||
found_types = []
|
||||
|
||||
for name, patterns in self._compiled_patterns.items():
|
||||
for pattern in patterns:
|
||||
if pattern.search(text):
|
||||
if name not in found_types:
|
||||
found_types.append(name)
|
||||
break
|
||||
|
||||
return found_types
|
||||
|
||||
def _redact_dict(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""脱敏字典中的敏感信息"""
|
||||
for key, value in data.items():
|
||||
if isinstance(value, str):
|
||||
data[key] = self._redact_string(value)
|
||||
elif isinstance(value, dict):
|
||||
data[key] = self._redact_dict(value)
|
||||
elif isinstance(value, list):
|
||||
data[key] = [self._redact_string(v) if isinstance(v, str) else v for v in value]
|
||||
|
||||
return data
|
||||
|
||||
def _redact_string(self, text: str) -> str:
|
||||
"""脱敏字符串中的敏感信息"""
|
||||
for name, patterns in self._compiled_patterns.items():
|
||||
for pattern in patterns:
|
||||
text = pattern.sub(f"[REDACTED:{name}]", text)
|
||||
|
||||
return text
|
||||
105
backend/app/agents/tools/hooks/config.py
Normal file
105
backend/app/agents/tools/hooks/config.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""Hook 配置持久化 - Phase 7.3"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any
|
||||
|
||||
from app.agents.tools.hooks.manager import get_hook_manager
|
||||
|
||||
|
||||
@dataclass
|
||||
class HookConfigEntry:
|
||||
"""Hook 配置条目"""
|
||||
|
||||
name: str
|
||||
hook_type: str
|
||||
enabled: bool
|
||||
tool_names: list[str] | None = None
|
||||
categories: list[str] | None = None
|
||||
priority: int = 0
|
||||
|
||||
|
||||
class HookConfigPersistence:
|
||||
"""Hook 配置持久化"""
|
||||
|
||||
def __init__(self, config_path: str | None = None):
|
||||
"""
|
||||
Args:
|
||||
config_path: 配置文件路径,None 则使用默认路径
|
||||
"""
|
||||
if config_path is None:
|
||||
config_path = os.path.join(
|
||||
os.path.dirname(__file__), "..", "..", "..", "..", "config", "hooks.json"
|
||||
)
|
||||
self.config_path = config_path
|
||||
|
||||
def load_config(self) -> list[HookConfigEntry]:
|
||||
"""从文件加载 Hook 配置"""
|
||||
if not os.path.exists(self.config_path):
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return [HookConfigEntry(**entry) for entry in data]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def save_config(self, entries: list[HookConfigEntry]) -> bool:
|
||||
"""保存 Hook 配置到文件"""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
|
||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||
json.dump([asdict(e) for e in entries], f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def apply_config(self) -> int:
|
||||
"""应用配置到 HookManager
|
||||
|
||||
Returns:
|
||||
应用的 Hook 数量
|
||||
"""
|
||||
from app.agents.tools.hooks.types import HookType
|
||||
|
||||
manager = get_hook_manager()
|
||||
entries = self.load_config()
|
||||
count = 0
|
||||
|
||||
for entry in entries:
|
||||
if entry.enabled:
|
||||
from app.agents.tools.hooks.types import HookDefinition, HookTrigger
|
||||
|
||||
trigger = HookTrigger(
|
||||
tool_names=entry.tool_names,
|
||||
categories=entry.categories,
|
||||
)
|
||||
|
||||
# 创建空的 handler,只是注册配置
|
||||
hook_def = HookDefinition(
|
||||
name=entry.name,
|
||||
hook_type=HookType(entry.hook_type),
|
||||
trigger=trigger,
|
||||
handler=lambda ctx, *args: ctx,
|
||||
priority=entry.priority,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
manager.register(hook_def)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
# 全局单例
|
||||
_persistence: HookConfigPersistence | None = None
|
||||
|
||||
|
||||
def get_hook_config_persistence() -> HookConfigPersistence:
|
||||
"""获取全局 Hook 配置持久化实例"""
|
||||
global _persistence
|
||||
if _persistence is None:
|
||||
_persistence = HookConfigPersistence()
|
||||
return _persistence
|
||||
Reference in New Issue
Block a user