echo-agent 前身为 2025 年 11 月启动的个人助理项目 fubot,最初面向长期陪伴型个人智能体,围绕认知记忆、上下文延续、用户偏好沉淀、任务闭环与持续自我优化展开。随着真实场景迭代,项目逐步形成多入口接入、统一事件模型、消息总线、Agent Loop、多模型抽象、工具调用、MCP 接入、任务调度、权限审批、运行轨迹、长期记忆和受控自演进等能力。目前已支持微信、QQ、CLI、Gateway、Webhook、Cron 等入口,服务用户超过 20 万、累计下载超过 50 万,是面向长期运行、记忆增强和可持续成长智能体的开源 Agent Runtime。

项目地址:https://github.com/fuyuxiang/echo-agent

14-cover

你让 Agent “帮我把测试跑一下,顺手修掉失败用例”。

一个普通脚本会直接拼出 pytestnpm test,然后在当前机器上执行。看起来很自然,直到你发现测试脚本会写缓存、安装依赖、访问网络,甚至启动一个长期占用端口的开发服务。

对工程师来说,问题从来不是“模型会不会生成命令”,而是:这条命令在哪里运行、继承谁的权限、能访问哪些文件、能不能联网、失败后留下什么状态

这就是执行器要解决的问题。

问题入口

工具系统解决的是“模型可以调用什么能力”。执行器解决的是“这些能力在哪里、以什么边界运行”。

如果把命令执行直接写在 ShellTool 里,短期看很快:工具收到 command,本地 subprocess 一跑,stdout 返回给模型。但这样会把四件事揉在一起:工具协议、运行环境、安全策略和部署形态。

结果是系统很难回答几个关键问题:

问题 直接写在工具里的后果
本地能跑,云端怎么跑 工具和宿主环境强耦合
需要隔离时怎么办 每个工具都要重写沙箱逻辑
是否允许联网 策略散落在工具内部
凭证怎么注入 工具容易读取全局环境变量
后台进程谁清理 一次调用结束后状态不可见

执行器不是为了让 Agent 更敢执行命令,而是为了让每一次执行都有明确的副作用半径。

为了不停留在抽象层面,下面以 echo-agent 的实现为例。它把 ShellToolCodeExecToolProcessTool 和真实运行环境之间拆出一层 executor:工具只表达“我要执行什么”,executor 决定“在哪里执行、怎么隔离、如何返回结果”。

执行契约

echo-agent 的执行器代码位于 echo_agent.agent.executors。基础契约由两个数据结构承担:ExecRequestExecResponse

ExecRequest 表示一次执行请求,包含命令、工作目录、环境变量、超时、标准输入和凭证。ExecResponse 表示执行结果,包含 stdout、stderr、返回码、耗时、执行器名称和审计 ID。

这组契约的意义不在于字段多,而在于它把“执行命令”从某个具体 subprocess 调用中抽离出来:

@dataclass
class ExecRequest:
    command: str
    cwd: str = ""
    env: dict[str, str] = field(default_factory=dict)
    timeout: int = 30
    stdin: str = ""
    credentials: dict[str, str] = field(default_factory=dict)
​
@dataclass
class ExecResponse:
    success: bool = True
    stdout: str = ""
    stderr: str = ""
    return_code: int = 0
    duration_ms: int = 0
    executor: str = ""
    audit_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])

所有执行器继承同一个 BaseExecutor,对工具层暴露统一的 execute()setup()teardown()

class BaseExecutor(ABC):
    name: str = "base"
​
    @abstractmethod
    async def execute(self, request: ExecRequest) -> ExecResponse:
        ...
​
    @abstractmethod
    async def setup(self) -> None:
        ...
​
    @abstractmethod
    async def teardown(self) -> None:
        ...

这个抽象让工具层不必知道底层是本地进程、临时沙箱、Docker 容器,还是远程 SSH。它只需要构造一次执行请求,剩下的边界问题交给执行器。

14-信任边界

这里还有一个容易被忽略的设计:executor 是长期存在的。create_executor() 会根据配置创建 localsandboxcontainerremote 执行器,并让它随 AgentLoop 复用。

原因很现实。沙箱目录、容器和远程连接的初始化都有成本。如果每次工具调用都重新 setup,Agent 会变慢,也会留下更多临时状态。长期存在意味着 setup 成本只付一次;同时也要求 executor 在停止时通过 teardown() 释放资源。

环境选择

执行器选择不是性能偏好,而是信任模型。

echo-agent 当前提供四类执行器:

