<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Security | Francis Du</title>
    <link>https://francisdu.com/tags/security/</link>
      <atom:link href="https://francisdu.com/tags/security/index.xml" rel="self" type="application/rss+xml" />
    <description>💻Data Engineer | 🦀 Rustacean | 📷 Photographer | 🤖Vibe Coder</description>
    <generator>Hugo 0.166.0</generator><language>zh-CN</language><copyright>© Francis Du</copyright><lastBuildDate>Wed, 26 Aug 2026 23:35:00 +0800</lastBuildDate>
    <item>
      <title>wcode 的授权现在怎么做</title>
      <link>https://francisdu.com/blog/wcode-authorization/</link>
      <pubDate>Wed, 26 Aug 2026 23:35:00 +0800</pubDate>
      <guid>https://francisdu.com/blog/wcode-authorization/</guid>
      <description>&lt;p&gt;我一直不想给 Agent 一个 Shell。&lt;/p&gt;
&lt;p&gt;但实际用久了以后，我也越来越不喜欢另一个极端：只要一个工具不在默认列表里，就永久拒绝；真要用的时候，只能重启进程加一个很宽的 Trust Flag。&lt;/p&gt;
&lt;p&gt;这两个选择都太粗：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;完全不许
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   或
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;整个进程都信任
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最近我把这些情况统一收进了 Pending Authorization Queue。模型可以把请求挂出来，最后还是我自己选哪一条批、哪一条拒绝。&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-access-management_hu_ac447683faaa24eb.webp&#34; alt=&#34;wcode 授权与访问控制界面&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-access-management_hu_ac447683faaa24eb.webp 960w, https://francisdu.com/img/wcode/wcode-access-management_hu_e5f2de5679dfbc7e.webp 1600w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;1600&#34; height=&#34;1000&#34;&gt;&lt;/figure&gt;&lt;h2 id=&#34;为什么-yn-以前看起来没用&#34;&gt;为什么 &lt;code&gt;Y/N&lt;/code&gt; 以前看起来没用&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e4%b8%ba%e4%bb%80%e4%b9%88-yn-%e4%bb%a5%e5%89%8d%e7%9c%8b%e8%b5%b7%e6%9d%a5%e6%b2%a1%e7%94%a8&#34; aria-label=&#34;章节链接：为什么 Y/N 以前看起来没用&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;TUI 其实早就有 &lt;code&gt;Y/N&lt;/code&gt; 的 Key Handler。&lt;/p&gt;
&lt;p&gt;问题是 UI 只在 Footer 里塞了一个 Pending 数字。&lt;/p&gt;
&lt;p&gt;用户看不到：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;到底是谁在申请？
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;申请哪个 Workspace？
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;要跑什么？
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;这是普通命令还是删除？
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Y 到底会批准哪一条？
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所以从人的角度看，&lt;code&gt;Y/N&lt;/code&gt; 就像两个没有上下文的快捷键。&lt;/p&gt;
&lt;p&gt;现在 Pending Request 会直接显示成一个列表。&lt;/p&gt;
&lt;p&gt;大概是这种信息：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;AUTHORIZATION REQUIRED                         3 PENDING
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;› AUTH-00000021 [COMMAND]      web   · authorize command: hugo
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  AUTH-00000020 [RISKY EXEC]   api   · allow repository-aware command: cargo metadata
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  AUTH-00000019 [DELETE]       api   · delete file: src/obsolete.rs
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;↑/↓ select request   Y approve selected   N deny selected
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Y&lt;/code&gt; 不再是“批准最新一条”。&lt;/p&gt;
&lt;p&gt;它只批准当前选中的 Request。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;N&lt;/code&gt; 也一样。&lt;/p&gt;
&lt;p&gt;当多个 Agent / Tool 同时工作时，这个区别很重要。&lt;/p&gt;
&lt;h2 id=&#34;命令授权不再被默认-catalog-封死&#34;&gt;命令授权不再被默认 Catalog 封死&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e5%91%bd%e4%bb%a4%e6%8e%88%e6%9d%83%e4%b8%8d%e5%86%8d%e8%a2%ab%e9%bb%98%e8%ae%a4-catalog-%e5%b0%81%e6%ad%bb&#34; aria-label=&#34;章节链接：命令授权不再被默认 Catalog 封死&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;最开始 wcode 有一份固定 Command Catalog：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cargo
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rustc
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rg
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pnpm
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;yarn
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bun
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;node
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python3
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pytest
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;go
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;make
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它很适合作为&lt;strong&gt;默认预授权集合&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但把它当成“用户永远只能授权这些命令”的上限，就会变得很别扭。&lt;/p&gt;
&lt;p&gt;真实项目会用：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hugo
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;flutter
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;deno
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mvn
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gradle
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;swift
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mix
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;甚至公司内部还有自己的 Build Tool。&lt;/p&gt;
&lt;p&gt;所以现在语义变成：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;默认安全集合 → 直接进入后续 Command Policy
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;其他合法 bare executable
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CommandAccess Pending Request
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;用户批准
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;只加入当前 Workspace 的运行时 allowlist
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;模型重试
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这不是让模型自己扩权。&lt;/p&gt;
&lt;p&gt;第一次请求仍然失败。&lt;/p&gt;
&lt;p&gt;真正改变权限的是本地用户的批准动作。&lt;/p&gt;
&lt;h2 id=&#34;为什么我还是不让它授权-bash&#34;&gt;为什么我还是不让它授权 &lt;code&gt;bash&lt;/code&gt;&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e4%b8%ba%e4%bb%80%e4%b9%88%e6%88%91%e8%bf%98%e6%98%af%e4%b8%8d%e8%ae%a9%e5%ae%83%e6%8e%88%e6%9d%83-bash&#34; aria-label=&#34;章节链接：为什么我还是不让它授权 bash&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;“用户可以授权模型请求的命令”不等于“所有字符串都应该变成可授权请求”。&lt;/p&gt;
&lt;p&gt;wcode 仍然有一层不能被 UI 点掉的硬边界。&lt;/p&gt;
&lt;p&gt;Program 必须是合法的裸可执行程序名：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hugo        ✓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;flutter     ✓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gradle      ✓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;./tool      ✗
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;../tool     ✗
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/usr/bin/x  ✗
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;foo/bar     ✗
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Shell Interpreter 也永久拒绝：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sh
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bash
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;zsh
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;fish
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pwsh
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;powershell
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cmd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;原因还是同一个。&lt;/p&gt;
&lt;p&gt;wcode 的 Command Contract 是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;program + args[]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;而不是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;shell -c arbitrary_string
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果允许模型拿到 Shell，前面的 Argument Inspection、Protected Path Check 和 No-shell 边界都会变得很容易绕。&lt;/p&gt;
&lt;p&gt;我宁愿让用户授权更多具体 Program，也不想把整个模型重新变成远程 Terminal。&lt;/p&gt;
&lt;h2 id=&#34;commandaccess-和-riskyexecution-是两层&#34;&gt;&lt;code&gt;CommandAccess&lt;/code&gt; 和 &lt;code&gt;RiskyExecution&lt;/code&gt; 是两层&lt;a class=&#34;heading-anchor&#34; href=&#34;#commandaccess-%e5%92%8c-riskyexecution-%e6%98%af%e4%b8%a4%e5%b1%82&#34; aria-label=&#34;章节链接：CommandAccess 和 RiskyExecution 是两层&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这个地方容易混。&lt;/p&gt;
&lt;p&gt;假设模型第一次想跑：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hugo --minify
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果 &lt;code&gt;hugo&lt;/code&gt; 还没被当前 Workspace 授权，先得到的是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CommandAccess
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个 Program 能不能出现在这个 Workspace？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但对已经有专门 Policy 的命令，例如 Cargo / Go / Package Manager，某些参数可能会执行 Repository-controlled Code。&lt;/p&gt;
&lt;p&gt;这时即使 Program 已经允许，仍可能得到：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;RiskyExecution
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它回答的是另一件事：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这组具体 repository-aware Operation 是否值得信任？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以权限不是一个 Boolean。&lt;/p&gt;
&lt;p&gt;更接近：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Program Access
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Argument / Path Policy
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Risky Operation Trust
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Execute
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;现在有四类-authorization&#34;&gt;现在有四类 Authorization&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e7%8e%b0%e5%9c%a8%e6%9c%89%e5%9b%9b%e7%b1%bb-authorization&#34; aria-label=&#34;章节链接：现在有四类 Authorization&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;当前我把它们分成四种。&lt;/p&gt;
&lt;h3 id=&#34;1-commandaccess&#34;&gt;1. CommandAccess&lt;a class=&#34;heading-anchor&#34; href=&#34;#1-commandaccess&#34; aria-label=&#34;章节链接：1. CommandAccess&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;模型请求当前 Workspace 尚未授权的 Program。&lt;/p&gt;
&lt;p&gt;批准以后，这个 Program 加进当前 Workspace 的运行时 allowlist。&lt;/p&gt;
&lt;h3 id=&#34;2-riskyexecution&#34;&gt;2. RiskyExecution&lt;a class=&#34;heading-anchor&#34; href=&#34;#2-riskyexecution&#34; aria-label=&#34;章节链接：2. RiskyExecution&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Program 本身允许，但这组参数可能加载或执行仓库控制的代码、配置或插件。&lt;/p&gt;
&lt;p&gt;批准的是精确 Operation Fingerprint 的 Session Grant。&lt;/p&gt;
&lt;h3 id=&#34;3-runtimeexecutor&#34;&gt;3. RuntimeExecutor&lt;a class=&#34;heading-anchor&#34; href=&#34;#3-runtimeexecutor&#34; aria-label=&#34;章节链接：3. RuntimeExecutor&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Property / Mutation / Fuzz / Runtime Canary 等 Verification Executor 可能来自仓库配置。&lt;/p&gt;
&lt;p&gt;它们是另一条显式 Trust Boundary。&lt;/p&gt;
&lt;h3 id=&#34;4-destructivedelete&#34;&gt;4. DestructiveDelete&lt;a class=&#34;heading-anchor&#34; href=&#34;#4-destructivedelete&#34; aria-label=&#34;章节链接：4. DestructiveDelete&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;这是最严格的。&lt;/p&gt;
&lt;p&gt;只能删除一个普通文件或空目录，文件要求当前 SHA，批准是 exact one-shot，用一次就失效。&lt;/p&gt;
&lt;p&gt;我不想让一个“同意删除”自动变成当前 Session 后续所有 Delete 都合法。&lt;/p&gt;
&lt;h2 id=&#34;webui-也必须能处理同一批-request&#34;&gt;WebUI 也必须能处理同一批 Request&lt;a class=&#34;heading-anchor&#34; href=&#34;#webui-%e4%b9%9f%e5%bf%85%e9%a1%bb%e8%83%bd%e5%a4%84%e7%90%86%e5%90%8c%e4%b8%80%e6%89%b9-request&#34; aria-label=&#34;章节链接：WebUI 也必须能处理同一批 Request&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;只在 TUI 做授权有个现实问题：我可能正盯着浏览器里的 Project Observatory，而不是 Terminal。&lt;/p&gt;
&lt;p&gt;所以现在 WebUI 的 &lt;code&gt;Manage access&lt;/code&gt; 里有三块：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Authorized projects
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Authorized commands
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Pending authorizations
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Project 可以在运行时添加。&lt;/p&gt;
&lt;p&gt;Command 可以手动授权或撤销。&lt;/p&gt;
&lt;p&gt;Pending Request 则逐条显示 Approve / Deny。&lt;/p&gt;
&lt;p&gt;WebUI 并不是另开了一套权限状态。&lt;/p&gt;
&lt;p&gt;它和 TUI 操作的是同一个 Authorization Manager。&lt;/p&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Model Request
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;one shared Pending Queue
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ↙              ↘
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;TUI               WebUI
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Y / N         Approve / Deny
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ↘              ↙
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; same runtime authorization state
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;WebUI 的这些 API 仍然需要原有的 Local UI Token 和 Origin Validation。&lt;/p&gt;
&lt;p&gt;不能因为它是“管理页面”，就变成一个没有保护的本地管理接口。&lt;/p&gt;
&lt;h2 id=&#34;多-workspace-时授权必须跟着项目走&#34;&gt;多 Workspace 时，授权必须跟着项目走&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e5%a4%9a-workspace-%e6%97%b6%e6%8e%88%e6%9d%83%e5%bf%85%e9%a1%bb%e8%b7%9f%e7%9d%80%e9%a1%b9%e7%9b%ae%e8%b5%b0&#34; aria-label=&#34;章节链接：多 Workspace 时，授权必须跟着项目走&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我还顺手修了一个容易忽略的问题。&lt;/p&gt;
&lt;p&gt;如果同一个进程同时暴露：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;backend
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frontend
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;website
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;CommandAccess&lt;/code&gt; 必须知道自己属于哪个 Registry Workspace ID。&lt;/p&gt;
&lt;p&gt;不能只根据目录 basename 猜，也不能批准一次以后给三个项目一起放开。&lt;/p&gt;
&lt;p&gt;所以现在授权 Request 会带精确 Workspace ID。&lt;/p&gt;
&lt;p&gt;批准 &lt;code&gt;website&lt;/code&gt; 的 &lt;code&gt;hugo&lt;/code&gt;，不会顺手让 &lt;code&gt;backend&lt;/code&gt; 也能跑 &lt;code&gt;hugo&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这是我想要的权限粒度。&lt;/p&gt;
&lt;h2 id=&#34;授权以后仍然不是-os-sandbox&#34;&gt;授权以后仍然不是 OS Sandbox&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e6%8e%88%e6%9d%83%e4%bb%a5%e5%90%8e%e4%bb%8d%e7%84%b6%e4%b8%8d%e6%98%af-os-sandbox&#34; aria-label=&#34;章节链接：授权以后仍然不是 OS Sandbox&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;最后还是要强调这一点。&lt;/p&gt;
&lt;p&gt;这些授权机制解决的是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;谁可以请求什么
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;谁决定放行
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;放行到什么粒度
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它不是操作系统级 Sandbox。&lt;/p&gt;
&lt;p&gt;一个用户批准的 Program 本身仍可能做很多事情。&lt;/p&gt;
&lt;p&gt;wcode 能继续保证的是它自己的边界：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不经过 Shell；&lt;/li&gt;
&lt;li&gt;Workspace-relative CWD；&lt;/li&gt;
&lt;li&gt;Program Name 不允许路径；&lt;/li&gt;
&lt;li&gt;Argument 不允许 Workspace Escape；&lt;/li&gt;
&lt;li&gt;Protected Path 继续拒绝；&lt;/li&gt;
&lt;li&gt;敏感环境变量继续清理；&lt;/li&gt;
&lt;li&gt;Git Helper / Mutation 等专门 Policy 继续生效；&lt;/li&gt;
&lt;li&gt;Timeout 和输出上限继续存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以授权按钮的含义不是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个命令绝对安全。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我知道模型为什么需要它，我愿意把这一层权限交给当前 Workspace。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个语义对我来说更诚实。&lt;/p&gt;
&lt;p&gt;v0.3 的完整变化见 &lt;a href=&#34;https://francisdu.com/blog/wcode-v0-3/&#34;&gt;wcode v0.3：从本地代码桥到 Software Intelligence Runtime&lt;/a&gt;，底层安全边界继续看 &lt;a href=&#34;https://francisdu.com/blog/wcode-security/&#34;&gt;我还是不想给 Agent 一个 Shell&lt;/a&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>wcode：我还是不想给 Agent 一个 Shell</title>
      <link>https://francisdu.com/blog/wcode-security/</link>
      <pubDate>Wed, 26 Aug 2026 03:17:00 +0800</pubDate>
      <guid>https://francisdu.com/blog/wcode-security/</guid>
      <description>&lt;p&gt;wcode 第一版里，我花时间最多的其实不是 MCP。&lt;/p&gt;
