同步至个人站点:Memo Code 安全设计:子进程、命令防护与权限审批的统一方案
Memo Code 是我最近两个多月投入较多精力的 Agent 项目。类似于Claude Code 和 Codex 的 轻量级本地编程 Agent,目前已具备 Coding Agent 完备技能。
如果你感兴趣的话,欢迎参与:Memo Code - Github,或者给个 Star 鼓励一下哈哈~
做 Agent 这类能「替用户干活」的工具,安全性是躲不掉的坎。
我一开始做 memo(github.com/minorcell/m…)的时候,安全问题还没想那么多——能跑起来就行。后来工具越加越多,shell 命令也越跑越复杂,就开始踩坑了:
rm -rf / 差点真被我跑出来这些问题逼着我认真设计了整套安全方案。今天把思路和实现细节都分享出来,希望对你有帮助。
我把它拆成三件事:
下面逐一展开。
memo 的 shell 执行用的是 Node.js 的 child_process.spawn,但光 spawn 是不够的——你还得管得住。
我写了一个 UnifiedExecManager(packages/tools/src/tools/exec_runtime.ts),核心思路是单例 + 会话池:
classUnifiedExecManager { private sessions = newMap<number, SessionState>() private nextId = 1privateMAX_SESSIONS = 64}好处很明显:
先看数量限制:
asyncstart(request: StartExecRequest) { this.cleanupSessions() if (this.activeSessionCount() >= MAX_SESSIONS) { thrownewError(`too many active sessions (max ${MAX_SESSIONS})`) } // ...}超过 64 个活跃会话就直接拒绝,防止被LLM恶意耗尽系统资源。
再看输出限制。Agent 交互是基于 token 计费的,子进程输出不能无限制返回:
functiontruncateByTokens(text: string, maxOutputTokens?: number) { const maxChars = (maxOutputTokens || 2000) * 4if (text.length <= maxChars) { return { output: text, deliveredChars: text.length } } return { output: text.slice(0, maxChars), deliveredChars: maxChars, }}默认最多返回 8000 字符,不够可以调,但不会无限大。
子进程跑飞了是常见问题。memo 的策略是先礼貌后强硬:
privateasyncterminateForTimeout(session: SessionState) { if (session.exited) return session.proc.kill('SIGTERM') awaitwaitForExit(session, 200) // 等 200msif (!session.exited) { session.proc.kill('SIGKILL') // 还是没退就直接杀了awaitwaitForExit(session, 200) }}为什么要等一下?因为有些程序接收到 SIGTERM 会做清理工作(比如写入缓存、关闭句柄),直接 SIGKILL 可能导致数据丢失。
会话不能只增不减。我加了一个自动清理逻辑:
privatecleanupSessions() { if (this.sessions.size <= MAX_SESSIONS) return// 优先清理已退出的,按启动时间从早到晚排序const ended = Array.from(this.sessions.values()) .filter(session => session.exited) .sort((a, b) => a.startedAtMs - b.startedAtMs) for (const session of ended) { if (this.sessions.size <= MAX_SESSIONS) breakthis.sessions.delete(session.id) }}这样即使跑了几百个命令,内存也不会无限涨。
子进程管住了还不够,还得管住跑什么命令。
我见过太多「rm -rf /」惨案,也见过 dd if=/dev/zero of=/dev/sda 这种物理层面不可逆的破坏。memo 的做法是命令解析 + 黑名单匹配。
直接正则匹配 rm -rf 是有漏洞的。比如 sudo rm -rf /、包裹在 bash -c 里、甚至写成十六进制,都能绕过简单匹配。
memo 的做法是先把命令拆成「段」,再逐段解析:
functionsplitCommandSegments(command: string) { // 按 ; | && || 分割,处理引号和转义// 返回每一段独立的命令}functionparseSegment(segment: string) { // 跳过 sudo/env/nohup 等包装// 提取真实的命令名和参数}这样不管外面包了多少层 sudo env bash -c,最终都能追溯到真正的命令。
目前 memo 拦截这几类(packages/tools/src/tools/command_guard.ts):
| 规则 | 触发条件 | 危险等级 |
|---|---|---|
rm_recursive_critical_target | rm -rf 目标包含 /、~、$HOME 等关键路径 | 极高 |
mkfs_filesystem_create | mkfs/mkfs.xxx | 极高 |
dd_write_block_device | dd 写入 /dev/ 下的块设备 | 极高 |
disk_mutation_block_device | fdisk/parted/shred 等操作块设备 | 高 |
redirect_block_device | 输出重定向到 /dev/ 块设备 | 高 |
拦截后返回的是 <system_hint> 标记,不是直接报错,方便 Agent 理解为什么被拦:
<system_hinttype="tool_call_denied"tool="exec_command"reason="dangerous_command"policy="blacklist"rule="rm_recursive_critical_target"command="rm -rf /"> Blocked a high-risk shell command to prevent irreversible data loss. Use a safer and scoped alternative.</system_hint>命令守卫是第一道关卡,但还有很多「不危险但需要知道」的操作,比如写文件、改配置。审批系统的目标就是分级管理、可追溯、可配置。
memo 把工具分成三级(packages/tools/src/approval/constants.ts):
| 级别 | 含义 | 审批策略(auto 模式) |
|---|---|---|
read | 只读操作 | 免审批 |
write | 文件修改 | 需审批 |
execute | 执行命令 | 需审批 |
check(toolName: string, params: unknown): ApprovalCheckResult { if (ALWAYS_AUTO_APPROVE_TOOLS.has(toolName)) { return { needApproval: false, decision: 'auto-execute' } } const riskLevel = classifier.getRiskLevel(toolName) if (!classifier.needsApproval(riskLevel, approvalMode)) { return { needApproval: false, decision: 'auto-execute' } } // 生成指纹,返回需要审批}如果每次执行都要点批准,用户体验会非常差。memo 用指纹 + 缓存解决这个问题:
const fingerprint = generateFingerprint(toolName, params)cache.toolByFingerprint.set(fingerprint, toolName)// 审批后记录recordDecision(fingerprint, decision: 'session' | 'once' | 'deny') { switch (decision) { case'session': cache.sessionTools.add(toolName); breakcase'once': cache.onceTools.add(toolName); breakcase'deny': cache.deniedTools.add(toolName); break }}审批系统是安全了,但有时候用户就是想要「无限制」——比如在本地开发、或者明确知道自己在干什么。
memo 提供了 dangerous 模式:
if (dangerous) { return { isDangerousMode: true, getRiskLevel: () =>'read', // 所有操作都视为最低风险check: () => ({ needApproval: false, decision: 'auto-execute' }), isGranted: () =>true, }}开启也很简单,CLI 里加上 --dangerous 标记:
memo --dangerous开启后:
这是一把双刃剑。 我在 CLI 里加了这个选项,但默认是关闭的。开发者如果想用,需要明确加上 --dangerous 标记。
memo 的安全设计可以总结为:
这套方案不完美,还在持续迭代。比如命令守卫目前是硬编码的黑名单,后续可以考虑支持用户自定义规则;审批系统也可以考虑接入外部信任模型。
(完)
小米路由器插件安装失败解决方案(小米路由器插件安装失败怎么办)
小米路由器为什么无法成功安装插件(小米路由器安装插件失败怎么处理)
小米路由器为什么无法将电视屏幕投射到其他设备上(如何解决小米路由器无法进行电视投屏的问题)
小米路由器为什么无法连接电视网络(小米路由器和电视连接不上网络怎么办)
Spring Boot项目中varchar字段为什么不用NULL?告别空指针从建表开始
程序员如何自学?编程能够自学吗?