编程 Zagens 深度实战:当桌面 Harness 遇上 LHT 完成门禁——从 CodeWhale 血统到 Windows 原生沙箱、从 CRAFT 多角色编排到 kernel-v2 重写的生产级完全指南(2026)

2026-06-22 12:56:34 +0800 CST views 12

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 品牌升级为 CodeWhalecodewhale 命令,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 gatesLHT 三层完成门禁(自检→硬验收→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 + WebViewWindows 桌面用户;文件树、diff、多面板、托盘通知
zagens-tuiratatui 全屏终端SSH 远程;习惯纯键盘操作的终端党
zagens CLIexec / serve --httpCI、脚本、headless 自动化

2.2 Sidecar 架构:安全边界的第一刀

这是 Zagens 区别于大多数「桌面 AI 应用」最关键的设计:

  • Agent 引擎跑在独立的子进程zagens-runtime,Tauri externalBin
  • 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-cliCLI 入口、serve HTTP、exec 模式
zagens-coreLLM 调用抽象、prompt 管理
zagens-agentAgent loop、tool 调用、状态机
zagens-tools内置工具集(edit_file、run_shell、git 等)
zagens-mcpMCP 协议客户端
zagens-skillsSkill 加载与执行框架
zagens-config配置文件解析、keyring 集成
zagens-sandboxOS 级沙箱抽象(Windows/macOS/Linux)
zagens-officeXLSX(Rust)+ DOCX/PPTX/PDF(Python)生成
zagens-lhtLHT 完成门禁核心逻辑
zagens-craftCRAFT 多角色编排协议
zagens-sessionSQLite 会话持久化
zagens-desktopTauri shell、WebView 通信
zagens-tuiratatui 全屏终端界面

每个 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 的做法(大多数工具):

  1. 模型说「我找到了所有 unwrap」
  2. 模型说「我已全部替换」
  3. 用户自己跑 cargo check → 编译失败
  4. 循环...

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_filerun_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(工具连接)
  • 模型/审批切换:/modelCtrl+A 循环审批策略
  • 启动恢复:--fresh 跳过上次 session
  • THK(思考链)动画条

这意味着 CodeWhale 老用户可以直接迁移到 Zagens TUI,无需换 runtime 血统。

8.2 kernel-v2 的交付计划

kernel-v2 各里程碑(M0-M5)均已落地或 partial 交付:

里程碑内容状态
M0Footprint 声明抽象已交付
M1DAG 调度器已交付
M2Manifest 驱动审批Partial
M3ResourceSet 资源池管理Planning
M4Provenance 溯源审计Planning
M5Prefix Cache 优化(kernel-v2 M5)Experimental

默认仍以 legacy 运行,通过 config.toml 两路开关灰度尝鲜。预计 v0.8.0 正式默认启用 kernel-v2。


九、与主流方案横向对比

9.1 Zagens vs CodeWhale(upstream)

维度CodeWhaleZagens
形态纯 TUI桌面(Tauri)+ TUI + CLI
沙箱macOS Seatbelt 强制Windows 完整 + macOS Seatbelt + Linux bwrap
任务验证durable task gatesLHT 三层门禁
多角色持久化 sub-agentCRAFT + fix-loop
OfficeXLSX/Rust + DOCX/PPTX/PDF
引擎单进程Sidecar + 桌面壳
UI全键盘 TUIWebView + 托盘 + 通知

不是替代关系:CodeWhale 是成熟的终端方案,Zagens 是 desktop harness 方向。血统相同,取向不同。

9.2 Zagens vs Claude Code / Cline

维度ZagensClaude CodeCline
平台Windows 桌面为主跨平台(终端)VS Code 插件
任务验证LHT 三层门禁checklist(主观)无(放手执行)
沙箱OS 级 Windows无桌面沙箱
Office原生支持
定价自备 API KeyClaude 订阅自备 API Key
开源MIT部分开源Apache 2.0

核心取舍:Claude Code 和 Cline 是「放手让 Agent 干」,Zagens 是「门禁过了才算」。追求效率选前者,追求可信度选后者。

9.3 Zagens vs Cursor / Windsurf

维度ZagensCursorWindsurf
形态独立桌面应用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。

复制全文 生成海报 AI编程 Rust Tauri Agent 沙箱 WebAssembly Windows

推荐文章

html流光登陆页面
2024-11-18 15:36:18 +0800 CST
聚合支付管理系统
2025-07-23 13:33:30 +0800 CST
go发送邮件代码
2024-11-18 18:30:31 +0800 CST
html夫妻约定
2024-11-19 01:24:21 +0800 CST
使用Rust进行跨平台GUI开发
2024-11-18 20:51:20 +0800 CST
Vue3中如何处理权限控制?
2024-11-18 05:36:30 +0800 CST
Nginx rewrite 的用法
2024-11-18 22:59:02 +0800 CST
程序员茄子在线接单