安全与审批
手能伸得远,但边界清清楚楚。
MateClaw 给 Agent 真实的能力——shell 访问、文件写入、浏览器自动化、委托给其他 Agent、通过 MCP 调用远程工具。这是"手能伸得远"那一半。这一页讲的是另一半:不让强有力的手干蠢事的边界。
- JWT 认证——你是谁
- Tool Guard(基于规则)——每个 Agent 被允许做什么
- 审批工作流——执行前什么时候需要人来决定
- File Guard——Agent 眼里的文件系统长什么样
- 工作空间隔离——每个团队能看到什么
- 审计日志——所有人做过的所有事,按时间顺序,永远保留
生产环境跑 MateClaw 的话,从头到尾读完这页。
Agentic, but not autonomous
每个公司的 IT 部门和 CISO 在买 AI 之前问的同一个问题:
"它会不会跑飞了,删了我不该删的东西?"
任何说"AI 不会跑飞"的人都在骗你。MateClaw 的答案不一样——敏感操作问你一句再执行。
Agent 想删文件、发邮件、跑写入型 SQL、调付费 API——任何一条 Tool Guard 规则匹配上的调用,会在回合中途暂停,审批通知推到你的 IM(飞书 / 钉钉 / Slack / 邮件),你点批准,Agent 从暂停的地方接着跑。每一个动作进 mate_tool_guard_audit_log——按时间序、永远保留、可导出 CSV。
会动手(agentic),但不擅自动手(not autonomous)。
这是「让 AI 替你干活」和「让 AI 替你做主」之间那条线。MateClaw 站在线的左边——也是你的 CISO 第一次不会一上来就否决的那一边。
JWT 认证
怎么工作的
- 用户往
/api/v1/auth/login提交凭证 - 服务器校验之后返回一个 JWT
- 之后所有请求在
Authorizationheader 里带 token - 服务器在每个请求上校验 token
登录
curl -X POST http://localhost:18088/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin123"}'响应:
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9...",
"tokenType": "Bearer",
"expiresIn": 86400
}
}修改密码
用户可以在个人设置里修改自己的密码。管理员可以在成员管理里重置任何成员的密码。
滑动窗口续签
MateClaw 实现了滑动窗口 token 续签。当 token 剩余有效期低于 renewal-threshold(默认 2 小时 / 7200000ms)时,服务器在响应头 X-New-Token 里发一个新 token。前端自动拿到新 token 替换旧的,用户感知不到。活跃用户不会被踢下线;空闲会话该过期还是过期。
配置
mateclaw:
jwt:
secret: your-secret-key-must-be-at-least-32-characters-long
expiration: 86400000 # token 有效期(毫秒,默认 24 小时)
renewal-threshold: 7200000 # 剩余有效期低于此值(毫秒)触发滑动续期WARNING
生产环境必须改默认 JWT secret。 至少 32 字符。用环境变量(JWT_SECRET=...)设置,不要 commit。
错误码
| 状态码 | 含义 | 响应 |
|---|---|---|
| 401 | Token 缺失、过期或无效 | {"code":401,"msg":"Token expired or invalid","data":null} |
| 403 | Token 有效但权限不足 | {"code":403,"msg":"Forbidden","data":null} |
前端统一处理——跳登录页、清空存储的 token。
默认凭证
MateClaw 出厂带 admin / admin123。除了你自己笔记本之外的任何部署都必须立刻改。
Spring Security 配置
- 无状态会话——服务端不存 session;所有状态都在 JWT 里
- 公共 API 端点——
GET /api/v1/settings/language、/api/v1/auth/login、/api/v1/chat/stream、/api/v1/chat/*/stop、/api/v1/agents/*/chat/stream、/api/v1/setup/**、/api/v1/channels/webhook/**、/api/v1/channels/webchat/**、/api/v1/talk/ws、/api/v1/files/generated/** - 受保护端点——
/api/**下的其他所有路径 - CSRF 关闭——无状态 JWT 不需要
Tool Guard —— 基于规则的权限引擎
Tool Guard 是 MateClaw 决定一次工具调用被允许做什么的机制。它不是一个扁平的"危险工具清单"。 它是一个规则引擎。每条规则说:对这个工具,可选匹配这些参数,在这个工作空间里,做 X——X 是 allow、deny、或 require_approval。
三张表
| 表 | 用途 |
|---|---|
mate_tool_guard_config | 全局配置——开关、默认策略、审批超时、通知渠道 |
mate_tool_guard_rule | 单条规则——工具模式、可选参数正则、工作空间范围、动作、优先级 |
mate_tool_guard_audit_log | 每一次受守护的调用一条记录——工具、参数、匹配的规则、决定、用户、时间戳 |
一条规则是怎么被评估的
收到工具调用
│
▼
加载这个工作空间 + 全局的所有规则,按优先级排序
│
▼
按优先级遍历每条规则:
┌─ 工具名匹配模式吗?
│ └─ 不 → 下一条
├─ 参数模式匹配吗(如果有)?
│ └─ 不 → 下一条
└─ 都匹配 → 执行这条规则的动作,停
│
▼
没有规则匹配 → 执行默认策略
│
▼
动作:allow / deny / require_approval
│
▼
写一条审计日志
│
▼
执行 / 拒绝 / 挂起等审批优先级更高的规则先执行。第一个匹配的规则赢。一条规则可以限定到特定的工作空间,也可以是全局的。
示例规则
规则 1(优先级 100):ShellExecuteTool,参数匹配 "^(ls|cat|grep|find)\\s" → allow
规则 2(优先级 50): ShellExecuteTool → require_approval
规则 3(优先级 50): WriteFileTool,arg.path 以 "/tmp" 开头 → allow
规则 4(优先级 40): WriteFileTool → require_approval
规则 5(优先级 30): * → allow(默认)只读的 shell 命令立刻执行。其他的需要审批。/tmp 下的文件写入自由;其他地方需要审批。其他所有工具放行。
管理规则
设置 → 安全与审批 → Tool Guard 规则 提供完整 UI。或者走配置文件:
mateclaw:
tool:
guard:
enabled: true
default-policy: require_approval
rules:
- tool: ShellExecuteTool
arg-pattern: "^(ls|cat|grep|find)\\s"
action: allow
priority: 100
- tool: ShellExecuteTool
action: require_approval
priority: 50
- tool: WriteFileTool
arg-pattern: "^/tmp/"
action: allow
priority: 50
- tool: WriteFileTool
action: require_approval
priority: 40或者走 API:
curl -X POST http://localhost:18088/api/v1/security/guard/rules \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"tool": "ShellExecuteTool",
"argPattern": "^(ls|cat|grep|find)\\s",
"action": "allow",
"priority": 100
}'凭证规则的开关(1.4.0)
凭证规则现在支持逐条开关——每条规则可单独 enable / disable,可单独设定决定(allow / deny / require_approval),整套守护规则还可以 JSON 导出 / 导入,方便在多套部署之间迁移或版本化管理。
危险模式检测
除了用户定义的规则之外,MateClaw 的 shell 工具内置了一套危险模式检测——不管你的规则怎么写,有些模式本身就是危险的。find -delete、rm -rf /、用管道把 bash 接到下载上之类的模式,即使有规则本来会 allow,也会强制触发更高级别的审批。
审批工作流 —— 人在回路
当一条规则评估为 require_approval 时,MateClaw 不会简单地让调用失败。它会在回合中途挂起 Agent,创建一条 pending approval,呈现给用户,等用户决定之后从暂停的地方恢复执行。
1.3.0 起:工作流也走同一套审批
v1.3.0 的 工作流 await_approval step 通过同一套 mate_tool_approval 表挂起整条 workflow run,跨服务重启不丢;审批结果通过 channel 通知(飞书 / 钉钉 / Slack / 企微)推回审批人,resolve 后 workflow runtime 自动 resume 下一 step。也就是说——同一份审计、同一份通知、同一种"暂停—恢复"语义,同时覆盖 Agent 工具调用和 workflow step。
工作流
Agent 调用工具
│
▼
Tool Guard:require_approval
│
▼
创建 mate_tool_approval 行(status=pending)
│
▼
图状态里 AWAITING_APPROVAL=true
│
▼
发出 approval_required SSE 事件
│
▼
图干净地终止
│
▼
前端显示审批卡片
│
▼
用户点 Approve 或 Reject
│
▼
POST /api/v1/chat/stream,消息为 /approve 或 /deny
│
├─ Approved → 重新加载 Agent,replay 工具调用,继续推理
└─ Rejected → 把拒绝作为 observation 返回,继续推理"replay" 机制很重要。Agent 恢复时不会从头重新推理——它直接跳到已经批准的工具调用、执行、从观察继续。没有重复的 LLM 调用,没有浪费的 token。
当前 Web 路径没有写入型 POST /api/v1/approvals/{id}/resolve 端点。批准和拒绝走普通聊天同一条 SSE 通道,这样 replay、持久化和取消都在同一个生命周期里。
mate_tool_approval 表
| 列 | 用途 |
|---|---|
id | 主键 |
agent_id | 哪个 Agent 在等 |
conversation_id | 哪个会话被挂起 |
tool_name | 要调的工具 |
tool_args | 实际参数的 JSON |
rule_id | 触发审批的规则 |
status | pending / approved / denied / consumed / timeout / superseded |
requested_at | 审批被创建的时间 |
resolved_at | 用户决定的时间 |
resolved_by | 谁决定的 |
notes | 决定时可选的备注 |
占位符替换
有时候 Agent 的工具参数里带占位符——一个被计算出来的文件路径、一条带模板的命令。审批工作流在弹出对话框之前就替换完占位符,用户看到的是真正的值。审批同样返回替换后的值,Agent 执行的就是用户看到的。
超时
Pending approval 在一个可配置的超时后过期(默认 30 分钟)。过期的审批变成 timeout,Agent 把这个过期当作用户的拒绝一样对待。
通知
MateClaw 可以通过 channel/notification/ 适配器通知——邮件、应用内提醒、钉钉/飞书推送。在 设置 → 安全与审批 → 通知 里配置。
当前 API 表面
# 刷新页面后补水 pending 审批
curl http://localhost:18088/api/v1/chat/{conversationId}/pending-approvals \
-H "Authorization: Bearer <token>"
# 在等待中的会话里批准
curl -N -X POST http://localhost:18088/api/v1/chat/stream \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"agentId":"1","conversationId":"conv-abc123","message":"/approve"}'
# 在等待中的会话里拒绝
curl -N -X POST http://localhost:18088/api/v1/chat/stream \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"agentId":"1","conversationId":"conv-abc123","message":"/deny"}'
# 管理自动批准授权
curl http://localhost:18088/api/v1/approval/grants \
-H "Authorization: Bearer <token>"自动批准:命中要可见,未命中要可解释(2.0.0+)
以前最气人的场景:你按直觉配好了自动批准策略,工具调用还是进了人审——策略页显示"已启用",审计日志只写"需审批",全程没有一处告诉你为什么没放行。2.0.0 把这条链路做成透明的:
- 未命中原因细分。自动批准解析器不再把所有未命中笼统记成"无策略":有策略但严重度上限不够(比如上限 LOW 挡住了 HIGH 的调用——最常见的踩坑)、根本没有候选策略、工作区不匹配、CRITICAL 强制人审……每种原因都有独立的原因码。
- 结果落在审计行上。守卫审计日志的每一行都记录自动批准的最终结果与原因——自动放行的调用不再误导性地显示"需审批",进人审的调用能一眼看到"为什么"。审计行还挂上了真实的待审批 ID,能从审计直接跳到那次审批。
- 审计页一键补策略。看到"严重度上限不够"这类未命中,审计行旁边就是创建授权快捷入口,带着工具名、范围、建议的严重度上限直接进表单——不用凭记忆重新配。
- 表单防呆。范围 ID 从自由文本换成按范围类型的选择器(AGENT 选智能体、CONVERSATION 选会话、WORKSPACE 选工作区),配错类型的死策略从源头绝迹;严重度上限带语义提示("LOW 只放行低危调用");跨工作区的死配置在创建时直接拒绝——配了永远不可能生效的策略,系统当场告诉你,而不是让你在审计日志里猜。
CRITICAL 永远人审、safety floor 硬拦截的底线语义不变。
File Guard
File Guard 是文件系统级的访问控制。它坐在读写文件的任何工具或技能下面,决定哪些路径在边界内。
评估管道
文件访问请求
│
▼
路径规范化(解析 ..、符号链接、相对路径)
│
▼
白名单检查:路径在允许的目录里吗?
│
▼
黑名单检查:路径在被拒的目录里吗?
│
▼
符号链接检查:顺着链接走会跳出沙盒吗?
│
▼
允许 / 拒绝内置规则
| 规则 | 说明 |
|---|---|
| 工作空间隔离 | 默认访问限定在工作空间目录内 |
| 系统路径拒绝 | /etc、/usr、/bin、/boot 等默认拒绝 |
| 敏感文件保护 | .ssh、.config、.env 拒绝 |
| 路径穿越防护 | ../ 攻击被检测和阻止 |
| 符号链接检查 | 符号链接的目标被解析并重新校验 |
配置
允许 / 禁止路径规则存在数据库,从管理台「安全」页或 GET / PUT /api/v1/security/guard/config/file-guard 管理——不在 application.yml。application.yml 里只有一项:会话没有 per-workspace base path 时,文件 / Shell 工具被限制其中的全局兜底沙箱根:
mateclaw:
workspace:
sandbox:
enabled: true # 设 false 恢复旧的不受限行为
root: ${user.dir}/data/workspace # 兜底沙箱根,启动时自动创建可视化编辑器在 设置 → 安全与审批 → File Guard。
工作空间隔离
工作空间是 MateClaw 把多个团队的数据隔开的方式。每个 Agent、技能、Wiki、会话、记忆文件都属于且只属于一个工作空间。
沿工作空间边界生效的安全基元
- File Guard——路径白名单默认是
workspace/{workspaceId}/... - Tool Guard 规则——可以限定到特定的工作空间
- Wiki 知识库——归属于工作空间,只有成员能读
- 记忆文件——每个 Agent 的记忆在它工作空间的目录下面
- 渠道——每个渠道归属于一个工作空间
角色(四级 RBAC)
权限叠加——高角色继承低角色的全部能力。
| 角色 | 能力(继承下层后新增) |
|---|---|
| Viewer | chat、view:wiki。只读。为了让聊天能跑通,Viewer 还能读取当前激活模型、读取员工的工作空间文件。 |
| Member | Viewer + view:memory、view:dashboard、manage:wiki、manage:agents |
| Admin | Member + manage:skills、manage:channels、manage:models、manage:security、manage:settings |
| Owner | 与 Admin 相同,外加 owner 专属:删除工作空间、转移所有权 |
后端是能力的唯一真相源——后端维护一份 RoleCapabilities 映射,前端从不本地推导。切换工作空间后、或遇到与权限相关的 403 时,前端调用 GET /api/v1/workspaces/{id}/access,拿回 memberRole、isGlobalAdmin、effectiveRole、capabilities。
全局管理员 vs 工作空间角色:mate_user.role='admin' 是系统级全局管理员——管理用户、创建工作空间,以 owner 等同的权限横跨所有工作空间(即便它不是某工作空间的成员);mate_workspace_member.role 是每工作空间的角色。系统级端点(模型 / provider / OAuth / 数据源、用户管理、创建工作空间)要求全局管理员(@RequireGlobalAdmin);工作空间级端点(技能 / 工具 / 插件)要求工作空间角色——读需要 Member、写需要 Admin。
完整细节在 工作空间。
工作空间隔离不覆盖的
- 共享的全局配置——JWT secret、模型 provider key、MCP 服务定义都是全局的
- 审计日志的跨工作空间访问——带权限的安全管理员可以跨所有工作空间查询审计事件
审计日志
每一个安全相关的动作都被记在 mate_audit_event。仅追加——你不能改一条已有的记录,按配置的窗口(默认 90 天)保留。
记什么
| 事件类型 | 捕获的数据 |
|---|---|
| 工具调用 | 工具名、参数、结果摘要、耗时、Agent、工作空间 |
| Tool Guard 决定 | 匹配的规则、执行的动作、规则 ID |
| 审批 | 谁批准/拒绝、什么时候、备注 |
| File Guard 决定 | 路径、允许/拒绝、原因 |
| 技能执行 | 技能名、参数、Agent |
| 登录事件 | 用户、IP、成功/失败 |
| 配置变更 | 安全相关设置的旧值和新值 |
记录结构
timestamp 什么时候发生
user_id 谁做的(自动事件是 system)
action 做了什么
resource 对什么做的
details 具体细节的 JSON
result success / failure / denied
ip_address 源 IP(可用时)
workspace_id 属于哪个工作空间查询
设置 → 安全与审批 → 审计日志:按时间范围、事件类型、用户、工作空间、结果过滤。导出 CSV。
API:
curl "http://localhost:18088/api/v1/audit/events?from=2026-04-01&to=2026-04-11&action=tool_call" \
-H "Authorization: Bearer <token>"技能安全扫描
自定义技能在变活之前会被扫描危险模式:
| 检查 | 找什么 |
|---|---|
| Prompt 注入 | 覆盖 system prompt 的企图、隐藏指令 |
| 危险工具引用 | 不在允许列表里的工具,或请求了需要审批却没声明的工具 |
| 外部 URL 引用 | 技能正文里指向不可信外部资源的链接 |
| 脚本注入 | 嵌入的脚本或代码执行企图 |
严重级别
| 级别 | 动作 |
|---|---|
CRITICAL | 安装被阻止;必须修好 |
HIGH | 警告 + 管理员必须确认 |
MEDIUM | 显示警告;允许安装 |
LOW | 仅记录 |
INFO | 仅记录 |
扫描报告在 设置 → 安全与审批 → 技能扫描 里。
API Key 保护
- API keys 在数据库里加密存储
- Keys 在所有 API 响应里脱敏显示(
sk-****abcd)——创建之后永远不会完整返回给前端 - MCP 服务的
env_json和headers_json值按同样方式脱敏 - MCP 配置里的环境变量引用(
${VAR})在运行时从进程环境解析
网络安全
生产建议
| 建议 | 细节 |
|---|---|
| HTTPS | 用反向代理 + TLS(Nginx 或 Caddy) |
| 关掉 H2 console | 生产环境 spring.h2.console.enabled=false |
| 防火墙 | 只开放对外端口 |
| 限流 | 在反向代理层配置 |
| MySQL,不是 H2 | 生产用独立的 MySQL 8 实例 |
Nginx 反向代理示例
server {
listen 443 ssl;
server_name mateclaw.example.com;
ssl_certificate /etc/ssl/certs/mateclaw.pem;
ssl_certificate_key /etc/ssl/private/mateclaw.key;
location / {
proxy_pass http://localhost:18080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE 支持
proxy_buffering off;
proxy_read_timeout 86400s;
}
}出站请求防护(SSRF)
凡是 Agent 能驱动的对外 HTTP 请求,都默认带 SSRF 防护,避免被诱导去探测内网或云厂商元数据端点。覆盖三条出站路径:
| 出站路径 | 触发方 | 默认行为 |
|---|---|---|
| 浏览器工具 | browser_use 的 open 动作 | 解析目标主机,命中受限地址即拒绝 |
| Hook Webhook | Hook 动作的 HTTP 调用 | 主机须在 trusted-domains 内,且不得是私网地址 |
| 图片下载 | 图片工具按 URL 拉取素材 | 命中私网/回环主机即拒绝 |
默认拦截的地址类别:回环(127.0.0.0/8、::1)、私网(10/8、172.16/12、192.168/16)、链路本地(169.254/16、fe80::/10)、任意本地地址、组播,以及云厂商元数据端点(169.254.169.254、100.100.100.200、192.0.0.192 等)。
放行内网地址:mateclaw.security.ssrf-allowlist
需要让 Agent 访问某个内网服务时,把它加进统一白名单。一处配置,三条出站路径同时生效。 每个条目是以下三种之一:
| 形态 | 例子 | 说明 |
|---|---|---|
| 字面主机名 | internal.corp | 大小写不敏感的精确匹配 |
| 字面 IP | 192.168.100.100 | 精确匹配该地址 |
| IPv4 CIDR 段 | 192.168.100.0/24 | 匹配该网段内的所有 IP |
mateclaw:
security:
ssrf-allowlist:
- 192.168.100.100 # 单个内网地址
- 192.168.100.0/24 # 整段内网
- internal.corp # 内网主机名放行规则只放开列出的条目:白名单里写 192.168.100.0/24 不会连带放开 192.168.200.x,写 192.168.100.100 也不会放开同段的其它 IP。改完需重启后台生效。
保持最小化
白名单条目可以重新放开云厂商元数据端点(如 169.254.169.254)。一旦放开,被攻陷的 Agent 可能借此窃取云凭据。只加确实需要的内网地址,永远不要用宽 CIDR(如 0.0.0.0/0、10.0.0.0/8)一把放开。
浏览器工具另有一个总开关 mateclaw.browser.ssrf-check-enabled(默认 true)。把它设为 false 会整体关闭浏览器路径的 SSRF 校验——连元数据端点一起放开,不推荐;优先用上面的白名单做精确放行。
安全最佳实践
- 改默认密码。 现在就改。每一个部署都改。
- 设一个真正的 JWT secret。 至少 32 字符,通过环境变量,永远不要 commit。
- 最小权限。 只启用 Agent 真的需要的工具。
- 默认
require_approval。 把 Tool Guard 的default-policy翻成require_approval,然后为安全场景加allow规则。新加的工具默认安全。 - 配好 File Guard。 在任何 Agent 真的碰文件系统之前把 allowed/denied 路径锁死。
- 定期看审计日志。 设个定时提醒。找异常。
- 盯住技能扫描。 CRITICAL 发现不该被轻易绕过。
- 网络隔离。 Ollama、H2 console、内部 MCP 服务——这些都不该对公网可达。
- 生产环境别跳过审批。 自动批准规则应该窄而具体。
allow *是定时炸弹。
安全配置参考
application.yml 里有三块安全相关配置——JWT、文件沙箱,以及出站请求白名单:
mateclaw:
jwt:
secret: ${JWT_SECRET:your-secret-key-at-least-32-chars}
expiration: 86400000 # token 有效期(毫秒)
renewal-threshold: 7200000 # 剩余有效期低于此值时滑动续期(毫秒)
# 文件 / Shell 工具的全局兜底沙箱:会话没有 per-workspace base path 时,
# 所有文件 / Shell 操作被限制在这个根目录内(fail-closed 默认)
workspace:
sandbox:
enabled: true
root: ${user.dir}/data/workspace
# 出站请求 SSRF 白名单:放行特定内网主机/IP/CIDR,浏览器、Hook、
# 图片下载三条出站路径共用。留空表示按默认策略拦截全部私网地址。
security:
ssrf-allowlist: [] # 例:[192.168.100.100, 192.168.100.0/24]其余安全配置不走 application.yml,而是存在数据库、从管理台「安全」页(或 /api/v1/security/guard/*)管理:
- Tool Guard 的开关、默认策略、规则、审批超时(默认 30 分钟)、通知渠道 →
mate_tool_guard_config/mate_tool_guard_rule - File Guard 的允许 / 禁止路径规则 →
GET/PUT /api/v1/security/guard/config/file-guard - 审计日志默认常开,逐条写入
mate_tool_guard_audit_log,可导出 CSV - 技能安全扫描的发现落在技能安装流程里,CRITICAL 发现默认拦截
