<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>AI Coding Security on Code is cheap, let&#39;s talk</title>
    <link>https://blog.ferstar.org/series/ai-coding-security/</link>
    <description>Code is cheap, let&#39;s talk</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>zh-CN</language>
    <copyright>© 2026 ferstar · [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright>
    <lastBuildDate>Sat, 26 Sep 2026 19:00:00 +0800</lastBuildDate>
    <ttl>60</ttl><atom:link href="https://blog.ferstar.org/series/ai-coding-security/index.xml" rel="self" type="application/rss+xml" /><image>
      <url>https://blog.ferstar.org/site-logo.png</url>
      <title>Code is cheap, let&#39;s talk</title>
      <link>https://blog.ferstar.org/</link>
    </image>
    
    <item>
      <title>做 RunSeal 的初衷：给本地 Agent 设一道命令执行边界</title>
      <link>https://blog.ferstar.org/posts/local-first-agent-execution-boundary-runseal/</link>
      <pubDate>Sat, 26 Sep 2026 19:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/local-first-agent-execution-boundary-runseal/</guid>
      <description>Coding Agent 长期在宿主裸奔与笨重 Docker 之间妥协，面临越界偷读凭证与未受控外发的结构性风险；介绍很早就已成型的 RunSeal 项目，通过 OS 原生策略、workspace containment、managed proxy 与 fail-closed 架构实现轻量防护；达成毫秒级拉起、全平台受控且免侵入的本地命令安全执行边界。</description><content:encoded><![CDATA[<p><a href="https://github.com/runseal-labs"  target="_blank" rel="noreferrer">RunSeal</a> 这个项目其实很早就成型了。</p>
<p>核心逻辑和测试早早跑通，当时想顺手写篇记录，结果后来忙着各种业务搬砖，开源整理一拖就拖到了现在。最近大家因为隐私和越界行为，又开始讨论 Agent 的本地安全，索性把这个项目当时的背景、踩过的坑以及技术实现理一遍。</p>
<hr>

<h2 class="relative group">本地命令执行的信任缺口
    <div id="本地命令执行的信任缺口" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9c%ac%e5%9c%b0%e5%91%bd%e4%bb%a4%e6%89%a7%e8%a1%8c%e7%9a%84%e4%bf%a1%e4%bb%bb%e7%bc%ba%e5%8f%a3" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>这几年 Coding Agent 成了很多人的主力工具。从早期的 Aider、Open Interpreter，到后来天天挂在后台的 Cursor、Claude Code、Devin、Codex，大家习惯了在终端里把执行权交给 AI：改完代码顺手跑个测试、调个脚本、装个依赖。</p>
<p>但只要去看一眼它跑出来的底层日志，问题很明显：</p>
<ol>
<li><strong>它就是你机器上的一个普通子进程。</strong> 你的终端有什么权限，它就有什么权限。读写当前项目代码是正常的，但如果顺手读一下 <code>~/.ssh/id_rsa</code>、扫一眼 <code>~/.aws/credentials</code>，或者翻翻旁边其他项目的代码，操作系统根本不会拦截。</li>
<li><strong><code>[Y/n]</code> 确认框容易变成心理安慰。</strong> 真让 Agent 排查一个复杂 Bug，一轮任务动辄调用几十次终端命令。没有人能每隔几秒认真看一行 Bash。连续按回车变成机械动作后，一旦被注入恶意指令，最后也是自己按下去的。</li>
<li><strong>已有的防护方式不够趁手。</strong>
<ul>
<li>扔进 Docker 跑：在 Mac 上通过虚拟机挂载本地目录，文件 I/O 很慢，<code>npm install</code> 能卡半天；本地配好的虚拟环境、编译器缓存和系统依赖用不上；为了下包查文档往往还得开外网，真有恶意 prompt 注入，该往外发的依然能发出去。</li>
<li>正则黑名单：拦截 <code>rm -rf /</code> 或 <code>cat ~/.ssh</code> 看着直接，但稍微用点字符串拼接、Base64 或简单脚本间接执行，静态正则就绕过去了。</li>
<li>社区小脚本：多是 Linux 上的 demo，依赖 <code>bwrap</code> 或 <code>landlock</code>。在 Mac 或 Windows 上用不上，网络策略通常也是二选一：要么拔网线，要么全开。</li>
</ul>
</li>
</ol>
<p>两边的体验很分裂：要么图省事直接跑，要么为了安全把开发体验搞得很别扭。</p>
<hr>

<h2 class="relative group">RunSeal 的设计范围
    <div id="runseal-的设计范围" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#runseal-%e7%9a%84%e8%ae%be%e8%ae%a1%e8%8c%83%e5%9b%b4" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>动手写 RunSeal 时，我在 <code>AGENTS.md</code> 里明确了边界：</p>
<blockquote><p>RunSeal 是一个 OS-native、受策略约束的本地命令安全执行环境——专注在本地命令隔离，不承接审批流、策略仪表盘或通用自动化框架。</p>
</blockquote><p>核心目标很明确：在不依赖虚拟机、不牺牲本地开发速度的前提下，在操作系统内核层把 Agent 的执行范围限定在当前工作区内。</p>
<pre class="not-prose mermaid">
flowchart TD
    A[Agent 准备跑命令] --> B[RunSeal CLI / RPC]
    B --> C{检查宿主策略能力}
    C -->|能力不满足| D[Fail-Closed 立即退出报错<br/>不静默放行]
    C -->|策略校验通过| E[构建 OS 原生执行沙箱]
    
    subgraph OS[OS 原生内核边界]
        E --> F[文件系统: Workspace-Contained<br/>仅限当前目录 + 临时 Root]
        E --> G[网络受控: Managed Proxy<br/>阻断外部直连与未授权 loopback]
    end
    
    F & G --> H[执行命令并捕获退出状态]
    H --> I[写入本地 JSONL 审计日志]
    I --> J[结果返回 Agent]
</pre>

<p>设计主要围绕这几个方面：</p>

<h3 class="relative group">1. 毫秒级原生拉起，不依赖虚拟化
    <div id="1-毫秒级原生拉起不依赖虚拟化" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-%e6%af%ab%e7%a7%92%e7%ba%a7%e5%8e%9f%e7%94%9f%e6%8b%89%e8%b5%b7%e4%b8%8d%e4%be%9d%e8%b5%96%e8%99%9a%e6%8b%9f%e5%8c%96" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>RunSeal 不启动 VM，也不用后台常驻 daemon。它直接调用操作系统底层的隔离原语：</p>
<ul>
<li><strong>Windows 作为参考实现</strong>：通过受限令牌（Restricted Token）、Job Objects 和专有 ACL 落地，策略到执行计划的映射统一走 <code>PlatformSandboxPlan</code> 契约；</li>
<li><strong>macOS / Linux 功能对齐</strong>：macOS 用 Seatbelt（<code>sandbox-exec</code>），Linux 用 Bubblewrap 加 Landlock，三级沙箱与三种网络模式全部原生执行，managed proxy 边界也已在两个平台落地（RFC-0019 / RFC-0020）。</li>
</ul>
<p>因为是系统原生调用，没有虚拟机中转损耗，启动是毫秒级的，本地编译器缓存和工具链照常使用。</p>

<h3 class="relative group">2. 工作区隔离（Workspace Containment）
    <div id="2-工作区隔离workspace-containment" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-%e5%b7%a5%e4%bd%9c%e5%8c%ba%e9%9a%94%e7%a6%bbworkspace-containment" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>在文件系统层面，RunSeal 规范了四个沙箱级别：<code>read-only</code>、<code>workspace-write</code>、<code>workspace-contained</code>，外加显式放弃保证的 <code>danger-full-access</code>。</p>
<p>核心是 <code>workspace-contained</code>：</p>
<ul>
<li>进程只能看见当前的工作区目录、运行时的私有临时目录、显式声明的只读路径，以及系统必需的最小只读基线；</li>
<li>宿主机上的其他位置（如 <code>~/.ssh</code>、<code>~/.aws</code>、<code>~/.config</code>，或其他目录的源码），在沙箱里不可见或直接报错；</li>
<li>哪怕 Agent 遇到 Prompt 注入去执行 <code>cat ~/.ssh/id_rsa</code>，或者试图通过路径穿越往外翻，内核都会直接拒绝（实测返回 <code>Operation not permitted</code>）。</li>
</ul>

<h3 class="relative group">3. 受控出网（<code>network.proxy</code>）
    <div id="3-受控出网networkproxy" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-%e5%8f%97%e6%8e%a7%e5%87%ba%e7%bd%91networkproxy" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>开发时完全断网并不现实，装依赖和查文档都需要网络。但直接放开公网，外发行为就不可控。</p>
<p>RunSeal 增加了 <code>network.proxy</code> 模式：</p>
<ul>
<li>限制直连外部公网 IP 或域名；</li>
<li>阻断未授权的本地 loopback 和宿主机 IPC 通信；</li>
<li>所有出网流量强制重定向到指定的 Managed Proxy 端点。</li>
</ul>
<p>这样代理层可以统一处理白名单路由、凭证过滤和流量审计。</p>

<h3 class="relative group">4. 无法满足策略时直接报错（Fail-Closed）
    <div id="4-无法满足策略时直接报错fail-closed" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-%e6%97%a0%e6%b3%95%e6%bb%a1%e8%b6%b3%e7%ad%96%e7%95%a5%e6%97%b6%e7%9b%b4%e6%8e%a5%e6%8a%a5%e9%94%99fail-closed" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>做安全隔离，最怕为了让命令跑通而默默放开限制。</p>
<p>如果策略指定了 <code>workspace-contained</code> 和 <code>network.proxy</code>，但当前系统权限不够或版本不支持，<strong>RunSeal 会直接 Fail-Closed 报错退出，不会静默降级为未受限环境执行</strong>。</p>
<p>项目里配合了黑盒对抗测试套件（RFC-0016），覆盖软链接穿透、父目录逃逸、环境变量污染、孤儿进程清理等常见路径，确保标明 <code>supported</code> 的能力经过了实际验证。</p>
<hr>

<h2 class="relative group">怎么用起来？
    <div id="怎么用起来" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%80%8e%e4%b9%88%e7%94%a8%e8%b5%b7%e6%9d%a5" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>RunSeal 的 CLI 刻意做得很简单：参数就那么几个，很容易嵌进各种 Agent 自动化脚本或框架里。</p>

<h3 class="relative group">1. 查一下当前机器支持什么能力
    <div id="1-查一下当前机器支持什么能力" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-%e6%9f%a5%e4%b8%80%e4%b8%8b%e5%bd%93%e5%89%8d%e6%9c%ba%e5%99%a8%e6%94%af%e6%8c%81%e4%bb%80%e4%b9%88%e8%83%bd%e5%8a%9b" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">runseal capabilities</span></span></code></pre></div></div>
<p>输出一段 JSON，告诉你当前机器能保证什么（节选，完整输出还包含 <code>features</code>、<code>capability_probes</code> 等字段）：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"platform"</span><span class="p">:</span> <span class="s2">"macos"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"sandbox_levels"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"read-only"</span><span class="p">:</span> <span class="s2">"supported"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"workspace-write"</span><span class="p">:</span> <span class="s2">"supported"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"workspace-contained"</span><span class="p">:</span> <span class="s2">"supported"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"danger-full-access"</span><span class="p">:</span> <span class="s2">"supported"</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"network_modes"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"unmanaged"</span><span class="p">:</span> <span class="s2">"supported"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"disabled"</span><span class="p">:</span> <span class="s2">"supported"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"proxy"</span><span class="p">:</span> <span class="s2">"supported"</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>协议与策略版本号则由 <code>runseal version --json</code> 单独给出。</p>

<h3 class="relative group">2. 跑一条受限命令
    <div id="2-跑一条受限命令" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-%e8%b7%91%e4%b8%80%e6%9d%a1%e5%8f%97%e9%99%90%e5%91%bd%e4%bb%a4" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>把要执行的命令限定在当前工作区，同时彻底断网：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">runseal <span class="nb">exec</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --policy workspace-contained <span class="se">\
</span></span></span><span class="line"><span class="cl">  --network disabled <span class="se">\
</span></span></span><span class="line"><span class="cl">  --cwd /Users/ferstar/myprojects/demo <span class="se">\
</span></span></span><span class="line"><span class="cl">  -- python3 test_pipeline.py</span></span></code></pre></div></div>
<p>如果命令里试图越界读取 <code>~/.ssh</code>，内核会直接拒绝这次读取。默认模式下终端不会回显任何报错——但命令已经被拦死了，加 <code>--json</code> 就能看到完整结果（节选）：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">runseal <span class="nb">exec</span> --json <span class="se">\
</span></span></span><span class="line"><span class="cl">  --policy workspace-contained <span class="se">\
</span></span></span><span class="line"><span class="cl">  --network disabled <span class="se">\
</span></span></span><span class="line"><span class="cl">  --cwd /Users/ferstar/myprojects/demo <span class="se">\
</span></span></span><span class="line"><span class="cl">  -- cat ~/.ssh/id_rsa</span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"exit_code"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"stderr"</span><span class="p">:</span> <span class="s2">"cat: /Users/ferstar/.ssh/id_rsa: Operation not permitted"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"sandbox"</span><span class="p">:</span> <span class="p">{</span><span class="nt">"enforced"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nt">"level"</span><span class="p">:</span> <span class="s2">"workspace-contained"</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>注意 <code>runseal exec</code> 自身的退出码目前固定为 0，子进程的真实退出状态以 JSON 结果和审计日志为准；如果要把结果接进自动化流程，<code>--json</code> 输出结构化结果，<code>--events</code> 则按行流式吐出执行事件。</p>

<h3 class="relative group">3. 本地结构化审计
    <div id="3-本地结构化审计" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-%e6%9c%ac%e5%9c%b0%e7%bb%93%e6%9e%84%e5%8c%96%e5%ae%a1%e8%ae%a1" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>每次执行到底用了什么策略、对应的 Canonical Policy Hash 是多少、网络做没做阻断，都会实时落盘成 JSONL 审计记录（节选，每条事件还带有 <code>execution_id</code>、<code>session_id</code>、<code>policy_epoch</code> 等字段）：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="nt">"type"</span><span class="p">:</span><span class="s2">"policy.resolved"</span><span class="p">,</span><span class="nt">"policy_id"</span><span class="p">:</span><span class="s2">"workspace-contained"</span><span class="p">,</span><span class="nt">"policy_hash"</span><span class="p">:</span><span class="s2">"sha256:7cb64a73..."</span><span class="p">,</span><span class="nt">"sandbox_level"</span><span class="p">:</span><span class="s2">"workspace-contained"</span><span class="p">,</span><span class="nt">"network"</span><span class="p">:{</span><span class="nt">"mode"</span><span class="p">:</span><span class="s2">"disabled"</span><span class="p">,</span><span class="nt">"routes"</span><span class="p">:[]},</span><span class="nt">"time"</span><span class="p">:</span><span class="s2">"2026-09-26T11:21:26.967279Z"</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="nt">"type"</span><span class="p">:</span><span class="s2">"execution.finished"</span><span class="p">,</span><span class="nt">"exit_code"</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span><span class="nt">"status"</span><span class="p">:</span><span class="s2">"finished"</span><span class="p">,</span><span class="nt">"time"</span><span class="p">:</span><span class="s2">"2026-09-26T11:21:26.971042Z"</span><span class="p">}</span></span></span></code></pre></div></div>
<hr>

<h2 class="relative group">一点想法
    <div id="一点想法" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%80%e7%82%b9%e6%83%b3%e6%b3%95" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>前阵子 ZCode 那个静默上传代码仓的事情闹得沸沸扬扬，其实那只是把“本地 Agent 权限裸奔”这个隐患，以最戏剧化的一种方式曝光在大众面前而已。</p>
<p>AI 编程工具迭代得太快了。模型越来越聪明，自主执行的链路也越来越长。但我们不能一边享受着几倍的代码产出速度，一边把主机的“万能钥匙”全押在服务商的道德自觉或者一份不可证伪的用户协议上。</p>
<p>平时在开发机上干活，没必要天天跟防贼一样提心吊胆，但也别等哪天真的被端了底裤才想起来穿衣服。</p>
<p>把不可逾越的物理边界稳稳卡在操作系统内核层，该放行的放行，该掐死的掐死——这才是让 Agent 能够放手跑下去的长久之计。</p>
<p><em>(RunSeal 的 RFC 规范和代码都在 GitHub 上公开维护，感兴趣的可以来看看：<a href="https://github.com/runseal-labs"  target="_blank" rel="noreferrer">github.com/runseal-labs</a>)</em></p>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
