Hermes 架构全解Hermes Architecture, in Full第 6 章 · 14 章Chapter 6 of 14
全文目录Contents
  1. 0 · 项目全景与代码地图
    1. 0.1 这个软件是什么
    2. 0.2 目录地图
    3. 0.3 从这张地图能读出的三件事
    4. 0.4 一条消息的完整旅程
    5. 0.5 全文章节索引
  2. 1 · 网关层
    1. 1.1 它解决什么问题
    2. 1.2 平台适配器抽象基类
    3. 1.3 消息事件的归一化
    4. 1.4 网关主循环里的那些防御机制
    5. 1.5 状态消息与进度反馈
    6. 1.6 中断与恢复
    7. 1.7 网关内置钩子
    8. 1.8 这一层的代价
  3. 2 · 身份与会话路由
    1. 2.1 Profile:一个进程承载多个身份
    2. 2.2 四级路由与「具体度」打分
    3. 2.3 显式路由被拒绝的情况
    4. 2.4 SOUL.md:人格文件
    5. 2.5 会话路由:跨平台的连续性
    6. 2.6 智能体实例的缓存
  4. 3 · 主循环 ★
    1. 3.1 循环入口:三重预算闸门
    2. 3.2 每轮的准备工作:build_turn_context
    3. 3.3 中途插话:/steer
    4. 3.4 墙上时钟预算的收尾提醒
    5. 3.5 中断检查
    6. 3.6 步骤回调:给网关的观测点
    7. 3.7 循环里的其他状态
    8. 3.8 循环退出原因
    9. 3.9 一个可选的旁路:Codex 应用服务运行时
  5. 4 · 工具系统
    1. 4.1 最重要的设计:实现与投放分离
    2. 4.2 工具集的组合与解析
    3. 4.3 工具的中心分发
    4. 4.4 参数强制矫正层
    5. 4.5 工具错误消息的净化
    6. 4.6 工具结果的观测与钩子
    7. 4.7 委派上下文识别
    8. 4.8 工具定义的缓存
  6. 5 · 审批与安全红线 ★
    1. 5.1 整体结构
    2. 5.2 12 条硬红线
    3. 5.3 真正的难点:区分「命令」和「数据」
    4. 5.4 引号遮蔽:但要给「真会执行的部分」留后门
    5. 5.5 引号不能成为绕过手段
    6. 5.6 敏感路径与写入目标
    7. 5.7 sudo 标准输入守卫
    8. 5.8 性能:预编译的理由
    9. 5.9 被拦截命令的留存
    10. 5.10 智能审批:可选的模型判断
    11. 5.11 上下文感知:不同场景不同策略
    12. 5.12 这一层的定位:最外面也最薄
  7. 6 · 执行环境
    1. 6.1 七种可选环境
    2. 6.2 必须诚实说明的一件事
    3. 6.3 抽象基类里的共用逻辑
    4. 6.4 文件同步
    5. 6.5 环境的选择时机
  8. 7 · 上下文引擎 ★
    1. 7.1 它定义的是什么
    2. 7.2 生命周期
    3. 7.3 三个必须实现的方法
    4. 7.4 最精辟的设计:select 和 compress 是两个正交动词
    5. 7.5 后置观察钩子
    6. 7.6 其他可选钩子
    7. 7.7 默认参数值
    8. 7.8 用户可见状态的可控性
    9. 7.9 内置实现的体量对比
  9. 8 · 记忆系统 ★
    1. 8.1 记忆提供者接口
    2. 8.2 生命周期与钩子
    3. 8.3 接口的版本化契约
    4. 8.4 琐碎提问过滤器
    5. 8.5 记忆使用指示器
    6. 8.6 内置的全息记忆
    7. 8.7 存储层与信任分
    8. 8.8 SQLite 状态层
    9. 8.9 三种记忆的分工总结
  10. 9 · 插件系统
    1. 9.1 三个发现来源
    2. 9.2 插件能提供什么
    3. 9.3 最重要的设计:区分「可叠加能力」与「互斥策略」
    4. 9.4 插件的存储
    5. 9.5 插件与工具集的联动
    6. 9.6 插件钩子
    7. 9.7 MCP:另一条扩展路径
    8. 9.8 这套扩展体系的整体形状
  11. 10 · 委派与多智能体
    1. 10.1 为什么需要委派
    2. 10.2 深度限制:只允许一层
    3. 10.3 并发限制
    4. 10.4 子智能体的工具限制
    5. 10.5 子智能体的审批策略
    6. 10.6 运行中的控制
    7. 10.7 亲缘关系检查
    8. 10.8 看板:智能体之间的协作
    9. 10.9 委派系统为什么有 5,071 行
  12. 11 · 模型供应商与凭据池
    1. 11.1 为什么不能只支持一家
    2. 11.2 供应商适配器清单
    3. 11.3 凭据池:核心机制
    4. 11.4 PooledCredential:单个凭据的状态
    5. 11.5 「持久化」为什么必要
    6. 11.6 与其他机制的配合
    7. 11.7 供应商抽象要抽象到哪一层
  13. 12 · 定时任务
    1. 12.1 「cron」是什么
    2. 12.2 定时智能体的场景
    3. 12.3 最重要的一个类:CronPromptInjectionBlocked
    4. 12.4 定时任务的工具集收窄
    5. 12.5 失败处理
    6. 12.6 防重复执行
    7. 12.7 为什么 scheduler.py 有 367 KB
    8. 12.8 定时任务在整个架构里的位置
  14. 13 · 技能系统
    1. 13.1 技能是什么
    2. 13.2 前置元数据逐字段解释
    3. 13.3 渐进式披露:技能系统的核心机制
    4. 13.4 技能的支撑设施
    5. 13.5 技能 vs 工具 vs 插件
    6. 13.6 技能系统的一个隐含设计:可组合
    7. 13.7 15 个技能类别
    8. 13.8 全文回顾:Hermes 的整体形状
  1. 0 · The Project at a Glance, and a Map of the Code
    1. 0.1 What This Software Is
    2. 0.2 Directory Map
    3. 0.3 Three Things You Can Read Off This Map
    4. 0.4 The Full Journey of One Message
    5. 0.5 Chapter Index
  2. 1 · The Gateway Layer
    1. 1.1 The Problem It Solves
    2. 1.2 The Platform Adapter Abstract Base Class
    3. 1.3 Normalizing Message Events
    4. 1.4 The Defensive Mechanisms in the Gateway Main Loop
    5. 1.5 Status Messages and Progress Feedback
    6. 1.6 Interruption and Recovery
    7. 1.7 Built-in Gateway Hooks
    8. 1.8 What This Layer Costs
  3. 2 · Identity and Session Routing
    1. 2.1 Profiles: One Process, Many Identities
    2. 2.2 Four-Level Routing and the “Specificity” Score
    3. 2.3 When an Explicit Route Is Rejected
    4. 2.4 SOUL.md: The Persona File
    5. 2.5 Session Routing: Continuity Across Platforms
    6. 2.6 Caching Agent Instances
  4. 3 · The Main Loop ★
    1. 3.1 The Loop Entry: A Triple Budget Gate
    2. 3.2 Per-Turn Preparation: build_turn_context
    3. 3.3 Mid-Turn Interjection: /steer
    4. 3.4 The Wall-Clock Budget Wrap-Up Reminder
    5. 3.5 The Interrupt Check
    6. 3.6 The Step Callback: An Observation Point for the Gateway
    7. 3.7 Other State Inside the Loop
    8. 3.8 Why the Loop Exited
    9. 3.9 An Optional Bypass: The Codex App Server Runtime
  5. 4 · The Tool System
    1. 4.1 The Most Important Design Decision: Implementation Separated from Exposure
    2. 4.2 Composing and Resolving Toolsets
    3. 4.3 Central Tool Dispatch
    4. 4.4 The Argument Coercion Layer
    5. 4.5 Sanitizing Tool Error Messages
    6. 4.6 Observing Tool Results, and Hooks
    7. 4.7 Recognizing a Delegation Context
    8. 4.8 Caching Tool Definitions
  6. 5 · Approval and Safety Red Lines ★
    1. 5.1 The Overall Structure
    2. 5.2 The 12 Hard Red Lines
    3. 5.3 The Real Difficulty: Telling “Command” from “Data”
    4. 5.4 Quote Masking: But Leave a Way In for “the Part That Really Executes”
    5. 5.5 Quoting Is Not a Bypass
    6. 5.6 Sensitive Paths and Write Targets
    7. 5.7 The sudo Stdin Guard
    8. 5.8 Performance: Why Precompile
    9. 5.9 Retaining Blocked Commands
    10. 5.10 Smart Approval: An Optional Model Judgment
    11. 5.11 Context Awareness: Different Situations, Different Policies
    12. 5.12 Where This Layer Sits: Outermost, and Thinnest
  7. 6 · Execution Environments
    1. 6.1 Seven Environments to Choose From
    2. 6.2 One Thing That Has to Be Said Plainly
    3. 6.3 Shared Logic in the Abstract Base Class
    4. 6.4 File Synchronization
    5. 6.5 When the Environment Is Chosen
  8. 7 · The Context Engine ★
    1. 7.1 What It Defines
    2. 7.2 Lifecycle
    3. 7.3 The Three Methods You Must Implement
    4. 7.4 The Sharpest Design Decision: select and compress Are Two Orthogonal Verbs
    5. 7.5 The Post-Turn Observation Hook
    6. 7.6 Other Optional Hooks
    7. 7.7 Default Parameter Values
    8. 7.8 Control Over User-Visible Status
    9. 7.9 Size Comparison of the Built-in Implementation
  9. 8 · The Memory System ★
    1. 8.1 The Memory Provider Interface
    2. 8.2 Lifecycle and Hooks
    3. 8.3 The Interface's Versioned Contract
    4. 8.4 The Trivial-Prompt Filter
    5. 8.5 The Memory-Usage Indicator
    6. 8.6 The Built-in Holographic Memory
    7. 8.7 The Storage Layer and Trust Scores
    8. 8.8 The SQLite State Layer
    9. 8.9 Summing Up the Division of Labor Among Three Kinds of Memory
  10. 9 · The Plugin System
    1. 9.1 Three Discovery Sources
    2. 9.2 What a Plugin Can Provide
    3. 9.3 The Most Important Design Decision: “Stackable Capabilities” vs. “Mutually Exclusive Strategies”
    4. 9.4 Plugin Storage
    5. 9.5 How Plugins Tie into Toolsets
    6. 9.6 Plugin Hooks
    7. 9.7 MCP: The Other Extension Path
    8. 9.8 The Overall Shape of This Extension System
  11. 10 · Delegation and Multi-Agent
    1. 10.1 Why Delegation Is Needed
    2. 10.2 The Depth Limit: One Level Only
    3. 10.3 Concurrency Limits
    4. 10.4 Tool Restrictions on Subagents
    5. 10.5 Approval Policy for Subagents
    6. 10.6 In-Flight Control
    7. 10.7 The Lineage Check
    8. 10.8 Kanban: Collaboration Between Agents
    9. 10.9 Why the Delegation System Is 5,071 Lines
  12. 11 · Model Providers and the Credential Pool
    1. 11.1 Why You Can't Support Just One
    2. 11.2 The Provider Adapter Roster
    3. 11.3 The Credential Pool: The Core Mechanism
    4. 11.4 PooledCredential: The State of a Single Credential
    5. 11.5 Why “Persistent” Is Necessary
    6. 11.6 How It Works with Other Mechanisms
    7. 11.7 How Deep the Provider Abstraction Should Go
  13. 12 · Scheduled Tasks (Cron)
    1. 12.1 What “cron” Is
    2. 12.2 Scenarios for Scheduled Agents
    3. 12.3 The Single Most Important Class: CronPromptInjectionBlocked
    4. 12.4 Narrowing the Toolset for Scheduled Tasks
    5. 12.5 Failure Handling
    6. 12.6 Preventing Duplicate Runs
    7. 12.7 Why scheduler.py Is 367 KB
    8. 12.8 Where Scheduled Tasks Sit in the Overall Architecture
  14. 13 · The Skill System
    1. 13.1 What a Skill Is
    2. 13.2 The Front Matter, Field by Field
    3. 13.3 Progressive Disclosure: The Core Mechanism of the Skill System
    4. 13.4 The Infrastructure Around Skills
    5. 13.5 Skills vs. Tools vs. Plugins
    6. 13.6 An Implicit Design in the Skill System: Composability
    7. 13.7 The 15 Skill Categories
    8. 13.8 Looking Back Across the Book: The Overall Shape of Hermes