执行器 运行位置 适合场景 主要风险
LocalExecutor 当前宿主机 个人 CLI、受信任工作区、外部已有隔离 直接影响真实文件和进程
SandboxExecutor 临时目录副本 默认隔离、运行测试、避免污染原工作区 隔离强度有限,仍依赖宿主环境
ContainerExecutor Docker 容器 不可信代码、依赖隔离、镜像化环境 需要 Docker,生命周期更复杂
RemoteExecutor SSH 远程主机 部署、远程计算、集中资源管理 凭证、审计和结果可信度要求更高

LocalExecutor 最直接。它会在宿主机上用 asyncio.create_subprocess_shell 执行命令。执行前会检查网络策略,如果 network_policy == "deny" 且命令命中网络行为,就返回失败结果。

它适合受信任的本机场景,但不适合公共入口或多租户环境。因为它继承的是当前用户权限,误操作会直接落在真实文件系统和真实进程空间。

SandboxExecutor 会在 sandbox root 下创建临时目录,并把 workspace 复制进去。复制时会忽略 .git__pycache__.pytest_cache.ruff_cache.venvnode_modulesdata/logs 等目录,减少成本,也避免把不必要的大目录带入沙箱。

执行时,它会把 HOMETMPDIR 指向沙箱工作目录,并限制 PATH。如果请求的 cwd 不在源 workspace 下,会回退到沙箱 workdir。这使得命令即使传入外部路径,也不容易跳出受控目录。

ContainerExecutor 通过 Docker 创建长期容器,挂载 workspace 到 /workspace,再通过 docker exec 执行命令。如果网络策略是 deny,容器使用 --network none;否则使用 bridge。Docker 不存在时,setup 会明确抛出错误。

RemoteExecutor 通过 SSH 执行远程命令。它会构造 StrictHostKeyCheckingConnectTimeout、key path 等参数,并对 cwd、环境变量和凭证做 shell quoting。书稿里特别提到测试覆盖了带空格 cwd、特殊字符 env、密钥路径和连接参数,这说明远程执行关注的不只是“能 ssh”,还包括命令拼接安全。

14-副作用半径

同一个 pytest,在不同环境里的意义完全不同。在沙箱副本中运行,主要影响临时目录;在真实 workspace 中运行,可能写缓存、更新快照或生成报告;在远程机器上运行,影响范围可能跨出本机和当前用户。

执行环境决定副作用半径。不能只问命令能不能跑,还要问它跑错时会影响哪里。

工具适配

执行器不是替代工具层安全检查。它和工具层是前后两道边界。

ShellTool 的工具名是 exec,参数包括 commandtimeoutcwd。它在调用 executor 之前会先做几类检查:内置危险模式、allowed/blocked 列表、安全策略评估,以及审批上下文。

例如,书稿中提到内置阻断模式会覆盖 /etc/passwd|shadow|sudoers|gshadow、根目录破坏性删除、文件系统格式化和系统关机等行为。之后还会检查 allowlist,未命中的命令会被拒绝。

cwd 也不能随意指向系统目录。ShellTool 会解析传入目录,把相对路径映射到 workspace 下,再调用 cwd 策略检查。违反边界时,工具返回类似 cwd is outside workspace 的失败结果。

最后,它才把命令交给 executor:

response = await executor.execute(ExecRequest(
    command=command,
    cwd=cwd,
    timeout=timeout,
    env={"WORKSPACE": workspace},
    credentials=ctx.credentials if ctx else {},
))

这段最小代码说明了三件事。第一,工具仍负责理解自己的参数和风险。第二,执行上下文里的凭证按请求传入,而不是让工具读取全局密钥。第三,真实执行统一落到 executor,便于切换运行环境。

CodeExecTool 的边界类似,但重点不同。它的工具名是 execute_code,支持 Python、JavaScript 和 Bash 三种 runner:python3 -nodebash。初始化时可以限制允许语言;模型请求不允许的语言会直接失败。

这里的语言 allowlist 是一个很朴素但有效的边界。用户让 Agent 写一段 Python 数据处理脚本,并不等于授权它运行 Bash 安装脚本;允许执行 JavaScript,也不等于允许它调用任意系统命令。语言本身就是风险信号。

代码片段通过 stdin 传入 executor,而不是拼接进 shell 命令字符串:

await executor.execute(ExecRequest(
    command="python3 -",
    cwd=str(workspace),
    timeout=timeout,
    stdin=code,
    env={"WORKSPACE": str(workspace)},
    credentials=ctx.credentials if ctx else {},
))

这个细节很重要。长代码如果塞进命令字符串,会引入 quoting、转义和截断问题。stdin 更接近“把代码交给 runner”,也更容易统一记录和审计。

后台进程