&lt;p&gt;是 Workspace。&lt;/p&gt;
&lt;p&gt;因为把模型接到本地仓库以后，最危险的从来不是它看错一个函数，而是 Tool Runtime 的边界没守住。&lt;/p&gt;
&lt;p&gt;上层现在已经有 Design State、Graph、Reconciliation、Verification，复杂了很多。但越往上加东西，我越不想动底下这几条线。&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-access-management_hu_ac447683faaa24eb.webp&#34; alt=&#34;wcode 授权与访问控制界面&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-access-management_hu_ac447683faaa24eb.webp 960w, https://francisdu.com/img/wcode/wcode-access-management_hu_e5f2de5679dfbc7e.webp 1600w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;1600&#34; height=&#34;1000&#34;&gt;&lt;/figure&gt;&lt;h2 id=&#34;root-不是字符串前缀&#34;&gt;Root 不是字符串前缀&lt;a class=&#34;heading-anchor&#34; href=&#34;#root-%e4%b8%8d%e6%98%af%e5%ad%97%e7%ac%a6%e4%b8%b2%e5%89%8d%e7%bc%80&#34; aria-label=&#34;章节链接：Root 不是字符串前缀&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;最偷懒的 Workspace 隔离大概是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;path.starts_with(workspace_root)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这当然不够。&lt;/p&gt;
&lt;p&gt;wcode 启动时会先 Canonicalize Root，文件系统根目录、Home 这种过大的范围默认不接受，多个 Workspace 也不能随便父子重叠。&lt;/p&gt;
&lt;p&gt;后续文件操作还会重新确认 Root。&lt;/p&gt;
&lt;p&gt;Unix 上会记 Device / Inode，所以服务启动以后，如果同一个路径被换成了另一个目录，字符串虽然没变，身份已经变了，也会停下来。&lt;/p&gt;
&lt;p&gt;模型传进来的路径只能是相对路径。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;..&lt;/code&gt;、绝对路径、Windows Prefix、受保护的凭据和 VCS 路径，都是 Workspace 层直接挡，不靠 Agent 自觉。&lt;/p&gt;
&lt;h2 id=&#34;symlink-是我早期低估过的坑&#34;&gt;Symlink 是我早期低估过的坑&lt;a class=&#34;heading-anchor&#34; href=&#34;#symlink-%e6%98%af%e6%88%91%e6%97%a9%e6%9c%9f%e4%bd%8e%e4%bc%b0%e8%bf%87%e7%9a%84%e5%9d%91&#34; aria-label=&#34;章节链接：Symlink 是我早期低估过的坑&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;只拦 &lt;code&gt;..&lt;/code&gt; 看起来像是做了 Path Traversal 防护，实际上一个 Symlink 就能把你带出去。&lt;/p&gt;
&lt;p&gt;所以 Workspace Path 的 Component 里有 Symlink，直接拒绝。&lt;/p&gt;
&lt;p&gt;已有文件和新文件还不能完全用同一种检查方式。&lt;/p&gt;
&lt;p&gt;已有文件能解析最终目标；新文件的叶子还不存在，只能先确认父目录安全，再创建。&lt;/p&gt;
&lt;p&gt;Unix 上写 Hard Link 也有限制，因为同一个 inode 可能从另一个名字被修改。&lt;/p&gt;
&lt;p&gt;这些逻辑写起来比一个 &lt;code&gt;canonicalize()&lt;/code&gt; 麻烦很多，但这是我不太愿意“简化”的地方。&lt;/p&gt;
&lt;h2 id=&#34;sha-是防-agent-和我互相踩代码&#34;&gt;SHA 是防 Agent 和我互相踩代码&lt;a class=&#34;heading-anchor&#34; href=&#34;#sha-%e6%98%af%e9%98%b2-agent-%e5%92%8c%e6%88%91%e4%ba%92%e7%9b%b8%e8%b8%a9%e4%bb%a3%e7%a0%81&#34; aria-label=&#34;章节链接：SHA 是防 Agent 和我互相踩代码&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;文件读出来以后，wcode 会给当前 SHA-256。&lt;/p&gt;
&lt;p&gt;后面修改已有文件，必须把这个 SHA 带回来。&lt;/p&gt;
&lt;p&gt;这个机制最常见的用途不是防攻击，而是防一个很普通的并发场景：Agent 读了版本 A，在它思考时我手动改成了版本 B，它最后还拿 A 的上下文回来写。&lt;/p&gt;
&lt;p&gt;没有 SHA，很容易把我的 B 一起覆盖掉。&lt;/p&gt;
&lt;p&gt;真正提交写入前还会拿锁、重新解析路径、重新读内容、再校一次 Hash。&lt;/p&gt;
&lt;p&gt;最后用同目录临时文件做原子替换，而不是直接 &lt;code&gt;truncate&lt;/code&gt; 原文件。&lt;/p&gt;
&lt;p&gt;这层我实际遇到过几次救命的情况，所以一直保留得很死。&lt;/p&gt;
&lt;h2 id=&#34;delete-后来加了但故意做得很烦&#34;&gt;Delete 后来加了，但故意做得很烦&lt;a class=&#34;heading-anchor&#34; href=&#34;#delete-%e5%90%8e%e6%9d%a5%e5%8a%a0%e4%ba%86%e4%bd%86%e6%95%85%e6%84%8f%e5%81%9a%e5%be%97%e5%be%88%e7%83%a6&#34; aria-label=&#34;章节链接：Delete 后来加了，但故意做得很烦&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;第一版 wcode 干脆没有 Delete Tool。&lt;/p&gt;
&lt;p&gt;后来实际 Coding Workflow 里确实有删除文件的需要，所以最终还是加了 &lt;code&gt;delete_path&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但它不是普通 Write。&lt;/p&gt;
&lt;p&gt;第一次调用只会产生一个本地 Authorization Request。我要在 TUI 或受保护的 Project Observatory 里批准这个&lt;strong&gt;精确操作&lt;/strong&gt;，Agent 再重试。&lt;/p&gt;
&lt;p&gt;而且授权是 one-shot。&lt;/p&gt;
&lt;p&gt;普通文件删除要求当前 SHA；目录只能删空目录；递归删、Workspace Root、Protected Path、Symlink、Hard-linked File 都不开放。&lt;/p&gt;
&lt;p&gt;这会让“帮我顺便清理十几个目录”没那么丝滑。&lt;/p&gt;
&lt;p&gt;我接受这个麻烦。&lt;/p&gt;
&lt;p&gt;恢复一个误删目录比多确认几次麻烦得多。&lt;/p&gt;
&lt;h2 id=&#34;run_command-还是不经过-shell&#34;&gt;&lt;code&gt;run_command&lt;/code&gt; 还是不经过 Shell&lt;a class=&#34;heading-anchor&#34; href=&#34;#run_command-%e8%bf%98%e6%98%af%e4%b8%8d%e7%bb%8f%e8%bf%87-shell&#34; aria-label=&#34;章节链接：run_command 还是不经过 Shell&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这一条从第一版到现在没变。&lt;/p&gt;
&lt;p&gt;命令执行是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;program + args[]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/bin/sh -c &amp;#34;...&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所以：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;amp;&amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$(...)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这些语法根本没有解释器去理解。&lt;/p&gt;
&lt;p&gt;默认预授权 Command Policy 仍然很窄，主要是受约束的 Git / ripgrep，以及 Harness 能确定形状的工程检查。&lt;/p&gt;
&lt;p&gt;但现在“默认没授权”不等于“永远不能授权”。模型如果请求一个合法的裸可执行程序名，例如 &lt;code&gt;hugo&lt;/code&gt;、&lt;code&gt;flutter&lt;/code&gt;、&lt;code&gt;deno&lt;/code&gt;、&lt;code&gt;mvn&lt;/code&gt;，第一次会生成当前 Workspace 的 &lt;code&gt;CommandAccess&lt;/code&gt; Pending Request。TUI 里可以用 ↑/↓ 选中某一条，&lt;code&gt;Y&lt;/code&gt; 只批准选中的请求、&lt;code&gt;N&lt;/code&gt; 只拒绝选中的请求；Project Observatory 的 Access Panel 也能处理同一批 Pending Request。&lt;/p&gt;
&lt;p&gt;批准后只是把这个 Program 加进当前 Workspace 的运行时授权，不是开放 Shell。&lt;code&gt;bash&lt;/code&gt;、&lt;code&gt;sh&lt;/code&gt;、&lt;code&gt;pwsh&lt;/code&gt;、&lt;code&gt;cmd&lt;/code&gt; 这类 Shell Interpreter 和带路径 Program Name 仍然硬拒绝；Argument 继续经过 Workspace Escape、Protected Path 和具体 Command Policy 检查。&lt;/p&gt;
&lt;p&gt;Git Mutation 会被挡，敏感环境变量会清掉，输出有限制，进程有 Timeout。&lt;/p&gt;
&lt;p&gt;我一直不认同“既然模型已经能写代码，不如直接给它 Terminal”这种推导。&lt;/p&gt;
&lt;p&gt;写代码能力和拿到一个继承了我所有环境的 Shell，不是一个权限级别。&lt;/p&gt;
&lt;h2 id=&#34;高风险执行现在可以按操作授权&#34;&gt;高风险执行现在可以按操作授权&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e9%ab%98%e9%a3%8e%e9%99%a9%e6%89%a7%e8%a1%8c%e7%8e%b0%e5%9c%a8%e5%8f%af%e4%bb%a5%e6%8c%89%e6%93%8d%e4%bd%9c%e6%8e%88%e6%9d%83&#34; aria-label=&#34;章节链接：高风险执行现在可以按操作授权&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这一块和第一版相比变过。&lt;/p&gt;
&lt;p&gt;以前要跑 Language Server、Runtime Executor 这类 repository-aware 操作，基本就是启动时加：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;--allow-risky-exec
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个开关现在还在，含义更像“这个 wcode 进程我整体信任，可以做这类执行”。&lt;/p&gt;
&lt;p&gt;但我不一定每次都想放这么宽。&lt;/p&gt;
&lt;p&gt;现在具体的 Risky Execution 也可以先触发本地 Authorization Request。我在 TUI 或受保护 WebUI 里批准当前 Session 的精确 operation，再让 Agent 重试。&lt;/p&gt;
&lt;p&gt;例如我只想允许一次 Semantic Refresh，就不必顺便授权后面所有仓库执行。&lt;/p&gt;
&lt;p&gt;这和 &lt;code&gt;CommandAccess&lt;/code&gt; 是两层：前者决定“这个 Program 能不能在这个 Workspace 出现”，后者继续决定“当前这组 repository-aware 参数是否值得信任”。&lt;/p&gt;
&lt;p&gt;这不是 OS Sandbox。&lt;/p&gt;
&lt;p&gt;只是把“你信不信这个仓库里的代码会被执行”从一个粗开关拆得更细一点。&lt;/p&gt;
&lt;h2 id=&#34;并发写入也不是谁先抢到锁谁赢&#34;&gt;并发写入也不是谁先抢到锁谁赢&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e5%b9%b6%e5%8f%91%e5%86%99%e5%85%a5%e4%b9%9f%e4%b8%8d%e6%98%af%e8%b0%81%e5%85%88%e6%8a%a2%e5%88%b0%e9%94%81%e8%b0%81%e8%b5%a2&#34; aria-label=&#34;章节链接：并发写入也不是谁先抢到锁谁赢&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;现在 &lt;code&gt;parallel_tools&lt;/code&gt; 能处理一部分独立 Workspace Write，这又多了一个安全问题：两个任务到底能不能同时跑。&lt;/p&gt;
&lt;p&gt;Scheduler 会先建立 Path Resource Model。&lt;/p&gt;
&lt;p&gt;如果两个操作碰同一个文件，或者 Move / Delete / Create 之间有父子路径依赖，就排序，不硬并发。&lt;/p&gt;
&lt;p&gt;同文件 &lt;code&gt;apply_edits&lt;/code&gt; 只有 SHA 一样、Edit Range 不冲突时才会合并成一次原子提交。&lt;/p&gt;
&lt;p&gt;我不想让“支持并发”最后变成“把 race condition 交给文件锁处理”。&lt;/p&gt;
&lt;p&gt;能不能并发应该在执行前就尽量说清楚。&lt;/p&gt;
&lt;h2 id=&#34;安全边界不能只写在-prompt-里&#34;&gt;安全边界不能只写在 Prompt 里&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e5%ae%89%e5%85%a8%e8%be%b9%e7%95%8c%e4%b8%8d%e8%83%bd%e5%8f%aa%e5%86%99%e5%9c%a8-prompt-%e9%87%8c&#34; aria-label=&#34;章节链接：安全边界不能只写在 Prompt 里&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我还是会在 Skill 和 Agent Instructions 里写：不要逃出 Workspace，不要绕过授权，不要自己扩大 risky execution。&lt;/p&gt;
&lt;p&gt;但这些只能算工作习惯。&lt;/p&gt;
&lt;p&gt;真正的安全边界必须在 Tool Runtime。&lt;/p&gt;
&lt;p&gt;模型理解错了、Prompt Injection 进来了、路径算错了，最后应该撞在 Workspace Policy / Authorization / Command Policy 上。&lt;/p&gt;
&lt;p&gt;而不是靠一句：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;请严格遵守安全规则。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上层功能越多，我越觉得这一点重要。&lt;/p&gt;
&lt;p&gt;Agent 可以很聪明，也可以犯很离谱的错。底层权限最好对这两种情况都一样冷淡。&lt;/p&gt;
&lt;p&gt;这一组文章从 &lt;a href=&#34;https://francisdu.com/blog/wcode-2026/&#34;&gt;最新版总览&lt;/a&gt; 开始。授权队列、&lt;code&gt;CommandAccess&lt;/code&gt; 和 TUI/WebUI 的交互细节单独写在 &lt;a href=&#34;https://francisdu.com/blog/wcode-authorization/&#34;&gt;授权中心：模型可以提权限，但不能替我批准&lt;/a&gt;。第一版关于 OAuth、Tunnel、MCP 请求链路和早期 Workspace 实现的记录还在 &lt;a href=&#34;https://francisdu.com/blog/wcode/&#34;&gt;这里&lt;/a&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>我写了 wcode：把 Web AI 接到本地代码库</title>
      <link>https://francisdu.com/blog/wcode/</link>
      <pubDate>Sun, 23 Aug 2026 18:30:00 +0800</pubDate>
      <guid>https://francisdu.com/blog/wcode/</guid>
      <description>&lt;p class=&#34;project-logo&#34;&gt;&lt;a href=&#34;https://wcode.francis.run/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34; title=&#34;打开 wcode 官网&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-logo.svg&#34; alt=&#34;wcode 官网&#34; width=&#34;320&#34; height=&#34;96&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p class=&#34;project-links&#34;&gt;&lt;a href=&#34;https://wcode.francis.run/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;官网与文档 ↗&lt;/a&gt;&lt;a href=&#34;https://github.com/francis-du/wcode&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;GitHub ↗&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这篇保留的是 wcode 第一版的实现状态，我不会跟着后续代码逐段回写。现在的版本已经继续做了 Design State、Product Scope、Verification、Evidence、Reconciliation 和 Project Observatory，见 &lt;a href=&#34;https://francisdu.com/blog/wcode-2026/&#34;&gt;wcode 最近做成什么样了&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我平时会同时用几个 AI 的 Web 端。它们的模型和对话体验已经很好了，但一碰到本地项目，事情就变得很别扭：要么手动复制代码，要么换到另一个 Coding Agent，要么再申请 API Key、单独付一份 Token 账单。&lt;/p&gt;
