Eosphor
Vyane 功能

workflow

用 JS 脚本编排多个 agent 任务,跑在安全沙箱里

vyane workflow 让你用一段 JS 脚本编排多个 agent 任务——并行跑、串成流水线、按预算和并发上限跑,而不用手动一个个 dispatch。脚本里的每个 agent() 调用都会落到 Vyane 的派发能力(跨 provider/harness、kernel 准入、owner 隔离),你只负责写编排逻辑。

一句话定位

它相当于「可编程版的 dispatch」:普通 dispatch 是「派一个任务」,workflow 是「写一段脚本,任意 fan-out / 流水线 / 评审团,一次跑完」。脚本 API 刻意对齐 Claude Code Workflow,所以在 CC 里验证过的编排套路能直接搬过来。

想看 Deno、host bridge、microVM 和 Rust 迁移边界,读workflow 架构与隔离。本页只讲怎么用。

为什么要它

Vyane 原来的编排能力散在几处:broadcast 做同任务多目标、collaborate 做多轮辩论、orchestrate 做子任务波次。但「派 3 个并行子任务、汇总、再根据结果决定下一步」这种任意形状的编排,以前没有统一入口。

workflow 把编排引擎从 harness 侧挪进了 Vyane:脚本即数据。任何 harness(Claude Code、Codex CLI、OpenCode、自建的,甚至一个裸 HTTP 客户端)写一段脚本文本交给 Vyane 跑,不再绑死在某个 harness 上。这样主会话换 harness,写出来的编排脚本也不作废。

设计精髓:编排不该是黑盒

很多框架的编排要么把流程硬写进代码(改一下就得动源码、难可视化),要么是个不透明的状态机。Vyane 的取向是:既支持任意复杂的多步骤,又让每一步都可观测、可控制——脚本里的每个 agent() 调用都能中途换模型、插入人工审批,整个运行有并发池和预算硬闸兜底(不会跑飞),还能暂停 / 恢复 / 回滚。复杂不等于失控。

一段脚本长什么样

下面是「3 个模型并行审同一份改动,再汇总成一句结论」的最小例子:

export const meta = {
  name: "triple-review",
  description: "三个模型并行审 diff,汇总结论",
};

phase("并行初审");
const reviews = await parallel([
  () => agent("审这份 diff 的正确性,列出 bug", { tag: "review" }),
  () => agent("审这份 diff 的安全问题", { tag: "review" }),
  () => agent("审这份 diff 的性能与可维护性", { tag: "review" }),
]);

phase("汇总");
const summary = await agent(
  `把这三份审查意见去重合并成一句结论:\n${reviews.join("\n---\n")}`,
);

log(`汇总完成`);
return summary;

几个要点:

  • parallel([...]) 接一组 thunk(用 () => 包起来的调用),并发跑,全部完成后返回结果数组。
  • 某个 agent() 失败或被跳过时,返回 null(不会让整个脚本崩),对齐 CC 语义。
  • phase() / log() 是进度叙述,会进 run 状态和 dashboard,方便你 tail 进度。
  • 脚本最后 return 的值就是整个 workflow 的产物。

脚本 API

脚本可用的编排原语(与 Claude Code Workflow 对齐):

API作用
export const meta = {...}脚本元信息(名字、描述、阶段),纯字面量
await agent(prompt, opts?)派一个子任务,返回文本或结构化对象;失败/跳过返回 null
parallel(thunks)一组 thunk 并发跑,全完成后返回数组;某位抛错→该位 null
pipeline(items, ...stages)每个 item 逐 stage 流水,无阶段屏障;stage 抛错→该 item 落 null 跳后续
phase(title)进度分组(叙述用)
log(msg)叙述行(进 run 状态与 dashboard)
args调用方传进来的 JSON 值,脚本里原样可读
budget.total / spent() / remaining()读 run 预算(用剩多少);不设预算时 total 为 null
workflow(nameOrRef, args)内嵌一层子 workflow(共享并发/预算);只允许一层嵌套

agent()opts 除了对齐 CC 的字段(label / phase / schema / effort / isolation),还有 Vyane 扩展:

