feat(api): Tool Invoke API + Naga Scoped Token 鉴权系统#49
Conversation
Add GET /api/v1/tools and POST /api/v1/tools/invoke endpoints to allow external systems (e.g. Naga) to discover and invoke Undefined's tools. Supports synchronous response and optional async webhook callback. - APIConfig: add 6 tool_invoke_* fields (enabled, expose, allow/denylist, timeouts) - Three-layer tool filtering: denylist > allowlist > expose scope - RequestContext + resource injection for proper tool execution - WebUI proxy routes for /api/runtime/tools and /api/runtime/tools/invoke - Update docs/openapi.md with full API reference, examples, and troubleshooting - Add 27 tests covering config parsing, filtering logic, invocation, and callbacks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- 保存 fire-and-forget task 引用到 _background_tasks 集合,防止被 GC - 合并 get_agents_schema() 调用,避免非 all 模式下重复调用 - WebUI 代理超时改为动态读取 tool_invoke_timeout + 60s 缓冲 - 回调移除多余 Content-Type header,由 aiohttp json= 自动处理 - 文档修正 callback URL 要求为支持 HTTP 和 HTTPS - RequestContext 类型注释补充 "api" 类型 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
实现 NagaAgent 双层鉴权(共享 api_key + scoped token)回调机制: - NagaStore 数据层:绑定/审批/吊销/校验,异步 JSON 持久化(文件锁 + 原子写入) - /naga 斜杠命令:bind/approve/reject/revoke/list/pending/info 七个子命令 - Callback API:POST /api/v1/naga/callback + GET /api/v1/naga/targets - scopes.json 子命令权限控制:支持 group_only/private_only/admin_only/superadmin_only 别名 - /help 权限可见性过滤:按用户权限级别隐藏不可用命令 - 总开关统一为 features.nagaagent_mode_enabled - 文档:configuration.md、slash-commands.md、openapi.md 同步更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SSRF 防护:_validate_callback_url 拒绝私有/回环 IP 字面量 - 临时文件泄漏:naga callback 渲染图片后 finally 清理 mkstemp 文件 - 后台任务泄漏:RuntimeAPIServer.stop() 先 cancel 再 cleanup - allowed_groups 查找优化:list[int] → frozenset[int],O(1) 查找 - 文档修正:明确群聊场景所有 /naga 子命令受 allowed_groups 白名单限制 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CQ image URI:改用 Path.as_uri() 构建跨平台 file URI(Win/Mac/Linux) - _load_scopes() 异步化:拆分 sync + asyncio.to_thread,遵循项目 IO 规范 - openapi.md:修正已删除的 [naga].enabled 引用为 features.nagaagent_mode_enabled Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 私聊代理绕过:approve/reject 通知申请人时使用 _notify_user 绕过 _PrivateCommandSenderProxy,确保消息发给目标用户而非命令调用者 - 热更新警告:_RESTART_REQUIRED_KEYS 加入 "naga",变更时提示需重启 - auth 中间件:/api/v1/naga/ 路径的认证跳过改为仅在 nagaagent_mode_enabled 时生效,关闭时所有 /api/ 路径统一走主认证 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Devin Review 未修项说明以下是 review 中标记但不需要修改的项目及原因: Bug:
|
开关分层: - features.nagaagent_mode_enabled — 总开关,控制 AI 侧行为(提示词、工具暴露) - naga.enabled — 子开关,控制外部网关集成(回调 API、/naga 命令、绑定管理) - nagaagent_mode_enabled=false 时强制关闭所有 Naga 功能 WebUI 多行注释修复: - comment.py: 多行同语言注释用换行符连接而非空格压缩 - toml_render.py: 渲染时按换行符拆分为多行 # zh:/en: 输出 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
markdown 格式回调中 render_markdown_to_html 未包裹 try/except, 异常时直接 500 而非回退文本发送。现与 render_html_to_image 一致, 失败时回退到纯文本。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| hostname = parsed.hostname or "" | ||
| if not hostname: | ||
| return "callback.url must include a hostname" | ||
|
|
||
| # 仅检查 IP 字面量(如 http://127.0.0.1/、http://[::1]/、http://10.0.0.1/) | ||
| try: | ||
| addr = ipaddress.ip_address(hostname) | ||
| except ValueError: | ||
| pass # 域名形式,放行 |
There was a problem hiding this comment.
🟡 SSRF protection bypass via localhost hostname in callback URL validation
The _validate_callback_url function blocks private/loopback IP literals (127.0.0.1, ::1, 10.x.x.x, etc.) but allows the localhost hostname, which is an RFC 6761 reserved name that always resolves to 127.0.0.1. Unlike arbitrary domain names (e.g. internal.company.com) that require DNS resolution to determine their target, localhost is a well-known loopback alias that can be blocked with a simple string check — no DNS needed. An authenticated caller can use http://localhost:<port>/path as the callback URL to reach any service on the bot's host, enabling SSRF-based internal service discovery and potential data exfiltration via the tool result payload. The test at tests/test_runtime_api_tool_invoke.py:136 explicitly asserts localhost is allowed, confirming the gap.
| hostname = parsed.hostname or "" | |
| if not hostname: | |
| return "callback.url must include a hostname" | |
| # 仅检查 IP 字面量(如 http://127.0.0.1/、http://[::1]/、http://10.0.0.1/) | |
| try: | |
| addr = ipaddress.ip_address(hostname) | |
| except ValueError: | |
| pass # 域名形式,放行 | |
| hostname = parsed.hostname or "" | |
| if not hostname: | |
| return "callback.url must include a hostname" | |
| # Block well-known loopback hostnames (no DNS needed to know these are local) | |
| _LOOPBACK_HOSTNAMES = {"localhost", "localhost.localdomain", "ip6-localhost", "ip6-loopback"} | |
| if hostname.lower() in _LOOPBACK_HOSTNAMES: | |
| return "callback.url must not point to a private/loopback address" | |
| # 仅检查 IP 字面量(如 http://127.0.0.1/、http://[::1]/、http://10.0.0.1/) | |
| try: | |
| addr = ipaddress.ip_address(hostname) | |
| except ValueError: | |
| pass # 域名形式,放行 |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
GET /api/v1/tools和POST /api/v1/tools/invoke端点,允许外部系统(如 NagaAgent)发现并调用 Undefined 的工具能力tool_invoke_enabled = false),需显式开启/naga斜杠命令Changes
Tool Invoke API
config/models.py,config/loader.py):APIConfig新增 6 个tool_invoke_*字段,含合法值校验api/app.py): 工具列表/调用 handler、三层过滤逻辑(denylist > allowlist > expose)、RequestContext 构建与资源注入、异步回调、OpenAPI spec 更新webui/routes/_runtime.py): 新增GET /api/runtime/tools和POST /api/runtime/tools/invoke代理路由config.toml.example):[api]段追加 tool_invoke 配置项docs/openapi.md): 完整 API 参考(配置、接口、字段表、过滤逻辑、错误码、cURL 示例、故障排查)Naga Scoped Token 鉴权系统
api/naga_store.py): 绑定数据层 — token 生成/校验/吊销,异步 JSON 持久化(文件锁 + 原子写入,跨平台兼容)api/app.py):POST /api/v1/naga/callback消息回调 +GET /api/v1/naga/targets目标查询,独立鉴权中间件skills/commands/naga/): bind/approve/reject/revoke/list/pending/info 七个子命令,群聊白名单 + 私聊放行skills/commands/naga/scopes.json): 子命令权限控制,支持group_only/private_only/admin_only/superadmin_only别名skills/commands/help/handler.py): 按用户权限级别隐藏不可用命令features.nagaagent_mode_enabled,无独立naga.enabledconfig/models.py,config/loader.py): 新增NagaConfig(api_url/api_key/allowed_groups)docs/configuration.md(4.25 节)、docs/slash-commands.md(/naga + scopes.json 开发指南)、docs/openapi.md(第 8 节)同步更新Security
Tool Invoke API
tool_invoke_enabled = true显式开启X-Undefined-API-Key认证中间件denylist>allowlist>expose范围Naga 鉴权
api_key(secrets.compare_digest)+ scopedudf_xxxtokenfeatures.nagaagent_mode_enabled关闭时端点不注册、命令不可用allowed_groups白名单限制chmod 600(非 Unix 平台跳过)Test plan
uv run ruff format . && uv run ruff check .通过uv run mypy src/通过(300 source files, 0 errors)uv run pytest tests/通过(301 tests passed)tool_invoke_enabled = false时返回 403GET /api/v1/tools返回过滤后的工具列表POST /api/v1/tools/invoke同步调用工具/naga bind在白名单群内提交绑定/naga approve审批后 token 同步到 NagaPOST /api/v1/naga/callback回调消息投递🤖 Generated with Claude Code