&lt;p&gt;但我并不想再造一个 Agent。&lt;/p&gt;
&lt;p&gt;模型、上下文管理和 Agent Loop，Web 端都已经有了。我缺的只是一根管子，让它能在我允许的范围内读写本地代码。于是有了 &lt;a href=&#34;https://github.com/francis-du/wcode&#34;&gt;wcode&lt;/a&gt;：一个用 Rust 写的 Remote MCP Bridge。&lt;/p&gt;
&lt;p&gt;在项目目录里运行一个二进制，它会启动本地 MCP Server、OAuth、HTTPS Tunnel、配置页和终端监控。然后把生成的 &lt;code&gt;/mcp&lt;/code&gt; 地址加到 Grok、Claude、ChatGPT、Mistral 或其他支持 Remote MCP 的客户端里，就可以直接聊本地代码。&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-tui_hu_13e59e78ee0ce797.webp&#34; alt=&#34;wcode 终端实时面板&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-tui_hu_13e59e78ee0ce797.webp 960w, https://francisdu.com/img/wcode/wcode-tui_hu_6437a17a51f06ca3.webp 1800w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;3420&#34; height=&#34;2146&#34;&gt;&lt;/figure&gt;&lt;p&gt;还有一个很现实的用法：&lt;strong&gt;薅 Web 端。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果一个 Web 产品允许在对话里接入自定义 MCP，那么模型继续用 Web 端提供的能力，代码搜索、读取、修改和验证则由本机的 wcode 完成。这样不需要为了 Code Agent 再配一套模型 API Key，也没有额外的按量 API Token 账单。&lt;/p&gt;
&lt;p&gt;这不是绕过平台限制。模型回复仍然受 Web 端自己的套餐、消息数、Credits、速率或上下文规则约束。准确地说，wcode 省掉的是“为了让 AI 访问本地代码，再买一份 API Token”的成本。&lt;/p&gt;
&lt;h2 id=&#34;wcode-只做桥接&#34;&gt;wcode 只做桥接&lt;a class=&#34;heading-anchor&#34; href=&#34;#wcode-%e5%8f%aa%e5%81%9a%e6%a1%a5%e6%8e%a5&#34; aria-label=&#34;章节链接：wcode 只做桥接&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我一开始就给它定了边界：不负责选模型，不负责实现 Agent Loop，也不保存聊天记录。它只把 MCP 请求转换成受控的本地代码操作。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Web AI / Coding Agent
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          │  Remote MCP + OAuth
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          ▼
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;┌──────────────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│            wcode             │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│                              │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│ Auth ─ MCP ─ Tool Harness    │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│                 │            │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│       Code Index / Workspace │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;└─────────────────┬────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  ▼
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          指定的代码目录
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;网络层用 Axum 和 Tokio，终端界面用 Ratatui，代码索引用 Tree-sitter。最后编译成一个原生二进制，没有数据库，也没有另外一组常驻服务。&lt;/p&gt;
&lt;p&gt;源码里的模块基本就是按照边界拆的：&lt;code&gt;auth.rs&lt;/code&gt; 管 OAuth，&lt;code&gt;mcp.rs&lt;/code&gt; 管协议和工具路由，&lt;code&gt;workspace.rs&lt;/code&gt; 管文件与命令安全，&lt;code&gt;code_index.rs&lt;/code&gt; 管语法索引，&lt;code&gt;harness.rs&lt;/code&gt; 管并发和项目级工作流，&lt;code&gt;monitor.rs&lt;/code&gt; 只记录并展示真实任务。&lt;/p&gt;
&lt;p&gt;启动时三个 Axum Router 会合到一起：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#00f&#34;&gt;let&lt;/span&gt; app = auth::router(auth.clone())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .merge(mcp::router(app_state))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .merge(control_router);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#00f&#34;&gt;let&lt;/span&gt; server_task = tokio::spawn(&lt;span style=&#34;color:#00f&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#00f&#34;&gt;move&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    axum::serve(listener, app).&lt;span style=&#34;color:#00f&#34;&gt;await&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本地 Server 先启动，公网 Tunnel 后启动。这样即使 Cloudflare DNS 或 TLS 还在预热，本地服务和公网问题也不会搅在一起。&lt;/p&gt;
&lt;h2 id=&#34;一次-mcp-请求怎么进来&#34;&gt;一次 MCP 请求怎么进来&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e4%b8%80%e6%ac%a1-mcp-%e8%af%b7%e6%b1%82%e6%80%8e%e4%b9%88%e8%bf%9b%e6%9d%a5&#34; aria-label=&#34;章节链接：一次 MCP 请求怎么进来&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;wcode 的 &lt;code&gt;/mcp&lt;/code&gt; 不是拿到 URL 就能调用。一个请求真正进入工具层之前，要依次过四关：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Origin 是否匹配
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Bearer Token 是否有效，并绑定当前 Resource
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;MCP 协议版本是否支持
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Header、JSON-RPC method、_meta 是否一致
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tools/list 或 tools/call
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MCP 生态还在快速变化，各个客户端升级并不同步。所以 wcode 里保留了两条路径：新协议走无状态 POST，老客户端仍然可以通过 &lt;code&gt;initialize&lt;/code&gt; 握手连接。未知版本会明确报错并返回支持的版本，不会悄悄猜一个版本继续跑。&lt;/p&gt;
&lt;p&gt;如果 HTTP 请求里带了 &lt;code&gt;Origin&lt;/code&gt;，wcode 会检查 Scheme、Host 和有效端口是否与公开 MCP 地址一致，同时拒绝额外的 Path、Query 和 Fragment。这不是普通的 CORS 装饰，主要是为了降低本地 HTTP Transport 被 DNS Rebinding 利用的风险。&lt;/p&gt;
&lt;p&gt;新协议的请求还会交叉检查 &lt;code&gt;Mcp-Method&lt;/code&gt; Header 和 JSON-RPC 的 &lt;code&gt;method&lt;/code&gt;。调用工具时，&lt;code&gt;Mcp-Name&lt;/code&gt; 也必须和 &lt;code&gt;params.name&lt;/code&gt; 对得上。协议边界宁愿多拒绝一次，也不应该模糊地接受两个互相矛盾的路由信息。&lt;/p&gt;
&lt;h2 id=&#34;oauth-为什么放在本地&#34;&gt;OAuth 为什么放在本地&lt;a class=&#34;heading-anchor&#34; href=&#34;#oauth-%e4%b8%ba%e4%bb%80%e4%b9%88%e6%94%be%e5%9c%a8%e6%9c%ac%e5%9c%b0&#34; aria-label=&#34;章节链接：OAuth 为什么放在本地&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Grok、Claude 这类云端产品访问不到 &lt;code&gt;127.0.0.1&lt;/code&gt;，所以 wcode 默认会用 Cloudflare Quick Tunnel 创建临时 HTTPS 地址。但 Tunnel 只解决网络可达性，不负责授权。&lt;/p&gt;
&lt;p&gt;第一次访问 &lt;code&gt;/mcp&lt;/code&gt;，客户端会得到 &lt;code&gt;401&lt;/code&gt; 和 Protected Resource Metadata 地址：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-http&#34; data-lang=&#34;http&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#00f&#34;&gt;HTTP&lt;/span&gt;/1.1 401 Unauthorized
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;WWW-Authenticate: Bearer resource_metadata=&amp;#34;https://…/.well-known/oauth-protected-resource/mcp&amp;#34;, scope=&amp;#34;mcp&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;后面的流程是标准 OAuth 2.1 + PKCE：客户端发现元数据、注册、打开授权页、交换 Authorization Code，最后拿到绑定当前 &lt;code&gt;/mcp&lt;/code&gt; Resource 的 Token。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Client          /mcp          OAuth          Browser
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  │               │             │               │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ├─ POST ───────►│             │               │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  │◄─ 401 + metadata ────────────┤               │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ├──────── register ───────────►│               │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ├──────── authorize + PKCE ───►├─ pairing ────►│
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ├──────── exchange code ──────►│               │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  │◄──────── access token ───────┤               │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  └─ Bearer + tools/call ───────►│               │
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;授权页还要输入终端显示的六位验证码。只拿到临时公网 URL 的人，不能直接替你授权。Authorization Code 是单次、短时的，Refresh Token 会轮换，Redirect URI 只接受 HTTPS 或受约束的 Loopback 地址。&lt;/p&gt;
&lt;p&gt;MCP 新版更推荐 Client ID Metadata Document，但我没有默认抓取客户端随便给出的 Metadata URL。自动化是多了一点，同时也新开了一个出站网络、SSRF 和 DNS Rebinding 的口子。在没有完整处理这个信任边界之前，我更愿意保留 DCR 兼容路径。&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-setup-hub_hu_dd9c03ed1e405294.webp&#34; alt=&#34;wcode Setup Hub&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-setup-hub_hu_dd9c03ed1e405294.webp 960w, https://francisdu.com/img/wcode/wcode-setup-hub_hu_90866dee58816e01.webp 1600w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;1600&#34; height=&#34;1000&#34;&gt;&lt;/figure&gt;&lt;p&gt;授权面板把项目、命令白名单和精确仓库操作的授权放在本地显式管理，模型不能批准自己的请求：&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-access-management_hu_ac447683faaa24eb.webp&#34; alt=&#34;wcode 授权与访问控制&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-access-management_hu_ac447683faaa24eb.webp 960w, https://francisdu.com/img/wcode/wcode-access-management_hu_e5f2de5679dfbc7e.webp 1600w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;1600&#34; height=&#34;1000&#34;&gt;&lt;/figure&gt;&lt;h2 id=&#34;文件沙箱最麻烦的不是-&#34;&gt;文件沙箱最麻烦的不是 &lt;code&gt;../&lt;/code&gt;&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e6%96%87%e4%bb%b6%e6%b2%99%e7%ae%b1%e6%9c%80%e9%ba%bb%e7%83%a6%e7%9a%84%e4%b8%8d%e6%98%af-&#34; aria-label=&#34;章节链接：文件沙箱最麻烦的不是 ../&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;最早写 Workspace 层时，我很快发现：检查一下路径里有没有 &lt;code&gt;..&lt;/code&gt;，离“只能访问这个目录”还差得很远。&lt;/p&gt;
&lt;p&gt;每个工作区在启动时都会先 &lt;code&gt;canonicalize&lt;/code&gt;。文件系统根目录、Home 目录这类范围过大的 Root 默认拒绝；多个工作区如果是父子关系，也默认拒绝。在 Unix 上，wcode 还会记住 Root 的 Device/Inode。&lt;/p&gt;
&lt;p&gt;每次文件操作之前，Root 都要重新解析并核对身份。如果服务启动后，同一路径被换成了另一个目录，即使字符串完全没变，操作也会中止并要求重启。&lt;/p&gt;
&lt;p&gt;模型传进来的路径只能是相对路径，除此之外还会拒绝：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;..&lt;/code&gt;、绝对路径和 Windows Prefix；&lt;/li&gt;
&lt;li&gt;含冒号的 Path Component，避免 Alternate Data Streams；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.git&lt;/code&gt;、&lt;code&gt;.env*&lt;/code&gt;、密钥和常见凭据位置；&lt;/li&gt;
&lt;li&gt;路径中的任意 Symlink Component；&lt;/li&gt;
&lt;li&gt;Unix 上指向多处的 Hard Link 写入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;已有文件和新文件也不能用同一套解析逻辑。已有文件可以 Canonicalize 到最终目标，再确认它仍在 Root 内；新文件的叶子还不存在，只能先解析父目录，然后确认父目录没有逃出去。&lt;/p&gt;
&lt;p&gt;读文件本身也有竞态。wcode 在读取前后各取一次 Metadata，比对长度和修改时间。如果中途变了，这次读取直接失败，让客户端重试。成功返回时会同时带一个 SHA-256，后续编辑必须使用它。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;stat before → read → stat after → SHA-256
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     │                 │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     └── 不相等就重试 ─┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;防止-ai-用旧上下文覆盖新代码&#34;&gt;防止 AI 用旧上下文覆盖新代码&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e9%98%b2%e6%ad%a2-ai-%e7%94%a8%e6%97%a7%e4%b8%8a%e4%b8%8b%e6%96%87%e8%a6%86%e7%9b%96%e6%96%b0%e4%bb%a3%e7%a0%81&#34; aria-label=&#34;章节链接：防止 AI 用旧上下文覆盖新代码&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;假设模型读到了版本 A。在它思考的几秒里，我手动改成了版本 B。模型如果还按 A 去替换文本，很容易把我的改动一起覆盖掉。&lt;/p&gt;
&lt;p&gt;wcode 的写入同时用了三层保护：请求携带读取时的 SHA-256；同一个文件的并发写入使用一把进程内锁；真正拿到锁以后，再重新解析路径、读取文件并检查哈希。&lt;/p&gt;
&lt;p&gt;写入也不是直接 &lt;code&gt;truncate&lt;/code&gt; 原文件。它会在相同目录创建一个 &lt;code&gt;create_new&lt;/code&gt; 临时文件，写完 &lt;code&gt;sync_all&lt;/code&gt;，继承原权限，再原子替换目标。Unix 走同文件系统的 &lt;code&gt;rename&lt;/code&gt;；Windows 使用带 &lt;code&gt;REPLACE_EXISTING&lt;/code&gt; 和 &lt;code&gt;WRITE_THROUGH&lt;/code&gt; 的系统调用。最后再尽量同步父目录。&lt;/p&gt;
&lt;p&gt;创建文件是另一条路径。临时文件会以 Create-new 语义落到目标位置；如果另一个任务恰好先创建了同名文件，这次请求失败，不会覆盖它。&lt;/p&gt;
&lt;p&gt;另外我没有给模型 Delete Tool。大幅缩短现有文件也会被当成破坏性写入，除非用户显式开启 &lt;code&gt;--allow-destructive-writes&lt;/code&gt;。这不保证 AI 永远不犯错，但能把最难恢复的错误挡在默认路径之外。&lt;/p&gt;
&lt;h2 id=&#34;tree-sitter-只承诺语法精度&#34;&gt;Tree-sitter 只承诺语法精度&lt;a class=&#34;heading-anchor&#34; href=&#34;#tree-sitter-%e5%8f%aa%e6%89%bf%e8%af%ba%e8%af%ad%e6%b3%95%e7%b2%be%e5%ba%a6&#34; aria-label=&#34;章节链接：Tree-sitter 只承诺语法精度&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;如果工具只有全文搜索和 &lt;code&gt;read_file&lt;/code&gt;，模型很容易退化成“把整个文件发给我看看”。上下文浪费大，定位也不精确。&lt;/p&gt;
&lt;p&gt;wcode 内置 Tree-sitter，提供三个语法级工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;file_outline&lt;/code&gt;：列出定义、签名和准确范围；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find_symbol&lt;/code&gt;：跨文件查找定义；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;symbol_context&lt;/code&gt;：围绕一个符号返回有限正文、嵌套定义和语法调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;索引是 Lazy 的。只有真正请求某个文件或符号时才解析；目录搜索会先做便宜的文本预过滤，再对候选源码建树。完整 AST Cache 最多保留 128 个文件，目录符号搜索最多扫描 50,000 个源文件。写入成功后，对应的 Symbol Record 和 AST 会立即失效。&lt;/p&gt;
&lt;p&gt;这里我刻意没有把结果包装成“语义理解”。所有模型可见的索引结果都写着：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;#34;provider&amp;#34;: &lt;span style=&#34;color:#a31515&#34;&gt;&amp;#34;tree-sitter&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;#34;precision&amp;#34;: &lt;span style=&#34;color:#a31515&#34;&gt;&amp;#34;syntax&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它能回答哪里定义了函数、语法上出现了哪些调用，但不假装自己做了宏展开、类型推断、重载选择或动态分派。对 Agent 工具来说，明确能力边界比伪造一个很强的答案更重要。&lt;/p&gt;
&lt;h2 id=&#34;并发不是越满越好&#34;&gt;并发不是越满越好&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e5%b9%b6%e5%8f%91%e4%b8%8d%e6%98%af%e8%b6%8a%e6%bb%a1%e8%b6%8a%e5%a5%bd&#34; aria-label=&#34;章节链接：并发不是越满越好&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;wcode 只有一个全局 Tokio &lt;code&gt;Semaphore&lt;/code&gt;，所有真实工具任务都从这里拿 Permit。默认上限是逻辑 CPU 数的八倍，并 Clamp 到 64–128；也可以通过 &lt;code&gt;-j&lt;/code&gt; 调整，内部硬上限是 256。&lt;/p&gt;
&lt;p&gt;一条任务只走这几个状态：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;queued → acquire permit → running → completed / failed
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;parallel_tools&lt;/code&gt;、&lt;code&gt;review_changes&lt;/code&gt; 和 &lt;code&gt;verify_project&lt;/code&gt; 这类组合工具有个容易踩的坑：父任务不能先占一个 Permit，再等子任务。否则当 &lt;code&gt;-j 1&lt;/code&gt; 时，父任务拿走唯一槽位，子任务永远跑不起来。&lt;/p&gt;
&lt;p&gt;所以组合工具本身不占父 Permit，每个真正做事的 Child 自己排队。&lt;code&gt;parallel_tools&lt;/code&gt; 只允许 Read/Discovery 操作，最多 128 个子任务；单个结果最多 512 KiB，总响应最多 8 MiB。&lt;/p&gt;
&lt;p&gt;另一方面，能一次遍历做完的工作没有必要强行并行。&lt;code&gt;search_many&lt;/code&gt; 和 &lt;code&gt;read_files&lt;/code&gt; 会优先做批量操作，减少 MCP Round Trip。互相依赖的编辑保持串行，Cargo Test、Clippy、Build 这类重任务也分阶段执行，避免一起争抢编译缓存。&lt;/p&gt;
&lt;p&gt;TUI 里的 Slots 和 Peak 就来自这些真实 Permit，不是 UI 模拟出来的繁忙程度。&lt;/p&gt;
&lt;h2 id=&#34;我不想给模型一个远程-shell&#34;&gt;我不想给模型一个远程 Shell&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e6%88%91%e4%b8%8d%e6%83%b3%e7%bb%99%e6%a8%a1%e5%9e%8b%e4%b8%80%e4%b8%aa%e8%bf%9c%e7%a8%8b-shell&#34; aria-label=&#34;章节链接：我不想给模型一个远程 Shell&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;run_command&lt;/code&gt; 接收的是 Program 和 Argument Array，从来不经过 Shell。默认允许面很窄：受限制的 Git/ripgrep 只读操作，以及形状完全匹配的 &lt;code&gt;cargo fmt --check&lt;/code&gt;、&lt;code&gt;cargo check&lt;/code&gt;、&lt;code&gt;cargo check --locked&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Git Mutation 始终阻止。子进程的 &lt;code&gt;GIT_*&lt;/code&gt; 状态会被清理，交互式 Prompt、Helper、外部 Diff 以及能改变仓库发现范围的配置也会被拒绝。stdout/stderr 有上限，执行有 Timeout，敏感环境变量不会传进去。&lt;/p&gt;
&lt;p&gt;为什么连 Build 和 Test 也不默认放开？因为 Cargo Build Script、Proc Macro、Makefile、Package Script 和测试代码都由仓库控制。“这是一个测试命令”不代表它安全。&lt;/p&gt;
&lt;p&gt;项目级验证走 &lt;code&gt;verify_project&lt;/code&gt;。Harness 会先识别 Cargo、Go、Node、Flutter、Make 等项目，再根据 Manifest 和仓库规则推导检查命令。只有通过 Exact-shape Validation 的命令，才会临时进入验证通道。便宜的检查可以重叠，编译重任务按阶段跑，返回给模型的诊断只保留有界尾部。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--allow-risky-exec&lt;/code&gt; 可以放宽命令策略，但它是一次明确的信任扩张，不是 OS Sandbox。我不希望为了让 Demo 看起来“什么都能跑”，把这个 Flag 默认打开。&lt;/p&gt;
&lt;h2 id=&#34;harness-不是一句-prompt&#34;&gt;Harness 不是一句 Prompt&lt;a class=&#34;heading-anchor&#34; href=&#34;#harness-%e4%b8%8d%e6%98%af%e4%b8%80%e5%8f%a5-prompt&#34; aria-label=&#34;章节链接：Harness 不是一句 Prompt&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这里单独说一下 Harness，因为它不是在 System Prompt 里写一句“改完记得跑测试”。它是一层有状态、可执行、带边界的工程工作流。&lt;/p&gt;
&lt;p&gt;模型第一次进入仓库，应该先调用 &lt;code&gt;project_context&lt;/code&gt;。Harness 会查看根目录里的 Manifest 和 Lockfile，识别 Rust、Node、Python、Go、Make 等项目类型；同时按固定优先级读取 &lt;code&gt;AGENTS.md&lt;/code&gt;、&lt;code&gt;CLAUDE.md&lt;/code&gt;、&lt;code&gt;README.md&lt;/code&gt; 一类仓库说明。每个文件有行数和字符上限，所有说明还有总字符预算，内容仍然经过 Workspace 的敏感信息脱敏。&lt;/p&gt;
&lt;p&gt;识别结果会生成一个 &lt;code&gt;ProjectProfile&lt;/code&gt;，里面包括项目类型、Manifest、仓库规则、推荐检查和默认工作流。例如发现 &lt;code&gt;Cargo.toml&lt;/code&gt; 时，会推导：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;quick: git diff --check
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;quick: cargo fmt --check
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;quick: cargo check --locked
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;full:  cargo test --locked
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;full:  cargo clippy --locked -- -D warnings
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;full:  cargo build --release --locked
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果存在 &lt;code&gt;package.json&lt;/code&gt;，Harness 只读取实际存在的 &lt;code&gt;lint&lt;/code&gt;、&lt;code&gt;typecheck&lt;/code&gt;、&lt;code&gt;check&lt;/code&gt;、&lt;code&gt;format:check&lt;/code&gt;、&lt;code&gt;test&lt;/code&gt;、&lt;code&gt;build&lt;/code&gt; Script，并根据 Lockfile 选择 npm、pnpm、yarn 或 bun。Makefile 也只识别明确存在的 &lt;code&gt;check&lt;/code&gt;、&lt;code&gt;lint&lt;/code&gt;、&lt;code&gt;test&lt;/code&gt; Target，不凭空猜命令。&lt;/p&gt;
&lt;p&gt;Project Profile 会缓存，但不是永远不变。Fingerprint 来自 Manifest 和 Guidance 的 Metadata；这些文件变化后，下次请求会重新构建。构建发生在 Cache Lock 外面，避免一个大仓库的上下文发现阻塞其他 Workspace。写回缓存前再检查一次，解决并发请求重复构建时的竞态。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;review_changes&lt;/code&gt; 也没有直接返回整份 Git Diff。它并行跑五个只读 Probe：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git status
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;unstaged numstat     staged numstat
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;unstaged diff-check  staged diff-check
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Harness 把结果合并成文件列表、增删行数和风险分类。改了源码却没改测试、动了认证或 Token 文件、改了 Manifest、删了测试、碰了 Migration/Workflow，都会产生对应 Finding。变更超过 25 个文件或约 1,000 行会提示拆分；风险再高一些时，会直接推荐 Full Verification。这里最多分析 500 个文件、保留 64 条 Finding，避免一个巨大 Working Tree 把上下文打爆。&lt;/p&gt;
&lt;p&gt;这些分析结果在 WebUI 里可以直接看到：当前变更、每个文件命中的 Requirement、代码统计和风险等级：&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-workspace-intelligence_hu_24fb32e29a6a581a.webp&#34; alt=&#34;wcode 工作区智能视图&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-workspace-intelligence_hu_24fb32e29a6a581a.webp 960w, https://francisdu.com/img/wcode/wcode-workspace-intelligence_hu_6e8fd5f6bd6bada.webp 1600w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;1600&#34; height=&#34;1000&#34;&gt;&lt;/figure&gt;&lt;p&gt;&lt;code&gt;verify_project&lt;/code&gt; 接受 &lt;code&gt;quick&lt;/code&gt; 或 &lt;code&gt;full&lt;/code&gt;。它先把推导出的检查排序，再按 Phase 执行：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Phase 0  format / static check / diff check
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ↓ barrier
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Phase 1  tests
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ↓ barrier
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Phase 2  clippy
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ↓ barrier
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Phase 3  release build
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;同一个 Phase 内彼此独立的检查可以并发，不同 Phase 之间有 Barrier。这样既不把所有命令串行到底，也不会让 Test、Clippy 和 Release Build 同时争 Cargo Cache。每个检查仍然单独获取全局 Semaphore Permit，单独进入 Monitor，并返回 Exit Code、耗时与截断后的 stdout/stderr 尾部。&lt;/p&gt;
&lt;p&gt;最关键的是，&lt;code&gt;verify_project&lt;/code&gt; 不能借“自动验证”绕过命令策略。推导出的 Program 和 Args 还要经过内部 Exact-shape Validator，只临时放行这一条已经识别的命令，再委托给同一个 Workspace Command Policy。Harness 提供的是一条更容易走对的路，不是第二个后门。&lt;/p&gt;
&lt;p&gt;需求详情页把 Desired State → Actual State → Change → Proof → Convergence 排成一条链，验证证据挂在 Proof 一环：&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-verification-detail_hu_11b6c44307e59389.webp&#34; alt=&#34;wcode 需求验证证据&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-verification-detail_hu_11b6c44307e59389.webp 960w, https://francisdu.com/img/wcode/wcode-verification-detail_hu_a6a04b35e0461913.webp 1600w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;1600&#34; height=&#34;1000&#34;&gt;&lt;/figure&gt;&lt;h2 id=&#34;tunnel-挂了以后怎么办&#34;&gt;Tunnel 挂了以后怎么办&lt;a class=&#34;heading-anchor&#34; href=&#34;#tunnel-%e6%8c%82%e4%ba%86%e4%bb%a5%e5%90%8e%e6%80%8e%e4%b9%88%e5%8a%9e&#34; aria-label=&#34;章节链接：Tunnel 挂了以后怎么办&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;每个 wcode 进程都有一个随机 &lt;code&gt;instance_id&lt;/code&gt;。&lt;code&gt;cloudflared&lt;/code&gt; 输出公网 URL 后，wcode 不会立刻显示 Ready，而是从公网请求 &lt;code&gt;/healthz&lt;/code&gt;。只有响应里的 &lt;code&gt;ok&lt;/code&gt; 为真，并且 &lt;code&gt;instance_id&lt;/code&gt; 与当前进程一致，才会打开 Setup Hub。&lt;/p&gt;
&lt;p&gt;这个检查主要防两个问题：Tunnel 的 DNS/TLS 还没准备好；或者同一台机器上的另一个 wcode 实例恰好能响应，造成假就绪。&lt;/p&gt;
&lt;p&gt;运行期间每 25 秒检查一次公网状态。连续失败三次，或者发现自己启动的 &lt;code&gt;cloudflared&lt;/code&gt; 子进程退出，就把整个 Runtime 当成恢复边界：先恢复终端，再停健康任务和本地 Server，Kill 并 Wait 子进程，最后用原参数重新启动。&lt;/p&gt;
&lt;p&gt;Quick Tunnel 重启后 URL 可能会变，所以 wcode 不会假装旧 OAuth 状态还能继续用。需要稳定地址时，应该配置自己的 Reverse Proxy，再通过 &lt;code&gt;--public-url&lt;/code&gt; 交给 wcode。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wcode restart&lt;/code&gt; 和 &lt;code&gt;wcode stop&lt;/code&gt; 复用了同一条 Graceful Shutdown 路径。控制接口有一枚 256-bit 本地随机 Token，保存在权限为 &lt;code&gt;0600&lt;/code&gt; 的 Runtime File 中，并做 Constant-time Compare。因为这条 Route 也可能被公网反向代理带出去，所以“它只在本机用”不能成为免认证的理由。&lt;/p&gt;
&lt;h2 id=&#34;tui-展示的都是真实状态&#34;&gt;TUI 展示的都是真实状态&lt;a class=&#34;heading-anchor&#34; href=&#34;#tui-%e5%b1%95%e7%a4%ba%e7%9a%84%e9%83%bd%e6%98%af%e7%9c%9f%e5%ae%9e%e7%8a%b6%e6%80%81&#34; aria-label=&#34;章节链接：TUI 展示的都是真实状态&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ratatui 面板里有本地服务、公网 Tunnel、OAuth、MCP 最近活动、工作区任务、队列、Slots、Peak 和吞吐量。&lt;/p&gt;
&lt;p&gt;这些数据来自同一个 &lt;code&gt;TaskTicket&lt;/code&gt; 生命周期：收到请求时入队，拿到 Permit 时运行，返回时完成或失败。如果异步任务异常 Drop，Ticket 也会结束记录，不会在面板上永久留下一个 Running。&lt;/p&gt;
&lt;p&gt;交互式终端使用 Alternate Screen 和 Raw Mode，并通过 RAII Guard 恢复鼠标捕获、光标和主屏幕。忙时大约 150 ms 刷新一次，闲时降到 500 ms。stdout 不是 TTY 或传入 &lt;code&gt;--no-monitor&lt;/code&gt; 时，就退化成普通日志。&lt;/p&gt;
&lt;p&gt;我不喜欢一些 Agent 产品为了显得很忙，凭空画出一堆并行任务。wcode 的原则很简单：没有发生的事，不显示。&lt;/p&gt;
&lt;p&gt;WebUI 侧遵循同一个原则。Project Observatory 把期望架构、实际依赖、漂移、证据与实现覆盖率放在同一个视图里：&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-architecture_hu_92a8fd07f12ae64a.webp&#34; alt=&#34;wcode 架构总览&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-architecture_hu_92a8fd07f12ae64a.webp 960w, https://francisdu.com/img/wcode/wcode-architecture_hu_3f3f08e508cea449.webp 1800w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;3232&#34; height=&#34;1932&#34;&gt;&lt;/figure&gt;&lt;p&gt;完整页面从架构总览、需求详情、当前变更、代码统计到图快照历史，一图到底：&lt;/p&gt;
&lt;figure class=&#34;content-image&#34;&gt;&lt;img src=&#34;https://francisdu.com/img/wcode/wcode-observatory-full_hu_2e0fc54b6ad597b6.webp&#34; alt=&#34;wcode Project Observatory 整页&#34;loading=&#34;lazy&#34; decoding=&#34;async&#34; srcset=&#34;https://francisdu.com/img/wcode/wcode-observatory-full_hu_2e0fc54b6ad597b6.webp 960w, https://francisdu.com/img/wcode/wcode-observatory-full_hu_77a4ca195d4c7462.webp 1600w&#34; sizes=&#34;(max-width: 900px) calc(100vw - 32px), 900px&#34; width=&#34;1600&#34; height=&#34;5017&#34;&gt;&lt;/figure&gt;&lt;h2 id=&#34;怎么用&#34;&gt;怎么用&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e6%80%8e%e4%b9%88%e7%94%a8&#34; aria-label=&#34;章节链接：怎么用&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;macOS 和 Linux：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;curl -fsSL https://raw.githubusercontent.com/francis-du/wcode/main/install.sh | sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Windows 有 PowerShell 安装脚本，也可以在源码目录直接构建：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cargo install --path .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;进入项目目录后运行：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;wcode --workspace &lt;span style=&#34;color:#a31515&#34;&gt;&amp;#34;&lt;/span&gt;$PWD&lt;span style=&#34;color:#a31515&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;要同时开放多个仓库，就重复 &lt;code&gt;--workspace&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;wcode &lt;span style=&#34;color:#a31515&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --workspace ~/Code/backend &lt;span style=&#34;color:#a31515&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --workspace ~/Code/frontend
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;启动后，在 AI 客户端中添加终端显示的 &lt;code&gt;https://…/mcp&lt;/code&gt;，选择 OAuth，再在授权页输入六位配对码。&lt;/p&gt;
&lt;p&gt;只想读代码可以加 &lt;code&gt;--read-only&lt;/code&gt;；不允许执行命令可以加 &lt;code&gt;--no-exec&lt;/code&gt;；已经有固定反向代理，就使用 &lt;code&gt;--public-url https://your-domain.example&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&#34;还没解决的东西&#34;&gt;还没解决的东西&lt;a class=&#34;heading-anchor&#34; href=&#34;#%e8%bf%98%e6%b2%a1%e8%a7%a3%e5%86%b3%e7%9a%84%e4%b8%9c%e8%a5%bf&#34; aria-label=&#34;章节链接：还没解决的东西&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;wcode 现在已经能稳定完成我最初想做的事，但它当然不是终点。&lt;/p&gt;
&lt;p&gt;Quick Tunnel 适合零配置，不适合需要固定地址的长期部署；Tree-sitter 是跨语言的语法索引，不是 Language Server；不同 Web 产品对 Remote MCP 和 OAuth Discovery 的支持也一直在变化。即使一个客户端声称支持 Streamable HTTP，也不代表它已经能完整跑通 OAuth。&lt;/p&gt;
&lt;p&gt;接下来我会继续补客户端实测、协议兼容、代码索引精度、诊断和安全回归。至于模型和 Agent Loop，我还是不打算做。让 wcode 保持一座小而清楚的桥，比长成另一套平台更有价值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/francis-du/wcode&#34;&gt;GitHub：francis-du/wcode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://wcode.francis.run/&#34;&gt;wcode 产品页、兼容矩阵与文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码留在本地，AI 客户端自己选。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>