opts 字段说明
provider指定派发目标(同 vyane dispatch 的 selector);默认 'auto' 走智能路由
profile命名 profile,一次定好 provider/protocol/harness/model 四层
model直接写具体 model ID
schema / schema_id要求结构化输出:内联 JSON Schema,或注册制 schema ID(二选一),校验失败会重试
sandboxread-only(默认)/ write / full,由 kernel 审批
workdir / isolation工作目录;isolation:'worktree' 时给这次调用套隔离 worktree
session续接一个 Vyane 持久 session,做多轮 agent 对话
tag路由分类信号(给智能路由用)

确定性约束

脚本里禁用 Date.now() / Math.random() / 无参 new Date()——用了会直接报错。原因是 resume 要靠脚本行为可复现(见下文「续跑」)。需要时间戳交给 host 侧盖,需要随机性用 args 传进来。harness 语义请用 profile / provider 表达,不要在脚本里指定执行壳。

沙箱与防护

脚本来自模型输出,按不可信输入对待。

  • 跑在 Deno 沙箱子进程里:独立 OS 进程,零权限 flag(无 --allow-*)。唯一对外通道是 stdio 上的一条 hook 桥(脚本发 agent(),host 侧才去真派发)。脚本崩了也拖不垮 daemon。真正的联网、文件、派发全在 Python host 这边做。

隔离现状:默认止血 + 可选 microVM 硬隔离(2026-07)

背景:Deno 的模块加载器是独立于文件读权限的另一条轴——零 --allow-read 挡不住脚本用 import("file://…", {with:{type:"text"}}) 读宿主文件(--deny-read/--deny-import 均无效,eval 还能绕过静态检测)。Deno 官方也明确零 flag「不是完整隔离边界」。Vyane 现在分两档应对,由 agent()/run 的 isolation 选择:

默认档(unsafe-local) —— 脚本仍跑在本机 Deno 子进程,但 macOS 上默认用 sandbox-exec 反向黑名单在 OS 层挡住对敏感路径(~/.codex / ~/.config/vyane / ~/.ssh / .env 等)的读取(env VYANE_WORKFLOW_SANDBOXauto/off/on)。这是临时防护,不是终局隔离:黑名单天生不完备、不保护 host 侧 dispatch、Linux 侧 bwrap 仍 TODO。所以默认档仍按「可信 / 受控内部使用」对待(运行目录不放 secrets、HTTP 入口不暴露给不可信调用方)。

硬隔离档(microvm) —— 脚本跑进一个独立的 Linux microVM:guest 有自己的内核和文件系统、不挂宿主目录——宿主 secrets 物理上不在 guest 里,读逃逸从「权限绕过」变成「要穿透硬件内存隔离」。bridge(nonce JSON-RPC 行分帧)跨 VM 边界走 guest deno 的 stdio,凭证 / 联网 / 派发全留在可信宿主、guest 只发意图;guest deno 零 --allow-*。backend 不可用时 fail-closed(直接报错,绝不静默回退到本机 stdio)。两个平行 backend,由 VYANE_WORKFLOW_MICROVM_BACKEND 选(默认 lima):lima(Apple Virtualization.framework,共享常驻 guest,mounts:null,桥走 limactl shell)是稳的生产 backend;krunvm(libkrun,每 run 一个 OCI 微 VM、官方 denoland/deno 镜像、桥走 krunvm start virtio-console)更轻更接近终局形态、opt-in。

两档共同成立的:sandbox=full 二次夹紧、budget/owner 隔离;网络 / env / 子进程 / FFI 未放开。bridge 的 nonce 不写进 module 源码,而是在脚本 body 运行前由 host 通过 stdin init frame 下发;用户脚本作为 AsyncFunction body 执行,旧的 self-source / wrapper-breakout nonce 读取形态被挡住。确定性锁(禁时钟 / 随机)是 resume 可复现的 best-effort、不是安全边界。

microVM 部署环境变量

isolation:'microvm' 的 backend 由 VYANE_WORKFLOW_MICROVM_BACKEND 选(lima 默认 / krunvm)。共用 env:VYANE_WORKFLOW_MICROVM_GUEST_DENO(guest 内 Deno 路径)、VYANE_WORKFLOW_MICROVM_MAX_CONCURRENT(并发微 VM 上限,默认 4)。

  • lima:VYANE_WORKFLOW_MICROVM_LIMA_INSTANCE(guest 实例名)。guest 需预置(mounts:null 不挂宿主目录 + 装好 Deno)。
  • krunvm:VYANE_WORKFLOW_MICROVM_KRUNVM_IMAGE(OCI 镜像,默认 docker.io/denoland/deno:2.9.1)、VYANE_WORKFLOW_MICROVM_KRUNVM_MEM_MB(默认 512)。macOS 需先配好 case-sensitive APFS 存储卷(/Volumes/krunvm)。每 run 建/删一个微 VM,无需预置常驻 guest。