6 · 执行环境

tools/environments/,12 个文件。这一章讲工具实际在哪里跑 —— 也就是纵深防御里唯一真正的硬边界。

6.1 七种可选环境

环境大小隔离程度与用途
local.py
本机
91.9 KB 默认。⚠️ 无沙箱。命令直接交给宿主机的 shell 执行。速度最快,但智能体拥有和你完全相同的权限
docker.py
容器
91.2 KB 在 Docker 容器里执行。文件系统、进程、网络都被隔离。这是最常见的生产选择
modal.py
managed_modal.py
16.9 + 9.7 KB Modal 云端沙箱。按需启动、闲置零成本。适合「智能体大部分时间在睡觉」的场景
vercel_sandbox.py 21.0 KB Vercel 的沙箱服务
daytona.py 9.8 KB Daytona 远程开发环境
singularity.py 10.0 KB Singularity 容器 —— 高性能计算集群常用(大学、科研机构的 GPU 集群通常不给 Docker 权限,只给 Singularity)
ssh.py 17.1 KB 通过 SSH 在另一台主机上执行

另外两个辅助模块:base.py(68.3 KB,抽象基类与共用逻辑)和 file_sync.py(20.2 KB,宿主机与环境之间的文件同步)。

6.2 必须诚实说明的一件事

