Zagens 深度实战:当桌面 Harness 遇上 LHT 完成门禁——从 CodeWhale 血统到 Windows 原生沙箱、从 CRAFT 多角色编排到 kernel-v2 重写的生产级完全指南(2026)
前言:当 AI Agent 不再说「我做完了」
用过 AI 编程助手的人,大概都经历过这一幕:模型信誓旦旦地说「已完成所有修改」,结果一跑测试,编译报错;再问它,它说「我修复了」,再跑,还是报错。来回三四轮,token 烧了一大半,任务还在原地打转。
这不是模型不够聪明——这是 任务验证机制的缺失。大多数 AI 编程工具的核心循环是:LLM 说做完了 → 交给用户 → 用户自己去验证。模型没有义务,也没被强制去做真实验证。
Zagens 解决的就是这个问题。它从 CodeWhale(deepseek-tui)血统出发,做了一个以本地执行为核心的桌面控制台——不是又一个聊天窗口,而是一套完整的 Harness:模型说了不算,门禁过了才算;沙箱在外面锁着,Office 文档和代码同一条流水线产出。
本文基于 Zagens v0.7.4/v0.7.5(unreleased),2026 年 6 月实测,覆盖:血统溯源、架构拆解、LHT 三层门禁机制、Windows 原生沙箱实现、CRAFT 多角色编排、kernel-v2 内核重写,以及和 CodeWhale/Claude Code 等主流方案的取舍对比。
一、血统溯源:deepseek-tui → CodeWhale → Zagens
理解 Zagens,要从它的 runtime 血统说起。这不是凭空冒出来的项目,而是一条持续演进的 agent runtime 血脉。
1.1 deepseek-tui 的诞生与退场
2024 年,deepseek-tui 以「让 DeepSeek 模型跑在终端里」为初衷出现,定位是轻量级 AI 编程 TUI 工具。在那个 Agent 概念还没有今天这么火的年代,它的卖点是:轻、快、不绑 IDE,用纯 Rust 写文件、跑命令、管 Git。
它迅速积累了社区关注,但也很快触到了单进程 TUI 的天花板:没有沙箱隔离、没有多代理编排、没有长程任务的持久化验证机制。
1.2 CodeWhale:upstream 的进化
deepseek-tui 品牌升级为 CodeWhale(codewhale 命令,v0.8.x),社区超过 30k stars,功能列表大幅扩展:
MCP 工具协议
持久化 sub-agent(agent_open / agent_eval)
SQLite 会话持久化与 thread 回放
macOS Seatbelt OS 级沙箱(已强制)
hooks 生命周期钩子
RLM 会话管理
codewhale serve --http(headless HTTP API)
CodeWhale 仍然以 TUI 为核心——纯终端、全屏、键盘操作。它是目前 Rust 生态里功能最完整的开源终端 Agent runtime 之一。
1.3 Zagens:分叉进化的 desktop harness
Zagens 从 CodeWhale v0.8.15 的 runtime 血统分叉出来,走的是桌面 Harness 路线。不是简单的换皮,而是架构层面的重新定位:
| 维度 | CodeWhale(upstream) | Zagens(当前) |
|---|---|---|
| 交互形态 | TUI 单进程为主 + HTTP/SSE API | 桌面应用(Tauri 2)为主 + 可选 zagens-tui |
| 进程模型 | TUI 单进程;或独立 serve --http | 桌面双进程:Tauri 壳 + zagens-runtime sidecar |
| 任务验证 | checklist/plan + durable task gates | LHT 三层完成门禁(自检→硬验收→Rust 对账) |
| 多代理 | 持久化 sub-agent + 并发子代理 | CRAFT 多角色 + 结构化裁决 + fix-loop |
| 文档产出 | 无 | write_office:XLSX(Rust)+ DOCX/PPTX/PDF(Python) |
| 沙箱隔离 | macOS Seatbelt 强制;Linux Landlock 规划中 | Windows 完整 OS 级沙箱 + macOS Seatbelt + Linux bwrap |
| Office 支持 | 无 | 原生 xlsx/docx/pptx/pdf 生成 |
Zagens 的 product bet 是:把 Harness 做进桌面原生,而不只是提供一个带 GUI 的聊天窗口。
二、核心架构:三端一引擎
2.1 架构全景
┌─────────────────────────────────────────────────────┐
│ Zagens (Tauri 2) │
│ WebView UI (React/TypeScript) │
│ ┌──────────────────┐ HTTP + SSE ┌────────────┐ │
│ │ Tauri Shell │◄────────────►│ sidecar │ │
│ │ (Rust 外壳) │ │ zagens-rt │ │
│ └──────────────────┘ │ (loopback) │ │
│ └─────┬──────┘ │
│ │ │
│ ┌─────────────────┴────┐ │
│ │ Runtime Core Crates│ │
│ │ agent|core|config │ │
│ │ tools|mcp|skills │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────┘
三个入口共用同一套引擎:
| 入口 | 形态 | 典型场景 |
|---|---|---|
| Zagens 桌面 | Tauri 2 + WebView | Windows 桌面用户;文件树、diff、多面板、托盘通知 |
| zagens-tui | ratatui 全屏终端 | SSH 远程;习惯纯键盘操作的终端党 |
| zagens CLI | exec / serve --http | CI、脚本、headless 自动化 |
2.2 Sidecar 架构:安全边界的第一刀
这是 Zagens 区别于大多数「桌面 AI 应用」最关键的设计:
- Agent 引擎跑在独立的子进程(
zagens-runtime,TauriexternalBin) - UI 只通过 loopback HTTP/SSE 与 sidecar 通信
- WebView 崩溃 → sidecar 继续跑
- sidecar 重启 → UI 壳不受影响
- 执行 Token 从不出 WebView——这是安全边界,不是免责声明
// Tauri 配置:sidecar 独立二进制
// src-tauri/tauri.conf.json
{
"bundle": {
"externalBin": [
"path/to/zagens-runtime"
]
}
}
// WebView 侧:只持有 loopback HTTP 端点,不接触 Token
// ui/src/lib/sidecar.ts
const SIDECAR_URL = 'http://127.0.0.1:7878';
export async function sendToSidecar(endpoint: string, body: object) {
const resp = await fetch(`${SIDECAR_URL}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
// 注意:不传 Authorization header——Token 在 sidecar 进程内
});
return resp.json();
}
Token 的流向:API Key 存在 sidecar 进程的内存或 OS keyring 里,WebView UI 永远接触不到。这意味着即使用了一个有 XSS 漏洞的 WebView 界面,攻击者也拿不到 API Key。
2.3 15 个 workspace crates 的模块边界
Zagens 源码不是一个大单仓库,而是一个 Rust monorepo,15 个 workspace crates,模块边界清晰:
| Crate | 职责 |
|---|---|
zagens-cli | CLI 入口、serve HTTP、exec 模式 |
zagens-core | LLM 调用抽象、prompt 管理 |
zagens-agent | Agent loop、tool 调用、状态机 |
zagens-tools | 内置工具集(edit_file、run_shell、git 等) |
zagens-mcp | MCP 协议客户端 |
zagens-skills | Skill 加载与执行框架 |
zagens-config | 配置文件解析、keyring 集成 |
zagens-sandbox | OS 级沙箱抽象(Windows/macOS/Linux) |
zagens-office | XLSX(Rust)+ DOCX/PPTX/PDF(Python)生成 |
zagens-lht | LHT 完成门禁核心逻辑 |
zagens-craft | CRAFT 多角色编排协议 |
zagens-session | SQLite 会话持久化 |
zagens-desktop | Tauri shell、WebView 通信 |
zagens-tui | ratatui 全屏终端界面 |
每个 crate 都有独立的 Cargo.toml 和单元测试,集成测试跨 crate 边界。这种模块化设计使得沙箱、Office、Skill 等能力可以在不同入口(Tauri 桌面 / ratatui TUI / CLI)之间共享。
三、LHT 完成门禁:模型说了不算,门禁过了才算
3.1 为什么需要 LHT
传统 AI 编程工具的任务完成判断是主观的:模型输出一段文字说「已完成」,系统就把任务标记为完成。用户得到的是一个承诺,不是事实。
Long-Horizon Task(LHT)是 Zagens 实现的一套客观完成门禁系统。它的核心思想是:任务的完成不是模型说了算,是门禁说了算。
3.2 三层验证架构
Layer 1: 模型自检 (Self-Check)
├── 模型自己写 checklist(plan + subtask breakdown)
├── 每个子任务完成后打勾
└── 模型判断:「我认为完成了」→ 进入 Layer 2
Layer 2: 硬验收命令 (Hard Gate)
├── 运行有退出码的工具命令
├── cargo check / cargo test / npm run build
├── clippy / eslint / ruff
└── 退出码 0 → 进入 Layer 3;非 0 → 返回 Layer 1 修复
Layer 3: 交付物对账 (Deliverable Reconciliation)
├── 纯 Rust 模块做机械验证(不经过 LLM)
├── 检查目标文件是否存在
├── 检查关键代码模式是否出现
├── diff 对比是否包含预期变更
└── 验证通过 → 任务完成;失败 → 返回 Layer 1
三层分离的意义:
- Layer 1 是模型的主观判断,提供上下文
- Layer 2 是客观的编译/测试执行,有退出码作为硬事实
- Layer 3 是交付物的机械验证,排除「编译通过但逻辑不对」的情况
3.3 代码实现
Layer 2 的硬验收命令是 LHT 最有技术含量的部分。Zagens 在 zagens-lht crate 中实现了一个 verifier 框架:
// zagens-lht/src/verifier.rs
use std::process::Command;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HardGate {
/// 运行 shell 命令,验证退出码
Command {
cmd: String,
args: Vec<String>,
expected_code: i32, // 通常是 0
label: String, // 用于报告的标签,如 "cargo check"
},
/// 检查文件是否存在
FileExists { path: String },
/// 检查文件内容包含指定模式
FileContains { path: String, pattern: String },
/// 检查 diff 中包含预期变更
DiffContains { path: String, expected_hunk: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LHTContext {
pub task_id: String,
pub checklist: Vec<ChecklistItem>,
pub hard_gates: Vec<HardGate>,
pub deliverable_spec: DeliverableSpec,
}
impl LHTContext {
/// 执行 Layer 2:运行所有硬验收命令
pub fn run_hard_gates(&self) -> Vec<GateResult> {
self.hard_gates
.iter()
.map(|gate| self.run_gate(gate))
.collect()
}
fn run_gate(&self, gate: &HardGate) -> GateResult {
match gate {
HardGate::Command { cmd, args, expected_code, label } => {
let output = Command::new(cmd)
.args(args)
.output();
match output {
Ok(out) if out.status.code() == Some(*expected_code) => {
GateResult::Pass { label: label.clone(), details: None }
}
Ok(out) => {
GateResult::Fail {
label: label.clone(),
reason: format!(
"exit code {} (expected {}): {}",
out.status.code().unwrap_or(-1),
expected_code,
String::from_utf8_lossy(&out.stderr)
),
}
}
Err(e) => GateResult::Fail {
label: label.clone(),
reason: format!("command failed: {}", e),
},
}
}
HardGate::FileExists { path } => {
if std::path::Path::new(path).exists() {
GateResult::Pass { label: path.clone(), details: None }
} else {
GateResult::Fail {
label: path.clone(),
reason: format!("file does not exist: {}", path),
}
}
}
HardGate::FileContains { path, pattern } => {
match std::fs::read_to_string(path) {
Ok(content) if content.contains(pattern) => {
GateResult::Pass { label: path.clone(), details: None }
}
Ok(_) => GateResult::Fail {
label: path.clone(),
reason: format!("pattern not found in {}: {}", path, pattern),
},
Err(e) => GateResult::Fail {
label: path.clone(),
reason: format!("cannot read {}: {}", path, e),
},
}
}
HardGate::DiffContains { path, expected_hunk } => {
// 获取文件的 staged diff,与预期变更对比
let diff = get_staged_diff(path);
if diff.contains(expected_hunk) {
GateResult::Pass { label: path.clone(), details: None }
} else {
GateResult::Fail {
label: path.clone(),
reason: format!("diff for {} does not contain expected change", path),
}
}
}
}
}
/// 执行 Layer 3:交付物对账(纯机械验证)
pub fn reconcile_deliverables(&self) -> ReconcileResult {
let spec = &self.deliverable_spec;
let mut passed = Vec::new();
let mut failed = Vec::new();
// 机械验证:文件存在性
for path in &spec.must_exist {
if std::path::Path::new(path).exists() {
passed.push(format!("exists: {}", path));
} else {
failed.push(format!("MISSING: {}", path));
}
}
// 机械验证:文件行数/规模
if let Some(max_lines) = spec.max_lines {
for path in &spec.must_exist {
if let Ok(content) = std::fs::read_to_string(path) {
let line_count = content.lines().count();
if line_count > max_lines {
failed.push(format!(
"LINE_COUNT_EXCEEDED {}: {} lines (max {})",
path, line_count, max_lines
));
}
}
}
}
if failed.is_empty() {
ReconcileResult::AllPassed(passed)
} else {
ReconcileResult::HasFailures { passed, failed }
}
}
}
#[derive(Debug, Clone)]
pub enum GateResult {
Pass { label: String, details: Option<String> },
Fail { label: String, reason: String },
Pending { label: String }, // 命令正在执行
}
#[derive(Debug, Clone)]
pub enum ReconcileResult {
AllPassed(Vec<String>),
HasFailures { passed: Vec<String>, failed: Vec<String> },
}
3.4 Fix-Loop:连续失败怎么办
如果三层门禁全部失败,Zagens 不会让模型无限重试。它实现了 Fix-Loop 机制:
Layer 1 (自检) → Layer 2 (硬验收) → Layer 3 (交付物对账)
↑ │
└──────── 失败 → Layer 1 修复 ←───────────┘
连续 3 轮无有效进展(GateResult 无改善)→ 强制停手
这里的「有效进展」定义是:至少有一个 GateResult 从 Fail 变为 Pass,或者 Layer 3 的 failed 列表数量减少。连续三次修复都未能改善任何一个门禁状态,系统会终止任务并向用户报告当前状态。
3.5 实测:unwrap 替换任务
用 Zagens 实测一个常见代码重构任务:
任务:将 Rust 项目中所有 unwrap() 替换为有意义的错误处理。
传统 Agent 的做法(大多数工具):
- 模型说「我找到了所有 unwrap」
- 模型说「我已全部替换」
- 用户自己跑
cargo check→ 编译失败 - 循环...
Zagens 的做法:
Step 1: Layer 1 - 模型自检
- 扫描代码库,找到所有 .rs 文件
- 列出 47 个 unwrap() 调用位置
- 制定替换策略:Result::? / unwrap_or_default / if let Some(x) = ...
- 写 checklist,逐一打勾
Step 2: Layer 2 - 硬验收
- 执行: cargo check
- 退出码 0 → 通过
- 执行: cargo test
- 退出码 0 → 通过
Step 3: Layer 3 - 交付物对账
- 检查所有标记文件是否已被修改
- grep -r "unwrap()" $workspace → 剩余 3 个(有意保留的测试代码)
- 确认剩余的 unwrap() 都在测试文件或已知 safe 位置
整个流程无需用户介入,模型自动在门禁失败时返回修复,直到全部通过或 Fix-Loop 触发强制停手。
四、Windows 原生沙箱:系统强制,不是免责声明
4.1 为什么桌面 Agent 需要 OS 级沙箱
大多数 AI 编程助手运行在「信任模型」模式下:模型说「我要删掉这个文件」,系统就执行。这在云端 SaaS 工具里是被接受的(因为在服务端运行),但对于桌面应用,这是灾难级的安全漏洞:
- 模型被提示词注入诱导删除用户目录
- 恶意指令通过对话历史污染模型
- MCP 工具被劫持执行未授权操作
Zagens 的沙箱策略是:OS 级强制,不是语言级约束。
4.2 Windows 三层隔离架构
Zagens 在 Windows 上实现了三层隔离:
┌────────────────────────────────────────────────┐
│ Zagens Desktop App │
├────────────────────────────────────────────────┤
│ Layer 1: Process Token(受限令牌进程隔离) │
│ - 移除 SeChangeNotifyPrivilege 之外的所有令牌 │
│ - 无法提升为管理员权限 │
│ - 无法访问SYSTEM进程资源 │
├────────────────────────────────────────────────┤
│ Layer 2: ACL + Working Directory(工作区写隔离) │
│ - 仅允许在指定工作区内创建/修改文件 │
│ - 工作区外所有写操作 → 被文件系统 ACL 拦截 │
│ - 配置文件只读隔离 │
├────────────────────────────────────────────────┤
│ Layer 3: WFP(Windows Filtering Platform 出站阻断)│
│ - 工作区外网络请求 → 被 WFP 内核级过滤 │
│ - 模型无法向外部服务泄露数据 │
│ - 例外:配置的 API 端点(DeepSeek 等) │
└────────────────────────────────────────────────┘
// zagens-sandbox/src/windows/sandbox.rs
use windows::Win32::Security::{
CreateRestrictedToken, TOKEN_DENY, TOKEN_ALLOW,
LUA_TOKEN_FLAGS, SECURITY_CAPABILITY_APPDESKTOP,
};
use windows::Win32::Foundation::HANDLE;
use std::path::Path;
/// Windows elevated 沙箱:创建受限令牌进程
pub fn create_sandboxed_process(
exe_path: &Path,
workspace: &Path,
) -> Result<SandboxedProcess, SandboxError> {
// 1. 获取当前进程令牌
let current_token = get_current_process_token()?;
// 2. 创建受限令牌:禁用所有管理员权限
let restricted_token = unsafe {
CreateRestrictedToken(
current_token,
TOKEN_DENY | TOKEN_ALLOW,
0, // 无禁用 SID
&[], // 无限制性 SID
&[], // 无权限删除
&LUA_TOKEN_FLAGS::default(),
)?
};
// 3. 设置工作区 ACL:只允许在 workspace 内读写
set_workspace_acl(workspace)?;
// 4. 用受限令牌启动子进程
let child = spawn_as_restricted(restricted_token, exe_path)?;
// 5. 创建 WFP 规则:仅允许工作区内文件操作
setup_wfp_filter(workspace)?;
Ok(SandboxedProcess::new(child, workspace))
}
4.3 unelevated vs elevated 双档
Zagens 提供了两个沙箱档位,满足不同安全需求:
unelevated 档(默认推荐):
- 模型进程以当前用户身份运行,但写操作被 ACL 限制在工作区
- 无法访问用户文档、下载、桌面等敏感目录
- 适合日常开发:能读写代码,但不会意外污染用户数据
elevated 档:
- 模型进程使用完全受限令牌
- 即使被提权也无法突破工作区边界
- 适合处理不可信代码/指令的高风险场景
# ~/.zagens/config.toml
[sandbox]
# sandbox_level: "off" | "unelevated" | "elevated"
sandbox_level = "unelevated"
# 工作区根目录,unelevated 模式下所有写操作被 ACL 限制在此
workspace_root = "/path/to/project"
# elevated 模式下的网络白名单
[sandbox.network]
# API 端点白名单(elevated 模式下模型只能访问这些网络资源)
allowed_endpoints = [
"https://api.deepseek.com",
"https://api.openai.com",
]
4.4 macOS Seatbelt 和 Linux bwrap
macOS 和 Linux 的沙箱实现采用了各自原生方案:
macOS(Seatbelt/sandbox-exec):
<!-- zagens-sandbox/src/darwin/default.sb -->
(version 1)
(allow default)
(process-all) ; 允许子进程
(network outbound) ; 允许出站网络
; 工作区外写文件 → deny
(deny file-write* (regex #"^(?!/Users/xxx/projects/).*$"))
Linux(bwrap,需 prefer_bwrap = true 开启):
# 启动参数示例
bwrap \
--unshare-ipc --unshare-pid --unshare-net \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--bind /path/to/workspace /path/to/workspace \
--tmpfs /tmp \
--die-with-parent \
-- zagens-runtime
注意:Linux 端 Zagens 默认是 degraded 模式(无强制沙箱),需要安装 bubblewrap 并显式开启。CodeWhale upstream 则是走 Linux Landlock 路线,尚未完全落地。
五、CRAFT 多角色编排:不是只有一个模型在干活
5.1 单 Agent 的困境
大多数 AI 编程工具是单 Agent 模式:用户说「帮我重构这个模块」,一个模型就开干了。它的局限在于:
- 角色单一:模型既是探索者、又是实施者、还是审查者,容易「当局者迷」
- 自我审查不足:自己写的代码自己很难发现 bug(工程上的经典认知偏差)
- 上下文长度:全量探索 + 全量实施 + 全量审查,塞在同一个 context window 里
5.2 CRAFT 角色链
CRAFT 是 Zagens 实现的多角色协作协议,全称是 Code Review And Fix by Thinking(基于 upstream CodeWhale 的结构化裁决扩展):
| 角色 | 职责 | 约束 |
|---|---|---|
| Explore | 只读探索代码库,理解结构 | 禁止修改任何文件 |
| Implementer | 写代码、实施变更 | 必须在 Explore 的分析结论基础上行动 |
| Reviewer | 审查 Implementer 的产出,给出结构化裁决 | 必须引用具体文件和行号 |
| Verifier | 运行测试和 lint,验证 Reviewer 的判断 | 工具驱动,不做主观判断 |
| Auditor | 全库级别审计,验证行号和路径引用 | 纯机械验证,用于 fix-loop 后验证 |
结构化裁决:Reviewer 不能只说「这段代码有问题」,必须给出:
{
"verdict": "BLOCKER | MAJOR | MINOR | PASS",
"file": "src/main.rs",
"line": 42,
"reason": "内存泄漏:Vec::push 后未调用 drop",
"suggestion": "使用 drop(ptr::read(ptr)) 或 Box::leak"
}
5.3 CRAFT Fix-Loop
Explore → Implementer → Reviewer → Verifier
↑ │
└──── 裁决 BLOCKER → 修复 ←──┘
每轮裁决 BLOCKER → Implementer 重新修复 → 重新 Review
最多 3 轮(Implementer 层面 escalation)
3 轮 BLOCKER 未解决 → 任务标记为「需人工介入」
// zagens-craft/src/orchestrator.rs
pub struct CraftOrchestrator {
agents: HashMap<Role, Box<dyn Agent>>,
max_fix_loops: usize, // 默认 3
}
impl CraftOrchestrator {
pub async fn run(&self, task: &Task) -> CraftResult {
// Phase 1: Explore - 只读理解代码库
let explore_ctx = self.agents[&Role::Explore]
.run(&task.question)
.await?;
// Phase 2: Implementer - 基于 Explore 结论实施
let mut impl_result = self.agents[&Role::Implementer]
.run_with_context(&task.question, &explore_ctx)
.await?;
let mut fix_count = 0;
loop {
// Phase 3: Reviewer - 结构化裁决
let verdict = self.agents[&Role::Reviewer]
.run_with_context(&impl_result.changes, &explore_ctx)
.await?;
if verdict.is_pass() {
break;
}
fix_count += 1;
if fix_count >= self.max_fix_loops {
return Err(CraftError::MaxFixLoopsReached {
verdict: verdict.summary(),
});
}
// Phase 4: Implementer 修复(escalate)
impl_result = self.agents[&Role::Implementer]
.fix_with_feedback(&verdict)
.await?;
}
// Phase 5: Verifier - 运行测试
let test_result = self.agents[&Role::Verifier]
.verify(&impl_result.changes)
.await?;
Ok(CraftResult {
impl_result,
verdict_history: vec![], // 可用于回放
test_result,
})
}
}
5.4 实测:调查模块测试覆盖率
任务:「调查一下这个模块的测试覆盖率」
User → "调查 src/api 模块的测试覆盖率"
↓
Explore Agent(只读)
- 读取 src/api 目录结构
- 读取所有 .rs 文件
- 读取 .github/workflows/ci.yml
- 读取 Cargo.toml
- 输出:模块结构分析 + 现有测试清单
↓
Reviewer Agent(结构化裁决)
- 对比 src/api 的每个模块与 tests/ 目录
- 输出:无测试覆盖的模块列表(带行号)
↓
Verifier Agent(工具验证)
- 运行 cargo test -- --list
- 运行 tarpaulin 或 cargo-llvm-cov(如果有)
- 交叉对比:模块列表 vs 测试清单
↓
User ← 结构化报告
- 有覆盖的模块:X 个
- 无覆盖的模块:Y 个(带路径和行数)
- 覆盖率建议:优先级排序
六、Office 工具链:代码和文档同一条流水线
6.1 为什么重要
在实际工作中,代码交付和文档交付往往是同一任务的两面。Zagens 把 Office 文档生成嵌入了同一套 Agent runtime——不需要切换工具,不需要复制粘贴,Agent 做完代码,顺手把周报、报价单、竞品分析 PDF 一起生成。
6.2 XLSX:纯 Rust 实现
XLSX 生成是纯 Rust 的,使用 rust_xlsxwriter crate:
// zagens-office/src/xlsx_writer.rs
use rust_xlsxwriter::*;
/// 生成 Git 提交周报 XLSX
pub fn generate_weekly_report(commits: &[GitCommit]) -> Result<Vec<u8>, OfficeError> {
let mut workbook = Workbook::new();
// Sheet 1: 工作项明细
let mut sheet = Worksheet::new();
sheet.set_name("工作项")?;
sheet.write(0, 0, "日期")?;
sheet.write(0, 1, "提交人")?;
sheet.write(0, 2, "描述")?;
sheet.write(0, 3, "文件变更数")?;
// 表头样式
let mut header_format = Format::new()
.set_font_size(11)
.set_bold()
.set_background_color(FormulaColor::Custom(0x36, 0x5A, 0x9C))
.set_font_color(FormulaColor::White);
for col in 0..=3 {
sheet.write_with_format(0, col, "", &header_format)?;
}
// 数据行(交替颜色)
for (i, commit) in commits.iter().enumerate() {
let row = i + 1;
sheet.write(row, 0, &commit.date)?;
sheet.write(row, 1, &commit.author)?;
sheet.write(row, 2, &commit.message)?;
sheet.write(row, 3, commit.files_changed)?;
if i % 2 == 1 {
let alt_fmt = Format::new()
.set_background_color(FormulaColor::Custom(0xF2, 0xF2, 0xF2));
for col in 0..=3 {
sheet.write_with_format(row, col, sheet.get_cell(row, col).unwrap().value(), &alt_fmt)?;
}
}
}
// Sheet 2: 汇总
// ... 省略
// 自动筛选 + 冻结首行
let auto_filter = AutoFilter::new(0, 0, commits.len(), 3);
sheet.set_auto_filter(auto_filter);
sheet.freeze_panes(1, 0)?;
workbook.save_to_buffer()
}
生成后文件放在 deliverables/.office/ 目录,并生成 .payload.json 元数据文件,记录生成时的输入参数,便于后续增量修改。
6.3 DOCX / PPTX / PDF:嵌入式 Python
复杂的文档格式(Word、PowerPoint、PDF)通过捆绑 Python 脚本实现:
# zagens-office/python/generate_docx.py
# 通过 python-docx 生成 Word 文档
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
import sys
import json
def main():
payload = json.load(open(sys.argv[1]))
doc = Document()
doc.add_heading(payload['title'], 0)
for section in payload['sections']:
doc.add_heading(section['heading'], level=1)
for para in section['paragraphs']:
doc.add_paragraph(para)
output_path = sys.argv[2]
doc.save(output_path)
print(f"Generated: {output_path}")
if __name__ == '__main__':
main()
Python 运行时是 Zagens 捆绑的,不需要用户预先安装 Python 环境(Windows 桌面包自带嵌入式 Python)。
七、kernel-v2:工具执行内核的系统性重写
7.1 V1 的问题
Zagens 的工具执行内核(kernel)从 V1 到 V2 经历了一次系统性重写。V1 的核心问题:
串行执行:工具按调用顺序一个个执行,等上一个结束再下一个。没有并行,没有依赖分析。
无边界声明:每个工具(edit_file、run_shell 等)没有声明自己会读/写什么资源。系统无法判断两个 edit_file 操作是否冲突。
审批模型粗糙:审批规则只能以「工具名称」为单位,不能精细到具体操作(比如允许 edit_file 但不允许 delete_file)。
7.2 V2 的核心改进
V2 把每个工具的三个属性 formalize 了:
| 属性 | 说明 |
|---|---|
| Footprint | 工具的读写边界(读哪些路径、写哪些路径、网络请求等) |
| ResourceSet | 工具执行所需的资源集合 |
| Provenance | 工具的来源(内置 / MCP / Skill / 用户自定义) |
// kernel-v2/src/tool/mod.rs
/// V2 工具声明:包含 Footprint、ResourceSet、Provenance
pub struct ToolManifest {
pub name: String,
pub footprint: Footprint,
pub resource_set: ResourceSet,
pub provenance: Provenance,
pub policy: ToolPolicy,
}
#[derive(Debug, Clone)]
pub struct Footprint {
pub reads: Vec<PathPattern>, // 读取的文件/目录
pub writes: Vec<PathPattern>, // 写入的文件/目录
pub network: Vec<EndpointPattern>, // 访问的网络端点
pub spawns: Vec<ProcessPattern>, // 启动的子进程
}
#[derive(Debug, Clone)]
pub struct ToolPolicy {
pub approval_required: ApprovalLevel, // 枚举:auto / on_request / never
pub can_override: bool, // 是否可被更高权限覆盖
pub sandbox_required: bool, // 是否必须在沙箱内执行
}
7.3 DAG 并行调度
V2 支持 scheduler = dag 模式,将无依赖的工具并行调度:
传统串行(V1):
edit_file A → edit_file B → run_shell "cargo check"
总耗时:T(A) + T(B) + T(check)
DAG 并行(V2):
edit_file A ─┐
├→ run_shell "cargo check" (等待 A+B 完成)
edit_file B ─┘
总耗时:max(T(A), T(B)) + T(check)
// kernel-v2/src/scheduler/dag.rs
/// 构建工具调用 DAG 并行调度器
pub struct DagScheduler {
tools: Vec<ToolCall>,
manifest: HashMap<String, ToolManifest>,
}
impl DagScheduler {
/// 构建依赖图:分析哪些工具必须等待哪些完成
fn build_dependency_graph(&self) -> DiGraph<String, ()> {
let mut graph = DiGraph::new();
for tool in &self.tools {
graph.add_node(tool.id.clone());
}
// 检查 Footprint 冲突:有相同 write path 的工具不能并行
for i in 0..self.tools.len() {
for j in (i+1)..self.tools.len() {
if self.tools[i].manifest.footprint
.conflicts_with(&self.tools[j].manifest.footprint)
{
graph.add_edge(self.tools[j].id.clone(), self.tools[i].id.clone());
// j 必须等待 i 完成
}
}
}
graph
}
/// 拓扑排序后按批次执行(批次内并行)
pub async fn execute(&self) -> Vec<ToolResult> {
let graph = self.build_dependency_graph();
let batches = topological_sort_batches(&graph);
let mut results = Vec::new();
for batch in batches {
// 同一批次并行执行
let handles: Vec<_> = batch
.iter()
.map(|id| {
let tool = self.get_tool(id);
spawn(tool.execute())
})
.collect();
for handle in handles {
results.push(handle.await);
}
}
results
}
}
7.4 Manifest 驱动的审批
有了 Footprint 声明后,审批策略可以从「工具级别」细化到「资源级别」:
# config.toml - kernel-v2 审批策略
[kernel_v2]
scheduler = "dag" # dag | sequential
policy = "engine" # engine(manifest驱动)| legacy
# Manifest 驱动的审批规则(kernel_v2 模式)
[tool_policy.edit_file]
# 允许编辑 workspace 内的任何 .rs 文件
approval_required = "auto"
path_allow = ["{workspace}/**/*.rs", "{workspace}/**/*.toml"]
path_deny = ["{workspace}/**/secret*.rs", "{workspace}/.env"]
[tool_policy.delete_file]
approval_required = "on_request" # 始终需要确认
path_allow = ["{workspace}/**"]
path_deny = ["{workspace}/.git/**", "{workspace}/node_modules/**"]
[tool_policy.run_shell]
approval_required = "on_request"
# 仅允许特定命令
cmd_allow = ["cargo", "npm", "git", "ruff", "clippy"]
# 禁止危险命令
cmd_deny = ["rm -rf /", "format c:", ":(){ :|:& };:"]
八、近期动态:zagens-tui 与 kernel-v2 合流
8.1 zagens-tui:Harness 能力回归终端
Zagens v0.7.5(unreleased)新增了 zagens-tui,把 Zagens 桌面版积累的 LHT、CRAFT 等能力带回了 ratatui 全屏终端:
┌─────────────────────────────────────────┐
│ Sessions │ Transcript + Composer │ Inspector │
│ │ │ [Files] │
│ session_1 │ > 分析这段代码... │ [Diff] │
│ session_2 │ │ [Agents] │
│ │ [AI 响应] │ [MCP] │
│ │ │ │
│ Ctrl+A │ /lht 切换 LHT 模式 │ j/k/1-4 │
└─────────────────────────────────────────┘
关键特性:
- 三栏布局,与桌面版一致的 UX
- LHT checklist 和 plan phases 实时展示
- Inspector 面板:Files(文件树)、Diff(staged/worktree)、Agents(子代理状态)、MCP(工具连接)
- 模型/审批切换:
/model、Ctrl+A循环审批策略 - 启动恢复:
--fresh跳过上次 session - THK(思考链)动画条
这意味着 CodeWhale 老用户可以直接迁移到 Zagens TUI,无需换 runtime 血统。
8.2 kernel-v2 的交付计划
kernel-v2 各里程碑(M0-M5)均已落地或 partial 交付:
| 里程碑 | 内容 | 状态 |
|---|---|---|
| M0 | Footprint 声明抽象 | 已交付 |
| M1 | DAG 调度器 | 已交付 |
| M2 | Manifest 驱动审批 | Partial |
| M3 | ResourceSet 资源池管理 | Planning |
| M4 | Provenance 溯源审计 | Planning |
| M5 | Prefix Cache 优化(kernel-v2 M5) | Experimental |
默认仍以 legacy 运行,通过 config.toml 两路开关灰度尝鲜。预计 v0.8.0 正式默认启用 kernel-v2。
九、与主流方案横向对比
9.1 Zagens vs CodeWhale(upstream)
| 维度 | CodeWhale | Zagens |
|---|---|---|
| 形态 | 纯 TUI | 桌面(Tauri)+ TUI + CLI |
| 沙箱 | macOS Seatbelt 强制 | Windows 完整 + macOS Seatbelt + Linux bwrap |
| 任务验证 | durable task gates | LHT 三层门禁 |
| 多角色 | 持久化 sub-agent | CRAFT + fix-loop |
| Office | 无 | XLSX/Rust + DOCX/PPTX/PDF |
| 引擎 | 单进程 | Sidecar + 桌面壳 |
| UI | 全键盘 TUI | WebView + 托盘 + 通知 |
不是替代关系:CodeWhale 是成熟的终端方案,Zagens 是 desktop harness 方向。血统相同,取向不同。
9.2 Zagens vs Claude Code / Cline
| 维度 | Zagens | Claude Code | Cline |
|---|---|---|---|
| 平台 | Windows 桌面为主 | 跨平台(终端) | VS Code 插件 |
| 任务验证 | LHT 三层门禁 | checklist(主观) | 无(放手执行) |
| 沙箱 | OS 级 Windows | 无桌面沙箱 | 无 |
| Office | 原生支持 | 无 | 无 |
| 定价 | 自备 API Key | Claude 订阅 | 自备 API Key |
| 开源 | MIT | 部分开源 | Apache 2.0 |
核心取舍:Claude Code 和 Cline 是「放手让 Agent 干」,Zagens 是「门禁过了才算」。追求效率选前者,追求可信度选后者。
9.3 Zagens vs Cursor / Windsurf
| 维度 | Zagens | Cursor | Windsurf |
|---|---|---|---|
| 形态 | 独立桌面应用 | IDE(VS Code fork) | IDE(VS Code fork) |
| Agent 自主性 | 高(沙箱内) | 高 | 中 |
| 任务验证 | LHT 三层门禁 | 有限(编译错误检测) | 有限 |
| 定价 | 免费(自备 Key) | Pro 订阅 | Freemium |
| Office | 原生支持 | 无 | 无 |
十、生产级部署决策指南
10.1 选 Zagens 的场景
✅ Windows 桌面开发者:Windows 沙箱已完整落地,Tauri 原生体验
✅ DeepSeek 重度用户:血统深度集成,token 成本可优化
✅ 长程代码任务:需要可回放、可审计的执行轨迹
✅ 代码 + Office 文档同流程:避免工具切换,提升交付效率
✅ 企业安全合规:OS 级沙箱,数据不离开本地
10.2 不选 Zagens 的场景
❌ macOS/Linux 桌面用户:GUI 仍在开发,需用 TUI 或 CLI
❌ 团队 SaaS 需求:需要统一计费和管理平台
❌ 极致简单:只想开网页聊天,不想装任何东西
❌ IDE 深度集成:需要在 VS Code 内即时反馈
10.3 迁移路径
从 CodeWhale 来:
# 1. 安装 Zagens
curl -fsSL https://zagens.com/install | sh
# 2. 首次启动自动迁移(配置文件 + skills + MCP)
# ~/.deepseek/ → ~/.zagens/
# 会话数据库不迁移(独立存储)
从 Claude Code / Cline 来:
# 保留 Claude Code 用于快速交互
# Zagens 用于需要门禁验证的长程任务
# 两者不冲突,各司其职
结语:Harness 不是附件,是本体
Zagens 给我们最值得思考的一点是:大多数 AI 编程工具把安全和质量当作附件插件,而 Zagens 把它们做成了产品的核心逻辑。
LHT 完成门禁不是为了炫技,而是解决了一个真实的工程问题:模型说完了,不算;门禁过了,才算。这个设计哲学贯穿了整个项目——Sidecar 架构让 Token 永远不出 WebView、Windows 沙箱用内核级 WFP 阻断数据泄露、CRAFT fix-loop 防止模型在错误方向上越走越远。
2026 年的开源 AI Agent 生态正在分化成两条清晰路线:一条是「让模型更强、更放手」;另一条是「让模型更可信、更可控」。Zagens 坚定地站在第二条路上。
项目地址:github.com/didclawapp-ai/zagens,MIT 许可,upstream 血统来自 github.com/Hmbown/CodeWhale。
本文基于 Zagens v0.7.4 / v0.7.5(unreleased)实测,2026 年 6 月,Windows 11 + DeepSeek V4 Pro。