一次性命令退出后,返回码和输出就能描述大部分状态。后台进程不一样。它会继续运行,继续占用端口、写日志、消耗资源,甚至在当前回答结束后仍然影响系统。

echo-agent 用 ProcessTool 管理后台进程。它的工具名是 process,支持四个动作:startlistpollstop

启动前,ProcessTool 同样会调用 shell 安全策略。启动成功后,进程记录在模块级 _PROCESSES 中,保存 process 对象、启动命令、开始时间、stdout/stderr 缓冲等信息。系统会异步收集输出;poll 返回最后一段 stdout/stderr;stop 先 terminate,超时后 kill。

14-进程生命周期

这说明后台进程不是“长一点的 shell 命令”,而是生命周期对象。

一个生产级 Agent 如果能启动开发服务器,就必须能回答:它启动了什么、属于哪个会话、监听哪个端口、日志在哪里、现在是否存活、什么时候应该停止、停止失败怎么办。

否则,Agent 的行动会在宿主机里留下不可解释的状态。你看见端口被占用,却不知道是哪次任务启动的;你看见日志滚动,却不知道它属于哪个用户;你让 Agent 停止,它也不知道该停哪一个进程。

会启动后台进程,只说明 Agent 有运行时能力;能管理后台进程,才说明它有运行时边界。

输出治理

命令和代码执行会产生大量输出。测试日志、构建日志、安装日志、后台进程日志都可能很长,也可能包含密钥、连接串、用户路径和隐私数据。

所以输出截断不是简单的体验优化,而是上下文治理和安全机制。

echo-agent 的 ShellTool 和 CodeExecTool 都会截断 stdout/stderr。原因有两个:一是保护模型上下文窗口,二是避免把大量无关或敏感输出原样灌回对话通道。

更合理的输出策略应该分两层:给模型的是摘要、退出码、关键错误、少量上下文和日志引用;给工程师追溯的是完整原始日志或审计记录。

这也是 ExecResponsereturn_codeduration_msexecutoraudit_id 有价值的地方。一次执行不是一段字符串输出,而是一条可追踪的工作单元。

如果后续要把执行器做得更生产化,还需要继续补齐产物管理:哪些文件被创建或修改,哪些报告需要返回给用户,哪些沙箱产物要同步回真实 workspace,哪些临时文件应该清理,哪些输出需要脱敏。

生产可用性

判断一个 Agent 执行器是否接近生产级,不能只看“能不能跑命令”。更硬的检查项应该是这些:

检查项 可验证标准
统一契约 命令、cwd、env、stdin、timeout、credentials 都进入 ExecRequest
环境隔离 至少能区分本地、沙箱、容器或远程执行
生命周期 executor 有 setup/teardown,后台进程有 start/list/poll/stop
文件边界 cwd 不能越过 workspace 或策略允许范围
网络策略 工具层和 executor 层都尊重 network policy
凭证注入 凭证从执行上下文按请求传入,不读取全局密钥
输出治理 stdout/stderr 有截断、摘要和完整日志分层
审计追踪 每次执行有 executor 名称、耗时、返回码和 audit_id
测试约束 覆盖 allowlist、网络 deny、stdin、cwd quoting、env quoting 和审批路径

这些标准背后的共同原则是:模型可以提出行动,但不能直接拥有宿主环境。

执行器把自然语言驱动的意图变成受控物理动作。它要处理文件系统边界、网络边界、进程边界、凭证边界和资源边界。哪怕模型判断错了,错误也应该被限制在工作区、副本、容器或受控远端内,而不是直接扩散到最敏感环境。

小结

执行器是 Agent 架构中最接近真实风险的位置。

工具层告诉模型“你可以请求什么能力”,执行器层决定“这项能力以什么身份、在哪个环境、在什么限制下发生”。这个区分越清楚,系统越容易根据任务风险切换执行环境,也越容易审计和复盘。

echo-agent 的 executor 抽象把本地、沙箱、容器和远程执行统一到同一套 ExecRequest / ExecResponse 契约下,再由 ShellTool、CodeExecTool 和 ProcessTool 分别承接一次性命令、代码片段和后台进程。

这不是把执行命令做复杂,而是承认一个现实:Agent 一旦能行动,风险就不再停留在文本里。真正的工程设计,必须在行动落地之前先画出边界。

(全篇完)


本文为 echo-agent 设计笔记系列第 14 篇。项目源码已开源至 GitHub。如果你对工业级 Agent 的工程落地感兴趣,欢迎加入技术交流群参与日常讨论。下一篇我们将探讨 《Agent 的安全边界:从可见性到执行控制》,敬请期待。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