2026 年 4 月的第三方安全审计

审计检查了约 36.4 万行代码。结论是:没有发现恶意代码、后门或隐藏的数据上报

但同时报告了 4 个「严重」(critical)级别、9 个「高」(high)级别的架构问题。头号问题是:

在默认的本机后端下,terminal 工具把命令直接交给系统 shell 执行,没有沙箱、没有白名单。也就是说:默认安装等于给模型一个真实的、完整权限的终端。

上一章那 5,802 行的红线代码不能替代这一层。它拦的是「一眼看去就是灾难」的命令,拦不住一条精心构造的、或者通过合法工具组合达成的破坏。

6.3 抽象基类里的共用逻辑

base.py 有 68.3 KB —— 它不只是一个接口定义,还包含了所有环境共用的实现。

连接类失败的专门异常

class EnvironmentConnectionError(RuntimeError):
    """Infrastructure/connection-class failure of a terminal backend."""

    def __init__(self, reason: str, *, retry_hint: str = ""):
        ...

为什么要单独一个异常类型?因为需要区分两种失败:

失败类型该怎么处理
命令本身失败
(编译错误、文件不存在)
把错误信息返回给模型,让它自己想办法。这是正常的工作流程
环境连接失败
(容器没启动、SSH 断了、云沙箱超时)
不是模型的问题。应该重试、或者告诉用户去检查基础设施