backend 不可用时 microVM run fail-closed 报错、不静默降级。默认档(unsafe-local)的 macOS interim 沙箱由 VYANE_WORKFLOW_SANDBOX(auto/off/on)控。

失控防护(防死循环、防把配额烧穿):

防护默认说明
run 级并发上限8同时在跑的 agent() 数;超出的排队
per-provider 信号量按 provider 配全局跨 run 共享,防一个大 fan-out 把某家配额打爆殃及别人
agent 总数上限200一次 run 累计能派多少个 agent
microVM 并发上限4isolation:'microvm':同时在跑的 guest microVM 数(env VYANE_WORKFLOW_MICROVM_MAX_CONCURRENT);超出的排队,防一个大 fan-out 冷启海量 guest 耗尽虚机
wall-clock 超时3600s覆盖本地 Deno spawn、microVM create/provision、脚本 pump 全程;超时=硬杀沙箱进程 + run 置 timeout(journal 保留,可续跑);microVM 下还会跨 VM 边界收割 guest 进程

预算门(CC 一致的有界门):run 参数 budget_tokens / budget_usd 设了之后,committed+reserved 触顶就拒新的 agent()(返 null + BudgetExceeded)。注意这是有界门,不是完美硬帽——不知道每次调用真实花费时,已在线下放行的一波并发调用可能把总量推过上限一个波次(上界 = 并发上限 × 单次成本,不会 N× 失控)。要真正 never-exceed,给 agent() 传一个 ≥ 单次最大成本的 estimate。不设预算=不限。

三个入口

同一套引擎,三种调用方式(哪个 harness 都能接):

1. MCP 工具 vyane_workflow —— 主参数 script(脚本文本),可选 args(JSON)、sandbox_defaultisolation(unsafe-local 默认 / microvm 硬隔离)、workdirbudget_tokensbudget_usdtimeoutresume_from。异步返回 run 句柄,进度走 vyane_task_status 族查看。

2. CLI vyane workflow run —— 这是 Codex CLI / OpenCode / 自建 harness 最省事的接法,一行 Bash 就能跑:

# 跑一个脚本文件,传参数
uv run vyane workflow run review.js --args '{"pr": 123}'

# 设 run 预算(花超硬闸拦住)
uv run vyane workflow run review.js --budget-tokens 200000

# 从上次的 run 续跑(没改的前缀直接回放,不烧配额)
uv run vyane workflow run review.js --resume-from <run_id>

# 不烧真配额的空跑,用假派发验编排逻辑
uv run vyane workflow run review.js --fake-dispatch

# 把不可信脚本关进 microVM 硬隔离跑(guest 不可用则 fail-closed 报错)
uv run vyane workflow run untrusted.js --isolation microvm

常用参数:--args(传给脚本的 JSON)、--sandbox-default(默认 read-only)、--workdir--budget-tokens / --budget-usd--timeout(默认 3600s)、--resume-from--fake-dispatch

3. HTTP —— POST /api/workflow/run 起一个 run(异步,返回 run_id),GET /api/workflow/<run_id> 查状态和 journal 摘要,GET /api/workflow/<run_id>/events 是 SSE 进度流。dashboard 消费同一组接口。

续跑(resume)

每个 agent() 调用都记进 journal(记 prompt+opts 的 hash、结果、用量)。用 --resume-from <run_id>(或 MCP 的 resume_from)续跑时,按调用序列做最长前缀匹配:脚本开头没变过的那些调用直接从缓存回放,不重新烧配额,只从第一处变化开始实跑。所以你可以改完脚本再 resume,只重跑改动点之后的部分。这也是为什么脚本必须确定性——不然前缀对不上。

v1 旧版说明

旧的 TOML 线性版计划淘汰

workflow 早期是一版 TOML 静态线性步骤链(步骤严格顺序跑、步间只传纯文本占位符、不能分支/循环/动态 fan-out)。现在这版计划淘汰:MCP 侧保留为 vyane_workflow_legacy 过渡,最终随编排收口一并删除。新写编排一律用本页讲的 JS 脚本版。

相关

On this page