新增: http_request AI工具(结构化HTTP·SSRF防御·High审批)

- 后端: Cargo.toml reqwest0.12(json/gzip/brotli native-tls对齐df-ai) + http.rs(handler+SSRF三层防御 validate_url/resolve_and_check/重定向≤3跳每跳校验) + tool_registry register_http_tools + 基线28→29
- 前端: useToolCard formatHttpResult + ToolCard渲染(method/url/status/body折叠/截断) + HIGH_RISK_TOOLS + confirmHighHttp + i18n
- SSRF: localhost/私有IP(127/10/172.16/192.168/169.254云元数据/100.64CGNAT/≥224/IPv6保留)拒绝 + DNS resolve校验防重绑定 + 重定向每跳重校验
- 审批: High(统一保守 GET也审批 防漏写操作)
- 截断: 响应50KB(T-05)+ 请求1MB + 超时1-60s默认30
This commit is contained in:
2026-06-19 12:48:28 +08:00
parent b42cc66e7a
commit f1a06732fd
8 changed files with 904 additions and 5 deletions

View File

@@ -368,9 +368,43 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
let mut registry = AiToolRegistry::new();
register_data_tools(&mut registry, db);
register_file_tools(&mut registry);
register_http_tools(&mut registry);
registry
}
/// 网络层 AI 工具注册(1 个:http_request)— 结构化 HTTP(GET/POST/PUT/PATCH/DELETE)。
/// 不持 db,纯 reqwest 调用。SSRF 防护 + 重定向每跳校验见 commands/ai/http.rs。
/// 风险:GET=Medium(只读但触发外发)/ 写方法=High(须人工批准,对齐 audit.rs risk 分流)。
///
/// 注意:一个工具名两种 risk 不支持(register 单一 risk_level),故按最高风险 High 注册。
// (写方法 High 兜底;GET 也走 High 审批更保守 — 宁可多审批不漏副作用。)
// 设计权衡见 http.rs 模块注释。若未来需 GET 走 Medium 自动,需拆 get_request/post_request 两工具。
fn register_http_tools(registry: &mut AiToolRegistry) {
registry.register(
"http_request", "发起结构化 HTTP 请求(GET/POST/PUT/PATCH/DELETE),用于查询外部 API。参数:method(默认 GET)、url(http/https)、headers(对象 map)、body(请求体字符串)、timeout_secs(默认 30,上限 60)、parse(json/text/auto,默认 auto)。安全:仅 http/https 协议,拒绝私网/保留 IP(SSRF 防护含 DNS resolve 后校验防重绑定),重定向≤3 跳且每跳重校验。响应 body 截断 50KB。GET 为只读但触发外发网络,POST/PUT/PATCH/DELETE 有副作用,统一按高风险须人工批准",
// schema:headers 是对象 map,object_schema 仅支持扁平标量三元组,故手工拼
{
let mut props = serde_json::Map::new();
props.insert("method".into(), serde_json::json!({ "type": "string", "description": "HTTP 方法:GET/POST/PUT/PATCH/DELETE(默认 GET)", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] }));
props.insert("url".into(), serde_json::json!({ "type": "string", "description": "请求 URL,仅 http/https,拒私网/localhost(SSRF 防护)" }));
props.insert("headers".into(), serde_json::json!({ "type": "object", "description": "请求头 map<string,string>,如 {\"Authorization\":\"Bearer xxx\",\"Content-Type\":\"application/json\"}", "additionalProperties": { "type": "string" } }));
props.insert("body".into(), serde_json::json!({ "type": "string", "description": "请求体(POST/PUT/PATCH 用),原样发送,Content-Type 须在 headers 显式指定" }));
props.insert("timeout_secs".into(), serde_json::json!({ "type": "integer", "description": "超时秒数(默认 30,上限 60)", "minimum": 1, "maximum": 60 }));
props.insert("parse".into(), serde_json::json!({ "type": "string", "description": "响应 body 解析:json(pretty 格式化)/text(原样)/auto(按 Content-Type 自动,默认)", "enum": ["json", "text", "auto"] }));
serde_json::json!({
"type": "object",
"properties": props,
"required": ["url"],
})
},
RiskLevel::High,
Box::new(|args: serde_json::Value| Box::pin(async move {
// 转调 http.rs handler(SSRF 防护 + 重定向 + 截断全在那)
super::http::execute_http_request(args).await
})),
);
}
/// 数据层 AI 工具注册(18 个持 db 的 CRUD/状态机/工作流工具)——从 build_ai_tool_registry 抽出。
///
/// 工具闭包捕获 `db: &Arc<Database>` Arc 重建 Repo(列表/创建/更新/删除/状态推进/工作流)。
@@ -1584,6 +1618,8 @@ pub(crate) fn display_hint_for_tool(name: &str) -> Option<(&'static str, &'stati
// F-03 收口:推进链工具审批文案(advance_task/run_workflow)
"advance_task" => ("推进任务状态:{} → {}", &["id", "target_status"]),
"run_workflow" => ("触发工作流:任务{} 推进到 {}", &["task_id", "target_status"]),
// http_request 审批文案:method + url(看是什么请求打到哪)
"http_request" => ("HTTP 请求:{} {}", &["method", "url"]),
_ => return None,
};
Some((template, keys))
@@ -1618,6 +1654,7 @@ pub(crate) fn tool_display_hint(name: &str) -> Option<&'static str> {
"append_file" => Some("追加写入"),
"delete_file" => Some("删除文件"),
"rename_file" => Some("重命名/移动"),
"http_request" => Some("发起 HTTP 请求"),
_ => None,
}
}
@@ -1691,7 +1728,7 @@ mod tests {
// 任一层漏移 register 调用,此测试立即红。工具名集合也断言,防 rename 致 LLM tool 突变。
// ============================================================
/// build_ai_tool_registry 应注册恰好 28 个工具(18 data + 10 file),且工具名集合稳定。
/// build_ai_tool_registry 应注册恰好 29 个工具(18 data + 10 file + 1 http),且工具名集合稳定。
///
/// 用 in-memory SQLite(Database::open_in_memory 自跑迁移),构造零外部依赖的 db,
// 不实际执行任何 handler——仅断言注册阶段的定义完整性,故无需真实数据。
@@ -1701,16 +1738,17 @@ mod tests {
let db = Arc::new(db);
let registry = build_ai_tool_registry(&db);
// 总量基线:28(18 data + 10 file)。拆分前后必须一致。
// 总量基线:29(18 data + 10 file + 1 http)。拆分前后必须一致。
assert_eq!(
registry.len(),
28,
"工具总数应为 28(18 data + 10 file),实际 {}", registry.len()
29,
"工具总数应为 29(18 data + 10 file + 1 http),实际 {}", registry.len()
);
// 工具名集合基线:防 rename / 漏注册 / 误删除。
// data 层 18 个(持 db):CRUD/状态机/工作流
// file 层 10 个(不持 db):命令/读/列/写/改/元/追加/删/移/搜
// http 层 1 个(不持 db):http_request
let mut expected: Vec<&str> = vec![
// ── data 层 (18) ──
"list_projects", "list_tasks", "list_ideas",
@@ -1723,6 +1761,8 @@ mod tests {
"run_command", "read_file", "list_directory",
"write_file", "patch_file", "file_info",
"append_file", "delete_file", "rename_file", "search_files",
// ── http 层 (1) ──
"http_request",
];
expected.sort_unstable();