如果不区分,模型会收到「连接失败」并试图「修复」它 —— 但它根本无能为力,只会白白浪费几轮尝试。而 retry_hint(重试提示)字段说明这个异常还携带了「该怎么重试」的信息。

有界输出收集器

class _BoundedOutputCollector:
    """Retain a bounded 40/60 head-tail window of streamed text."""

    def __init__(self, max_chars: int, spill_path: "Path | None" = None): ...
    def _maybe_spill(self, text: str) -> None:
        """Tee ``text`` to the spill file (opened lazily on first overflow)."""
    def close_spill(self) -> "str | None":
        """Close the spill file and return its path if it was used."""
    def buffered_chars(self) -> int
    def total_chars(self) -> int
    def append(self, text: str) -> None
    def render(self, *, suffix: str = "") -> str:
        """Render within ``max_chars``, preserving a required status suffix."""
「40/60 头尾窗口」是什么意思

一条命令可能输出几十万行(比如跑一个大项目的测试)。这些输出不能全部进上下文。

朴素做法:只留前 N 行,或者只留后 N 行。两种都有问题:

  • 只留开头 → 丢失了最关键的错误摘要和退出码(那些通常在末尾)
  • 只留结尾 → 丢失了是哪一步开始出问题的(那在开头)

头尾窗口:保留开头 40%、结尾 60%,中间省略。这样两端的关键信息都在。

而且比例是不对称的(40/60 而不是 50/50)—— 因为结尾通常信息密度更高(错误汇总、失败列表、退出状态)。

_maybe_spill(溢出落盘)的设计也很实用:超出窗口的完整输出被写到一个文件里,而且是「第一次溢出时才惰性打开文件」。大多数命令输出很短,根本不会溢出 —— 那就完全不产生文件 I/O。真的溢出了,模型可以拿到文件路径去读全文。

render(suffix=...) 里那个「保留必需的状态后缀」也值得注意:无论怎么截断,「命令退出码是多少」这类状态信息必须保留。它们不能因为输出太长就被截掉。

活动回调:长命令的心跳

def set_activity_callback(cb: Callable[[str], None] | None) -> None:
    """Register a callback that _wait_for_process fires periodically."""
def get_activity_callback() -> Callable[[str], None] | None:
    """Return the thread-local activity callback…"""
def touch_activity_if_due(...):
    """Fire the activity callback at most once every ``state['interval']`` seconds."""

一条命令可能跑几分钟(编译、测试、下载)。这段时间里:

  • 用户需要知道「还在跑,没死」
  • 网关需要更新聊天窗口的状态消息
  • 不能每秒都刷 —— 会刷屏、会触发平台的频率限制

touch_activity_if_due(到期才触发)就是节流器:最多每 N 秒触发一次回调。而且它是线程局部的 —— 因为多个工具可能在不同线程里并行执行,各自需要自己的回调。

沙箱目录

def get_sandbox_dir() -> Path:
    """Return the host-side root for all sandbox storage (Docker workspaces, …)"""

所有沙箱的宿主机侧存储都在一个统一的根目录下。这样清理、备份、磁盘配额管理都有单一入口。

6.4 文件同步

file_sync.py(20.2 KB)解决的是一个必然出现的问题:如果工具在容器/远程主机里执行,那么它读写的文件在哪里?

场景:你的代码在本机 ~/myproject/ 智能体的 terminal 工具跑在 Docker 容器里 模型说:"读一下 src/main.py" ↓ 容器里没有这个文件 —— 除非挂载或同步进去 模型说:"把这个文件改成……" ↓ 改的是容器里的副本。你在本机看不到变化 —— 除非同步回来 → 需要一层双向文件同步

这一层的存在解释了为什么隔离是有成本的:不只是「启动容器慢」,还有持续的文件同步开销和一致性问题。这也是为什么本机模式是默认值 —— 它最快、最简单,代价是没有隔离。

6.5 环境的选择时机

环境是按会话/身份配置的,不是按单次工具调用。这个粒度选择有它的道理:

  • 如果按单次调用切换环境,那么「先写个文件、再读它」这样的连续操作会跨环境失效
  • 而且每次切换都有启动开销

所以更合理的模式是:高信任场景(你自己的终端)用本机;低信任场景(公开 webhook、多用户群聊)用容器。而这个判断和第 4 章的工具集投放是同一个维度 —— 信任边界

两层防护的正确组合
信任级别工具集(第 4 章)执行环境(本章)

你自己的终端
全量核心工具本机(快)

团队群聊
核心工具,可能去掉几个Docker 容器

公开 webhook
只有 4 个只读工具容器(即使工具已经很安全,也不给例外)

两层是相乘的关系,不是二选一。工具集收窄减少了攻击面,执行隔离限制了攻击的后果。任何一层单独都不够。

6 · Execution Environments

tools/environments/, 12 files. This chapter is about where tools actually run — which is the only truly hard boundary in the defense-in-depth stack.

6.1 Seven Environments to Choose From

EnvironmentSizeDegree of isolation and purpose
local.py
Local
91.9 KB The default. ⚠️ No sandbox. Commands are handed straight to the host machine's shell. Fastest, but the agent has exactly the same permissions you do
docker.py
Container
91.2 KB Runs inside a Docker container. Filesystem, processes, and network are all isolated. The most common production choice
modal.py
managed_modal.py
16.9 + 9.7 KB Modal cloud sandbox. Starts on demand, costs nothing while idle. Suited to setups where “the agent is asleep most of the time”
vercel_sandbox.py 21.0 KB Vercel's sandbox service
daytona.py 9.8 KB Daytona remote development environments
singularity.py 10.0 KB Singularity containers — common on high-performance computing clusters (university and research-lab GPU clusters usually don't grant Docker permissions, only Singularity)
ssh.py 17.1 KB Executes on another host over SSH

Plus two supporting modules: base.py (68.3 KB, the abstract base class and shared logic) and file_sync.py (20.2 KB, file synchronization between the host and the environment).

6.2 One Thing That Has to Be Said Plainly

The third-party security audit of April 2026

The audit examined about 364,000 lines of code. Its conclusion: no malicious code, backdoors, or hidden data exfiltration were found.

But it also reported 4 “critical” and 9 “high” severity architectural issues. The number one issue:

Under the default local backend, the terminal tool hands commands directly to the system shell, with no sandbox and no allowlist. In other words: a default install hands the model a real, fully privileged terminal.

The 5,802 lines of red-line code from the previous chapter cannot substitute for this layer. They stop commands that are “obviously a disaster at a glance”; they cannot stop a carefully constructed one, or damage achieved by combining legitimate tools.

6.3 Shared Logic in the Abstract Base Class

base.py is 68.3 KB — it isn't just an interface definition; it also contains the implementation shared by every environment.

A dedicated exception for connection-class failures

class EnvironmentConnectionError(RuntimeError):
    """Infrastructure/connection-class failure of a terminal backend."""

    def __init__(self, reason: str, *, retry_hint: str = ""):
        ...

Why a separate exception type? Because two kinds of failure need to be told apart:

Kind of failureHow to handle it
The command itself failed
(compile error, file not found)
Return the error to the model and let it work something out. This is the normal workflow
The environment connection failed
(container not started, SSH dropped, cloud sandbox timed out)
Not the model's problem. Retry, or tell the user to check the infrastructure

Without the distinction, the model receives “connection failed” and tries to “fix” it — but there's nothing it can do, and it just burns a few rounds of attempts. The retry_hint field shows that the exception also carries information about “how to retry.”

The bounded output collector

class _BoundedOutputCollector:
    """Retain a bounded 40/60 head-tail window of streamed text."""

    def __init__(self, max_chars: int, spill_path: "Path | None" = None): ...
    def _maybe_spill(self, text: str) -> None:
        """Tee ``text`` to the spill file (opened lazily on first overflow)."""
    def close_spill(self) -> "str | None":
        """Close the spill file and return its path if it was used."""
    def buffered_chars(self) -> int
    def total_chars(self) -> int
    def append(self, text: str) -> None
    def render(self, *, suffix: str = "") -> str:
        """Render within ``max_chars``, preserving a required status suffix."""
What a “40/60 head-tail window” means

A single command can produce hundreds of thousands of lines (running a large project's test suite, say). All of that cannot go into the context.

The naive approaches: keep only the first N lines, or only the last N lines. Both have problems:

  • Keep only the head → you lose the most important part, the error summary and exit code (usually at the end)
  • Keep only the tail → you lose which step things started going wrong at (that's at the beginning)

The head-tail window: keep the first 40% and the last 60%, and elide the middle. The key information at both ends survives.

And the split is asymmetric (40/60 rather than 50/50) — because the tail is usually denser in information (error roll-ups, failure lists, exit status).

The design of _maybe_spill is also very practical: the full output beyond the window is written to a file, and the file is “opened lazily on the first overflow.” Most command output is short and never overflows — so no file I/O happens at all. When it really does overflow, the model gets the file path and can read the whole thing.

The “preserving a required status suffix” in render(suffix=...) also deserves attention: however the output is truncated, status information like “what was the command's exit code” must survive. It can't get cut off just because the output was too long.

The activity callback: a heartbeat for long commands

def set_activity_callback(cb: Callable[[str], None] | None) -> None:
    """Register a callback that _wait_for_process fires periodically."""
def get_activity_callback() -> Callable[[str], None] | None:
    """Return the thread-local activity callback…"""
def touch_activity_if_due(...):
    """Fire the activity callback at most once every ``state['interval']`` seconds."""

A command may run for minutes (compiling, testing, downloading). During that time:

  • The user needs to know “still running, not dead”
  • The gateway needs to update the status message in the chat window
  • But not every second — that spams the channel and trips the platform's rate limits

touch_activity_if_due is the throttle: fire the callback at most once every N seconds. And it is thread-local — because several tools may be executing in parallel on different threads, and each needs its own callback.

The sandbox directory

def get_sandbox_dir() -> Path:
    """Return the host-side root for all sandbox storage (Docker workspaces, …)"""

All host-side sandbox storage lives under one root directory. That gives cleanup, backup, and disk-quota management a single entry point.

6.4 File Synchronization

file_sync.py (20.2 KB) solves a problem that is bound to come up: if tools execute inside a container or on a remote host, where are the files they read and write?

Scenario: your code is on your machine at ~/myproject/ the agent's terminal tool runs inside a Docker container The model says: "read src/main.py" ↓ The container doesn't have that file — unless it's mounted or synced in The model says: "change this file to…" ↓ It changes the copy inside the container. You see no change on your machine — unless it's synced back → you need a layer of two-way file synchronization

The existence of this layer explains why isolation has a cost: not just “containers are slow to start,” but ongoing file-sync overhead and consistency problems. That is also why local mode is the default — it's the fastest and simplest, at the price of no isolation.

6.5 When the Environment Is Chosen

The environment is configured per session/identity, not per tool call. There's a reason for that granularity:

  • If the environment switched per call, a sequence like “write a file, then read it” would break across environments
  • And every switch carries startup overhead

So the more sensible pattern is: high-trust situations (your own terminal) run local; low-trust situations (public webhooks, multi-user group chats) run in a container. And that judgment is the same axis as the toolset exposure in Chapter 4 — the trust boundary.

The right way to combine the two layers of protection
Trust levelToolset (Chapter 4)Execution environment (this chapter)
High
Your own terminal
The full core toolsetLocal (fast)
Medium
Team group chat
Core tools, possibly minus a fewDocker container
Low
Public webhook
Only the 4 read-only toolsContainer (no exceptions, even though the tools are already safe)

The two layers multiply; they are not an either/or. Narrowing the toolset shrinks the attack surface; execution isolation limits the consequences of an attack. Neither layer is enough on its own.