orca-per-workspace-env

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Per-Workspace Environments

每个工作区独立环境

Help a user stand up and maintain a repo-owned per-workspace environment recipe end to end. Each workspace gets its own on-demand, disposable runtime (a cloud sandbox, a VM, or a local one), created fresh and torn down after.
Orca is a thin wrapper: you guide, detect, and scaffold; you never own the user's cloud account, billing, images, or credentials.
  • You DO: sequence the setup, detect what's detectable (provider CLI present/logged-in? recipe present?
    doctor
    passing?), scaffold provider-templated scripts the user fills in, drive the slow snapshot/auth phases with the user, and always show the next action.
  • You DO NOT: create accounts, choose plans/regions, invent org/project/scope ids, store or print secrets, or run anything that spends money without an explicit user OK.
First-time setup has four phases before the per-workspace recipe runs — easy to miss, so walk them in order:
  1. Prerequisites — cloud account, provider CLI, scope/project, plan limits, git token (§2).
  2. Base snapshot — reusable image: tools + repo + headless build, snapshotted once (§3).
  3. Agent-auth snapshot — boot the base, run interactive device-auth, re-snapshot (§4).
  4. State — thread snapshot id / scope / project / port between phases via a state file (§6).
Then the per-workspace contract (create/suspend/resume/destroy) runs fast (§8).
The one branch that shapes everything — connection mode: Orca-server (
create
runs
orca serve
in the env and emits a
pairingCode
; §7c/§7f) vs SSH (
create
runs no server and emits a
connection.type:"ssh"
block Orca dials into; §7g/§7h). Settle this first — it changes the
create
output shape and half the templates.
Quick-start (happy path): interview the user (connection mode Orca-server vs SSH, provider, agent CLI, git auth — §1.2) + read the provider's CLI docs → scaffold
scripts/orca-vm/
from §7 → run the base-snapshot script, then the auth script (you invoke these by hand; not via
orca.yaml
) → wire
environmentRecipes
in
orca.yaml
orca vm recipe doctor <id> --json
(free) → then the
--provision
self-test loop (§9) until it passes.

协助用户端到端地搭建和维护仓库专属的工作区环境配方。每个工作区都会拥有自己的按需、一次性运行时(云沙箱、虚拟机或本地运行时),使用后即被销毁。
Orca是一个轻量封装层:你仅需提供引导、检测和模板生成服务;绝不掌控用户的云账户、账单、镜像或凭证。
  • 你需要做的: 规划设置流程,检测可识别项(提供商CLI是否已安装/登录?配方是否存在?
    doctor
    命令是否通过?),生成提供商模板化脚本供用户填充,引导用户完成耗时的快照/认证阶段,并始终明确下一步操作。
  • 你不能做的: 创建账户、选择套餐/区域、生成组织/项目/范围ID、存储或打印密钥,或在未获得用户明确许可的情况下执行任何产生费用的操作。
首次设置包含四个阶段,需按顺序执行,否则容易遗漏:
  1. 前置条件 —— 云账户、提供商CLI、范围/项目、套餐限制、Git令牌(第2节)。
  2. 基础快照 —— 可复用镜像:包含工具、仓库和无头构建,只需创建一次(第3节)。
  3. Agent认证快照 —— 启动基础镜像,运行交互式设备认证,重新生成快照(第4节)。
  4. 状态管理 —— 通过状态文件在各阶段传递快照ID/范围/项目/端口等信息(第6节)。
完成上述阶段后,工作区生命周期操作(创建/暂停/恢复/销毁)即可快速执行(第8节)。
决定所有流程的核心分支——连接模式: Orca-server
create
命令在环境中运行
orca serve
并生成
pairingCode
;第7c/7f节)与SSH
create
命令不运行服务器,而是生成
connection.type:"ssh"
块供Orca连接;第7g/7h节)。需先确定此模式,它会改变
create
命令的输出格式和半数模板内容。
快速入门(理想流程): 与用户沟通确认(连接模式:Orca-server或SSH、提供商、Agent CLI、Git认证——第1.2节)+ 查阅提供商CLI文档 → 基于第7节生成
scripts/orca-vm/
目录下的脚本 → 手动运行基础快照脚本,再运行认证脚本(不通过
orca.yaml
调用) → 在
orca.yaml
中配置
environmentRecipes
→ 执行
orca vm recipe doctor <id> --json
(免费) → 重复执行
--provision
自测循环(第9节)直至通过。

1. Setup workflow

1. 设置流程

Drive these with the user. [CHECKPOINT] steps need explicit confirmation — they spend money, take a long time, or need the user at the keyboard. Never create an Orca workspace or commit unless asked.
  1. Inspect the repo for an existing
    environmentRecipes
    entry,
    scripts/orca-vm/
    , a state file, or setup notes. If a working recipe exists, jump to Doctor (§9) instead of rebuilding.
  2. Interview the user up front — gather these choices and confirm them back before scaffolding anything. Don't pick for them (§11); don't guess.
    • Connection mode: how Orca attaches to the environment — an Orca server (the VM runs
      orca serve
      and Orca pairs over its pairing URL; worked example §7f) or SSH (Orca connects to the host over SSH; §7g). This decides the recipe's connection shape, so settle it first.
    • Provider: Vercel Sandbox, Fly, Modal, an existing SSH host, … For non-obvious providers, also ask scope/project/region and plan limits (§2). Then read that provider's CLI/SDK docs (or
      <cli> --help
      ) before scaffolding — you need its exact create/exec/snapshot/remove verbs. If a provider advertises
      ssh
      , verify whether it exposes a real dialable SSH target (host/port/user/key or proxy command) or only a provider-mediated interactive shell; Orca SSH mode needs the former.
    • Coding-agent CLI + account: which agent runs in the VM (
      codex
      ,
      claude
      , …) and that the user has an account for it — it gets logged in during the Phase-3 auth snapshot (§4).
    • Git auth: the token source for cloning a private repo (
      GH_TOKEN
      /
      GITHUB_TOKEN
      or
      gh auth token
      ; §5).
  3. Check prerequisites (§2) — detect the provider CLI + auth and confirm the items above are in place before any paid step.
  4. Scaffold scripts + state file from §7 (worked Vercel example: §7f; SSH host: §7g; Docker SSH: §7h; Windows: §7i), filling in the provider's real commands. Make them executable.
  5. [CHECKPOINT] Build the base snapshot (§3) — paid, slow.
  6. [CHECKPOINT] Authenticate the agent (§4) — interactive; the user follows a URL/code. You cannot drive this step — you run commands non-interactively, so there's no TTY for
    docker exec -it
    /
    ssh -t
    to prompt against. The user runs the Phase-3 login in their own terminal (or via the Claude Code harness bang-prefix —
    ! <cmd>
    , with the required space after
    !
    ); you scaffold and drive the non-interactive phases around it. After kicking it off, ask the user to report back once the login finishes — you can't observe it completing, and you need that confirmation before resuming the non-interactive steps (base/auth commit, doctor, provision).
  7. Wire the recipe so
    orca.yaml
    points create/suspend/resume/destroy at the scripts (§8). The workspace composer reads
    environmentRecipes
    from the project's primary checkout of
    orca.yaml
    , not from a feature branch or worktree. So a recipe added only on a branch won't appear as a "Run on" option until that
    orca.yaml
    change is committed and merged to the project's primary branch. Tell the user this up front:
    doctor
    /
    --provision
    validate the scripts from the working copy on any branch, but creating a workspace from the recipe in the picker needs it on primary.
  8. Dry-run doctor
    orca vm recipe doctor <recipe-id> --repo-path <repo> --json
    (free, static; §9). Fix every failure before going live.
  9. [CHECKPOINT] Live self-test — get the user's OK once, then run
    orca vm recipe doctor <recipe-id> --provision --json
    as a loop: it runs create → validates → destroys, and on failure returns a full transcript. Read it, fix the scripts, and re-run yourself until it passes (§9). Spends cloud money; the one approval covers the loop.
  10. [CHECKPOINT] Optional workspace test — only if asked: create a workspace via the picker, then verify sleep/wake/delete.

引导用户完成以下步骤。[检查点] 步骤需要用户明确确认——这些操作会产生费用、耗时较长,或需要用户在场操作。除非用户要求,否则不要创建Orca工作区或提交代码。
  1. 检查仓库 是否存在现有
    environmentRecipes
    条目、
    scripts/orca-vm/
    目录、状态文件或设置说明。若已有可用配方,直接跳至Doctor验证环节(第9节),无需重新构建。
  2. 提前与用户沟通 —— 收集以下选项并确认后再生成模板。不要替用户做选择(第11节);不要猜测。
    • 连接模式: Orca连接环境的方式——Orca server(虚拟机运行
      orca serve
      ,Orca通过配对URL连接;示例见第7f节)或SSH(Orca通过SSH连接主机;第7g节)。此模式决定配方的连接格式,需优先确定。
    • 提供商: Vercel Sandbox、Fly、Modal、现有SSH主机等。对于不常见的提供商,还需询问范围/项目/区域和套餐限制(第2节)。然后查阅该提供商的CLI/SDK文档(或
      <cli> --help
      )再生成模板——你需要了解其创建/执行/快照/删除的准确命令。 如果提供商支持
      ssh
      ,需确认其是否提供可直接连接的SSH目标(主机/端口/用户/密钥或代理命令),还是仅提供提供商介导的交互式Shell;Orca的SSH模式需要前者。
    • 编码Agent CLI + 账户: 虚拟机中运行的Agent(
      codex
      claude
      等),以及用户是否拥有对应账户——在第3阶段的认证快照中会完成登录。
    • Git认证: 克隆私有仓库的令牌来源(
      GH_TOKEN
      /
      GITHUB_TOKEN
      gh auth token
      ;第5节)。
  3. 检查前置条件(第2节) —— 检测提供商CLI是否已安装并完成认证,确认上述所有项已准备就绪后,再执行任何付费操作。
  4. 基于第7节生成脚本 + 状态文件(Vercel示例:第7f节;SSH主机:第7g节;Docker SSH:第7h节;Windows:第7i节),填充提供商的实际命令。确保脚本可执行。
  5. [检查点] 构建基础快照(第3节) —— 付费且耗时较长。
  6. [检查点] Agent认证(第4节) —— 交互式操作;用户需跟随URL/验证码完成。你无法自行驱动此步骤——你执行的是非交互式命令,没有TTY供
    docker exec -it
    /
    ssh -t
    弹出提示。用户需在自己的终端中执行第3阶段的登录操作(或通过Claude Code harness的前缀
    ! <cmd>
    ,注意
    !
    后需加空格);你负责生成模板并驱动非交互阶段。启动后,请用户在登录完成后告知你——你无法观察到登录完成,需要用户确认后再继续执行非交互步骤(基础/认证提交、Doctor验证、部署)。
  7. 配置配方 使
    orca.yaml
    指向创建/暂停/恢复/销毁脚本(第8节)。工作区组合器从项目主分支的
    orca.yaml
    中读取
    environmentRecipes
    而非功能分支或工作树。因此,仅在分支中添加的配方不会显示为“运行于”选项,直到
    orca.yaml
    的变更提交并合并到项目主分支。请提前告知用户:
    doctor
    /
    --provision
    可验证任何分支工作副本中的脚本,但从选择器创建工作区需要配方已在主分支中。
  8. Dry-run Doctor验证 —— 执行
    orca vm recipe doctor <recipe-id> --repo-path <repo> --json
    (免费、静态检查;第9节)。在正式使用前修复所有失败项。
  9. [检查点] 在线自测 —— 获得用户一次许可后,循环执行
    orca vm recipe doctor <recipe-id> --provision --json
    :它会执行创建→验证→销毁,失败时返回完整记录。读取记录、修复脚本并重新执行,直至通过(第9节)。此操作会产生云费用;一次许可覆盖整个循环。
  10. [检查点] 可选工作区测试 —— 仅在用户要求时执行:通过选择器创建工作区,然后验证休眠/唤醒/删除功能。

2. Phase 1 — Prerequisites

2. 阶段1 —— 前置条件

The user's responsibility; verify what's verifiable, ask for the rest, invent nothing. State which items you verified vs. which the user asserted.
  • Connection mode (Orca server vs SSH) confirmed with the user — see §1 step 2; it shapes the recipe.
  • Cloud account + plan that allows sandboxes/VMs. Ask.
  • Provider CLI installed + authenticated — detect (
    command -v <cli>
    ), check auth (e.g.
    vercel whoami
    ). If missing, point at the provider's docs; don't log them in.
  • Scope / project / region the sandboxes live under. Ask; flows into every script via state.
  • Plan / timeout / RAM caps. Record them — e.g. Vercel Hobby caps sandbox timeout at 45m, which limits both the base build and per-workspace runtime (see §10).
  • Git token for private repos (
    GH_TOKEN
    /
    GITHUB_TOKEN
    , or the provider's git auth; can fall back to
    gh auth token
    ). See §5.
  • Coding-agent CLI choice (
    codex
    ,
    claude
    …) and that the user has an account — it gets authenticated into the VM in Phase 3.

这些是用户的责任;验证可核实的项,询问其余项,不要自行编造。说明哪些项是你已验证的,哪些是用户确认的。
  • 连接模式(Orca server或SSH)已与用户确认——见第1步第2节;它决定配方的形态。
  • 云账户 + 套餐 支持沙箱/虚拟机。需询问用户。
  • 提供商CLI已安装 + 已认证 —— 检测(
    command -v <cli>
    ),检查认证状态(例如
    vercel whoami
    )。若缺失,指向提供商文档;不要替用户登录。
  • 沙箱所属的范围 / 项目 / 区域。需询问用户;这些信息会通过状态文件流入每个脚本。
  • 套餐 / 超时 / RAM限制。记录这些信息——例如Vercel Hobby套餐的沙箱超时限制为45分钟,这会限制基础构建和工作区运行时间(见第10节)。
  • 私有仓库的Git令牌
    GH_TOKEN
    /
    GITHUB_TOKEN
    ,或提供商的Git认证;可 fallback 到
    gh auth token
    )。见第5节。
  • 编码Agent CLI选择
    codex
    claude
    等)以及用户是否拥有对应账户——在第3阶段会完成虚拟机中的Agent认证。

3. Phase 2 — Base snapshot (the reusable image)

3. 阶段2 —— 基础快照(可复用镜像)

Build once, snapshot, and every workspace boots from it in seconds instead of rebuilding. Provisioning + building takes a while (often ~20–30 min), so it runs behind a checkpoint. The script shape is §7a; key points:
  • Build the headless Electron main only (not the renderer) so it fits in plan RAM.
  • Use the VM image's package manager (
    apt
    /
    dnf
    /
    apk
    , per the base distro — not the provider brand).
  • Clone with the git token via
    GIT_ASKPASS
    (§5).
  • Trap errors and remove the half-built sandbox so a crash doesn't leave a paid resource running.
  • Snapshot the stopped sandbox, parse the snapshot id, and write it + scope/project/port/repo to state.

只需构建一次并生成快照,之后每个工作区都可在几秒内从该快照启动。部署+构建耗时较长(通常约20-30分钟),因此需在检查点后执行。脚本格式见第7a节;关键点:
  • 仅构建无头Electron主进程(不构建渲染进程),以适配套餐RAM限制。
  • 使用虚拟机镜像的包管理器(
    apt
    /
    dnf
    /
    apk
    ,基于基础发行版——而非提供商品牌)。
  • 通过
    GIT_ASKPASS
    使用Git令牌克隆仓库(第5节)。
  • 捕获错误并删除半构建的沙箱,避免崩溃后留下付费资源持续运行。
  • 快照已停止的沙箱,解析快照ID,并将其+范围/项目/端口/仓库信息写入状态文件。

4. Phase 3 — Agent-auth snapshot (interactive)

4. 阶段3 —— Agent认证快照(交互式)

The base snapshot has the agent CLI installed but not logged in, and per-workspace VMs are ephemeral — so authenticate once and bake it into a second snapshot layer. Script shape is §7b:
  1. Boot a sandbox from the base
    snapshotId
    (from state).
  2. Run the agent's login interactively (
    --interactive --tty
    ); the user completes the URL/code in their browser. On a headless VM this must be the device-auth flow (e.g.
    codex login --device-auth
    ), not plain
    codex login
    : the default OAuth login starts a loopback callback server on a container port the host browser can't reach, so it hangs. Device-auth instead prints a URL + code the user opens on the host.
  3. Verify login; refuse to snapshot an unauthenticated VM. Prefer the status command's exit code (most agent CLIs exit non-zero when unauthenticated). If you grep instead, agent status often goes to stderr (e.g.
    codex login status
    prints "Logged in using ChatGPT" there), so fold stderr first (
    ... 2>&1 | grep …
    ) and match the agent's exact success line — never
    grep -qi 'logged in'
    , which also matches "not logged in" and would commit an unauthenticated image.
  4. Re-snapshot, parse the new id, and overwrite
    snapshotId
    in state to the authenticated image (recording
    authSourceSnapshotId
    ). Remove the auth sandbox.
You can't drive step 2 yourself (you run commands non-interactively — no TTY). The user runs it in their own terminal, or via the Claude Code harness bang-prefix (
! <cmd>
, with the required space after
!
). You scaffold/boot the sandbox and run steps 3–4, but you cannot observe the interactive login finishing — so ask the user to tell you when it's done before you verify and re-snapshot.
If the agent's credentials are short-lived, warn that the snapshot may need periodic re-auth (§10).
For disposable runtimes, do not treat a host agent config directory (for example
~/.codex
) as the auth snapshot by bind-mounting or copying it wholesale. Agent homes often contain sqlite state, hook approval state, caches, logs, and host-specific env/config. Instead, authenticate/configure the agent inside the disposable runtime and snapshot/commit that runtime layer.

基础快照已安装Agent CLI但未登录,且工作区虚拟机是临时的——因此只需认证一次并将其嵌入到第二个快照层中。脚本格式见第7b节:
  1. 从基础
    snapshotId
    (来自状态文件)启动沙箱。
  2. 交互式运行Agent登录命令(
    --interactive --tty
    );用户在浏览器中完成URL/验证码操作。在无头虚拟机上必须使用设备认证流程(例如
    codex login --device-auth
    ),不能使用普通的
    codex login
    :默认OAuth登录会在容器端口启动环回回调服务器,主机浏览器无法访问,导致挂起。设备认证会打印URL+验证码,用户在主机上打开即可。
  3. 验证登录状态;拒绝快照未认证的虚拟机。优先使用状态命令的退出码(大多数Agent CLI在未认证时会返回非零退出码)。若必须使用grep,Agent状态通常输出到stderr(例如
    codex login status
    会在stderr中打印"Logged in using ChatGPT"),因此需先合并stderr
    ... 2>&1 | grep …
    )并匹配Agent的准确成功行——绝不要使用
    grep -qi 'logged in'
    ,因为它也会匹配"登录",导致提交未认证的镜像。
  4. 重新生成快照,解析新ID,并将状态文件中的
    snapshotId
    覆盖为认证后的镜像ID(同时记录
    authSourceSnapshotId
    )。删除认证沙箱。
你无法自行驱动第2步(你执行的是非交互式命令——没有TTY)。用户需在自己的终端中执行,或通过Claude Code harness的前缀
! <cmd>
(注意
!
后需加空格)。你负责生成模板/启动沙箱并执行第3-4步,但你无法观察到交互式登录完成——因此请用户在完成后告知你,再进行验证和重新快照。
如果Agent凭证有效期较短,需提醒用户快照可能需要定期重新认证(第10节)。
对于一次性运行时,不要将主机Agent配置目录(例如
~/.codex
)通过绑定挂载或整体复制作为认证快照。Agent目录通常包含sqlite状态、钩子审批状态、缓存、日志和主机特定的环境/配置。相反,应在一次性运行时内完成Agent的认证/配置,并快照/提交该运行时层。

5. Credentials

5. 凭证管理

  • Never commit secrets or put them in
    userData
    , recipe JSON, comments, docs, or the state file.
  • Git token: read from env (
    GH_TOKEN
    /
    GITHUB_TOKEN
    ), falling back to
    gh auth token
    . Pass to the VM only via the provider's ephemeral
    --env
    . Inside the VM, use a
    GIT_ASKPASS
    helper with
    x-access-token
    (not the token in the clone URL) and
    GIT_TERMINAL_PROMPT=0
    so a missing token fails fast instead of hanging. When you write the helper from inside
    bash -lc
    under
    set -u
    , escape the positional arg and the token (
    \$1
    ,
    \$GH_TOKEN
    ) so they land literally and resolve at git-runtime — an unescaped
    $1
    aborts with "unbound variable", and a literal
    $GH_TOKEN
    keeps the real token out of the written file.
    rm -f
    the helper after the clone/fetch.
  • Provider auth: rely on the provider CLI's logged-in session, not checked-in keys.
  • Agent auth: lives in the authenticated snapshot (Phase 3) — never a file you write or commit.
  • State holds only non-secret wiring (snapshot ids, scope, project, port, repo url/ref).

  • 绝不提交密钥或将其放入
    userData
    、配方JSON、注释、文档或状态文件中。
  • Git令牌: 从环境变量读取(
    GH_TOKEN
    /
    GITHUB_TOKEN
    ),fallback到
    gh auth token
    。仅通过提供商的临时
    --env
    参数传递给虚拟机。在虚拟机内部,使用
    GIT_ASKPASS
    助手和
    x-access-token
    (不要在克隆URL中直接使用令牌),并设置
    GIT_TERMINAL_PROMPT=0
    ,确保令牌缺失时快速失败而非挂起。当你在
    bash -lc
    下使用
    set -u
    编写助手时,需转义位置参数和令牌(
    \\$1
    \\$GH_TOKEN
    ),使其按字面量写入并在Git运行时解析——未转义的
    $1
    会因“未绑定变量”而中止,字面量
    $GH_TOKEN
    可避免真实令牌写入文件。克隆/获取完成后
    rm -f
    助手文件。
  • 提供商认证: 依赖提供商CLI的已登录会话,而非已签入的密钥。
  • Agent认证: 存储在认证快照中(第3阶段)——绝不存储在你编写或提交的文件中。
  • 状态文件仅存储非机密配置(快照ID、范围、项目、端口、仓库URL/引用)。

6. State file

6. 状态文件

A repo-local JSON file (e.g.
scripts/orca-vm/<provider>-state.json
) threads non-secret values between phases. Each script resolves values as env var → state → built-in fallback, and merges its outputs back. Phase 2 writes the base
snapshotId
; Phase 3 overwrites it with the authenticated snapshot; per-workspace
create
boots from
snapshotId
.
json
{
  "baseName": "orca-base",
  "snapshotId": "snap_authenticated_image_id",
  "authSourceSnapshotId": "snap_base_image_id",
  "scope": "<provider-scope>",
  "project": "<provider-project>",
  "port": 7331,
  "repoUrl": "https://host/org/repo.git",
  "repoRef": "main",
  "projectRoot": "/abs/path/on/remote/repo"
}

仓库本地的JSON文件(例如
scripts/orca-vm/<provider>-state.json
)用于在各阶段传递非机密值。每个脚本按环境变量 → 状态文件 → 内置默认值的顺序解析值,并将输出合并回状态文件。第2阶段写入基础
snapshotId
;第3阶段将其覆盖为认证后的快照ID;工作区
create
命令从
snapshotId
启动。
json
{
  "baseName": "orca-base",
  "snapshotId": "snap_authenticated_image_id",
  "authSourceSnapshotId": "snap_base_image_id",
  "scope": "<provider-scope>",
  "project": "<provider-project>",
  "port": 7331,
  "repoUrl": "https://host/org/repo.git",
  "repoRef": "main",
  "projectRoot": "/abs/path/on/remote/repo"
}

7. Script templates (provider-agnostic shapes)

7. 脚本模板(提供商无关格式)

Scaffold under
scripts/orca-vm/
. These are shapes — fill in the provider's real commands. All reserve stdout for the final JSON and log progress to stderr. Include a shared
json_value <key>
/
env_value <NAME>
reader (env → state → fallback) in each.
Where each script runs:
  • Local-side (
    create
    /
    suspend
    /
    resume
    /
    destroy
    + the base-snapshot/auth scripts the user invokes) runs on the user's desktop, so it must run on their OS. macOS/Linux:
    #!/usr/bin/env bash
    ,
    set -euo pipefail
    , quoted paths. Windows: a bare
    .sh
    won't run — scaffold
    .ps1
    /
    .cmd
    or require WSL/Git-Bash and point
    orca.yaml
    at the right launcher.
  • Remote-side (commands you
    exec
    inside the Linux VM) always runs in the VM's Linux shell, so bash is fine there regardless of the user's OS.
scripts/orca-vm/
目录下生成模板。这些是格式框架——需填充提供商的实际命令。所有脚本将标准输出保留给最终JSON,进度日志输出到标准错误。每个脚本包含共享的
json_value <key>
/
env_value <NAME>
读取函数(环境变量→状态文件→默认值)。
各脚本的运行位置:
  • 本地端
    create
    /
    suspend
    /
    resume
    /
    destroy
    + 用户调用的基础快照/认证脚本)运行在用户桌面,因此必须适配用户的操作系统。macOS/Linux:使用
    #!/usr/bin/env bash
    set -euo pipefail
    、带引号的路径。Windows:
    .sh
    文件无法运行——生成
    .ps1
    /
    .cmd
    脚本,或要求用户使用WSL/Git-Bash并在
    orca.yaml
    中指向正确的启动器。
  • 远程端(在Linux虚拟机内部执行的命令)始终运行在虚拟机的Linux Shell中,因此无论用户操作系统如何,使用bash即可。

7a. Base-snapshot (
<provider>-base-snapshot.sh
) — Phase 2

7a. 基础快照(
<provider>-base-snapshot.sh
)—— 阶段2

bash
#!/usr/bin/env bash
set -euo pipefail
bash
#!/usr/bin/env bash
set -euo pipefail

resolve base_name/repo_url/repo_ref/project_root/port/scope/project/timeout (env→state→fallback)

解析base_name/repo_url/repo_ref/project_root/port/scope/project/timeout(环境变量→状态文件→默认值)

resolve gh token: GH_TOKEN | GITHUB_TOKEN |
gh auth token

解析gh令牌:GH_TOKEN | GITHUB_TOKEN |
gh auth token

1. provision a sandbox (timeout/vcpus/published port/snapshot retention); trap: remove on error

1. 部署沙箱(超时/vcpus/发布端口/快照保留);捕获错误:出错时删除沙箱

2. remote exec (long timeout): install pkgs + gh + corepack/pnpm + agent CLI;

2. 远程执行(长超时):安装包 + gh + corepack/pnpm + Agent CLI;

clone with GIT_ASKPASS(token); write headless main-only build config;

使用GIT_ASKPASS(令牌)克隆仓库;编写仅无头主进程的构建配置;

dev setup; pnpm install; build CLI; build headless electron main; smoke-check tools

开发环境设置;pnpm install;构建CLI;构建无头Electron主进程;检查工具可用性

3. snapshot stopped sandbox; parse snapshot id (fail if unparseable)

3. 快照已停止的沙箱;解析快照ID(无法解析则失败)

4. merge { baseName, snapshotId, projectRoot, repoUrl, repoRef, port, scope, project } into state

4. 将{ baseName, snapshotId, projectRoot, repoUrl, repoRef, port, scope, project }合并到状态文件

print only the state JSON to stdout

仅将状态JSON输出到标准输出


Worked Vercel commands for this phase are in §7f. You run this script by hand (not via `orca.yaml`),
after exporting the first-run inputs the state file doesn't have yet — e.g. provider scope/project, the
repo URL/ref, and a git token (`GH_TOKEN`); later runs read them back from state.

此阶段的Vercel命令示例见第7f节。你需手动运行此脚本(不通过`orca.yaml`),先导出状态文件中尚未包含的首次运行输入——例如提供商范围/项目、仓库URL/引用和Git令牌(`GH_TOKEN`);后续运行从状态文件中读取这些信息。

7b. Auth (
<provider>-base-auth.sh
) — Phase 3

7b. 认证(
<provider>-base-auth.sh
)—— 阶段3

bash
#!/usr/bin/env bash
set -euo pipefail
bash
#!/usr/bin/env bash
set -euo pipefail

read source snapshot from state.snapshotId (fail if absent); auth_name="${base_name}-auth"

从state.snapshotId读取源快照(缺失则失败);auth_name="${base_name}-auth"

1. boot sandbox from source snapshot; trap: remove on error

1. 从源快照启动沙箱;捕获错误:出错时删除沙箱

2. INTERACTIVE/TTY remote exec: agent login — user completes URL/code. Headless VM: MUST use the

2. 交互式/TTY远程执行:Agent登录——用户完成URL/验证码操作。无头虚拟机:必须使用

device-auth flow (e.g.
codex login --device-auth
) — plain OAuth login binds a loopback callback

设备认证流程(例如
codex login --device-auth
)——普通OAuth登录绑定的环回回调

port the host can't reach and hangs. User runs this themselves (you have no interactive TTY); ask

端口主机无法访问,导致挂起。用户需自行运行此命令(你没有交互式TTY);请用户完成后告知你再继续。

them to report back when it's done before continuing.

3. 验证登录状态,未登录则拒绝快照。优先使用状态命令的退出码(大多数Agent CLI在未认证时返回非零退出码),而非字符串匹配。若必须使用grep,先合并stderr

3. verify login, then refuse to snapshot if not logged in. Prefer the status command's EXIT CODE (most

(
status 2>&1 | grep …
——许多Agent会在stderr中打印成功行)并匹配Agent的准确成功行;绝不要使用
grep -qi 'logged in'
,因为它也会匹配"未登录"。Codex示例见第7f节。

agent CLIs exit non-zero when unauthenticated) over string-matching. If you must grep, fold stderr

4. 生成快照;解析新ID

first (
status 2>&1 | grep …
— many agents print the success line there) and match the agent's exact

5. 将{ snapshotId:<new>, authSourceSnapshotId:<source> }合并到状态文件;删除认证沙箱

success line; never
grep -qi 'logged in'
, which also matches "not logged in". Codex example: §7f.

仅将状态JSON输出到标准输出

4. snapshot; parse new id

5. merge { snapshotId:<new>, authSourceSnapshotId:<source> } into state; remove auth sandbox

print only the state JSON to stdout

undefined
undefined

7c. Create (
<provider>-create.sh
) — per workspace

7c. 创建(
<provider>-create.sh
)—— 每个工作区

bash
#!/usr/bin/env bash
set -euo pipefail
bash
#!/usr/bin/env bash
set -euo pipefail

read authenticated snapshotId/scope/project/port/repo*/project_root (env→state→fallback)

解析已认证的snapshotId/scope/project/port/repo*/project_root(环境变量→状态文件→默认值)

fail clearly if snapshotId is missing (point back to Phases 2–3)

若snapshotId缺失则明确失败(提示返回第2-3阶段)

name = orca-${ORCA_VM_RECIPE_ID}-${ORCA_VM_INSTANCE_ID} (sanitized, length-capped)

name = orca-${ORCA_VM_RECIPE_ID}-${ORCA_VM_INSTANCE_ID}(已 sanitize、长度受限)

1. boot sandbox from snapshotId with a published port; capture the public URL → pairing address

1. 从snapshotId启动沙箱并发布端口;捕获公共URL → 配对地址

(an externally reachable wss:// URL); trap: remove sandbox on error

(可外部访问的wss:// URL);捕获错误:出错时删除沙箱

2. remote exec: ensure repo at desired commit; rebuild only if commit changed (cache marker)

2. 远程执行:确保仓库处于指定提交;仅在提交变更时重新构建(缓存标记)

3. remote exec: start orca serve in the background and read the recipe JSON it writes (see below)

3. 远程执行:在后台启动orca serve并读取其写入的配方JSON(见下文)

4. print serve's JSON to stdout, optionally enriched with userData:

4. 将serve的JSON输出到标准输出,可选择性添加userData:

{ schemaVersion:1, pairingCode, projectRoot, userData:{ provider, resourceId:name, snapshotId } }

{ schemaVersion:1, pairingCode, projectRoot, userData:{ provider, resourceId:name, snapshotId } }


**The exact `orca serve` invocation and its output (verified — do not improvise the flags).** Inside the
VM, run:

```bash
orca serve \
  --port "$PORT" \
  --project-root "$ABS_REPO_PATH_ON_REMOTE" \
  --pairing-address "$EXTERNAL_WSS_URL" \
  --recipe-json
Binary name: in a VM built from source (the Phase-2 flow), run it as
pnpm exec orca-dev serve …
from the repo root —
orca-dev
is the in-repo entrypoint and is what the §7f example uses. Plain
orca serve …
is the same command when the built CLI is installed on the VM's PATH. The flags/output are identical either way.
There is no
--host
flag
.
--project-root
must be an absolute directory on the remote. With
--recipe-json
the server stays running and prints exactly this single object to stdout, then keeps serving:
json
{ "schemaVersion": 1, "pairingCode": "<orca pairing URL>", "projectRoot": "<the --project-root you passed>" }
pairingCode
is the pairing URL, already pointing at whatever you passed as
--pairing-address
— so set
--pairing-address
to the externally reachable address and pass
pairingCode
through unchanged; never hand-rewrite it
. Because serve runs in the foreground and doesn't exit, redirect its stdout to a file and poll until that file parses as JSON (and bail if the process dies — dump its stderr log). Your
create
script then prints that JSON (optionally merging
userData
). Concrete pattern: §7f.

**`orca serve`的准确调用方式及其输出(已验证——不要自行修改参数)。** 在虚拟机内部运行:

```bash
orca serve \\
  --port "$PORT" \\
  --project-root "$ABS_REPO_PATH_ON_REMOTE" \\
  --pairing-address "$EXTERNAL_WSS_URL" \\
  --recipe-json
二进制名称: 在从源码构建的虚拟机中(第2阶段流程),从仓库根目录运行
pnpm exec orca-dev serve …
——
orca-dev
是仓库内的入口点,第7f节示例使用此命令。当构建后的CLI已安装在虚拟机PATH中时,
orca serve …
是相同的命令。两种方式的参数/输出完全一致。
没有**
--host
参数**。
--project-root
必须是远程端的绝对目录。使用
--recipe-json
参数时,服务器会保持运行并向标准输出打印以下单个对象,然后持续提供服务:
json
{ "schemaVersion": 1, "pairingCode": "<orca pairing URL>", "projectRoot": "<the --project-root you passed>" }
pairingCode
是配对URL,已指向你传递的
--pairing-address
——因此需将
--pairing-address
设置为可外部访问的地址,并直接传递
pairingCode
;绝不要手动修改
。由于serve在前台运行且不会退出,需将其标准输出重定向到文件并轮询,直到该文件可解析为JSON(若进程终止则退出——输出其stderr日志)。你的
create
脚本随后输出该JSON(可选择性合并
userData
)。具体模式见第7f节。

7d. Suspend / resume / destroy — per workspace

7d. 暂停 / 恢复 / 销毁 —— 每个工作区

bash
#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"                       # Orca passes lifecycle JSON on stdin
resource_id="$(node -e 'const d=JSON.parse(process.argv[1]); process.stdout.write(d.recipeResult?.userData?.resourceId ?? "")' "$payload")"
[ -n "$resource_id" ] || { echo "No resource id in lifecycle payload" >&2; exit 1; }
bash
#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"                       # Orca通过标准输入传递生命周期JSON
resource_id="$(node -e 'const d=JSON.parse(process.argv[1]); process.stdout.write(d.recipeResult?.userData?.resourceId ?? "")' "$payload")"
[ -n "$resource_id" ] || { echo "No resource id in lifecycle payload" >&2; exit 1; }

suspend: provider suspend "$resource_id"

暂停: provider suspend "$resource_id"

resume: provider resume "$resource_id"; then RE-EMIT fresh recipe JSON (pairing may change)

恢复: provider resume "$resource_id"; 然后重新输出最新的配方JSON(配对信息可能变更)

destroy: provider remove "$resource_id" (or set destroy: none in orca.yaml)

销毁: provider remove "$resource_id" (或在orca.yaml中设置destroy: none)

undefined
undefined

7e. State file — scaffold with scope/project/repo filled in and snapshot ids empty (§6).

7e. 状态文件 —— 生成时填充范围/项目/仓库信息,快照ID留空(第6节)。

7f. Worked example — Vercel Sandbox (all three phases)

7f. 示例 —— Vercel Sandbox(三个阶段)

A real, working shape (the Vercel surface is a CLI:
vercel sandbox create|exec|snapshot|remove
). Adapt names; verify flags against
vercel sandbox --help
for the user's CLI version before relying on them. These ground §7a (base snapshot) and §7b (auth), which are otherwise generic skeletons.
Phase 2 — base snapshot (§7a): provision → install tools + clone + headless build → snapshot.
bash
undefined
真实可用的格式(Vercel提供CLI:
vercel sandbox create|exec|snapshot|remove
)。调整名称;在依赖前需根据用户CLI版本验证参数(通过
vercel sandbox --help
)。这些示例是第7a节(基础快照)和第7b节(认证)的具体实现,否则它们只是通用框架。
阶段2 —— 基础快照(第7a节): 部署 → 安装工具 + 克隆 + 无头构建 → 快照。
bash
undefined

provision a fresh build sandbox (retain a couple of snapshots); trap-remove on error

部署全新的构建沙箱(保留几个快照);捕获错误:出错时删除沙箱

vercel sandbox create --name "$base" --runtime node24 --timeout 30m --vcpus 4 --publish-port "$port"
--snapshot-expiration 30d --keep-last-snapshots 2 "${vercel_args[@]}" >&2
vercel sandbox create --name "$base" --runtime node24 --timeout 30m --vcpus 4 --publish-port "$port" \ --snapshot-expiration 30d --keep-last-snapshots 2 "${vercel_args[@]}" >&2

remote build (long timeout): install pkgs+gh+pnpm+agent CLI, clone with GIT_ASKPASS (write the helper

远程构建(长超时):安装包+gh+pnpm+Agent CLI,使用GIT_ASKPASS克隆仓库(编写助手时

with LITERAL $1/$GH_TOKEN so they resolve at git-runtime, not write-time — see §5/§7f create — then

使用字面量\$1/\$GH_TOKEN,使其在Git运行时解析而非写入时解析——见第5/7f节创建脚本——然后

rm -f /tmp/askpass.sh
), write the headless main-only build config (drop the renderer), dev setup,

rm -f /tmp/askpass.sh
),编写仅无头主进程的构建配置(移除渲染进程),开发环境设置,

build CLI + headless main, smoke-check

构建CLI + 无头主进程,检查工具可用性

vercel sandbox exec "$base" "${vercel_args[@]}" --timeout 25m --env "GH_TOKEN=$gh_token" … -- bash -lc '…build…' >&2
vercel sandbox exec "$base" "${vercel_args[@]}" --timeout 25m --env "GH_TOKEN=$gh_token" … -- bash -lc '…build…' >&2

snapshot the STOPPED sandbox and parse the id from CLI output (fail if unparseable)

快照已停止的沙箱并从CLI输出中解析ID(无法解析则失败)

out="$(vercel sandbox snapshot "$base" --stop --expiration 30d "${vercel_args[@]}" 2>&1)"; printf '%s\n' "$out" >&2 snapshot_id="$(printf '%s\n' "$out" | sed -nE 's/.(snap_[A-Za-z0-9]+)./\1/p' | tail -1)"
out="$(vercel sandbox snapshot "$base" --stop --expiration 30d "${vercel_args[@]}" 2>&1)"; printf '%s
' "$out" >&2 snapshot_id="$(printf '%s
' "$out" | sed -nE 's/.(snap_[A-Za-z0-9]+)./\1/p' | tail -1)"

merge { baseName, snapshotId, scope, project, port, repoUrl, repoRef, projectRoot } into state; print state JSON

将{ baseName, snapshotId, scope, project, port, repoUrl, repoRef, projectRoot }合并到状态文件;输出状态JSON


**Phase 3 — agent-auth snapshot (§7b):** boot the base, log the agent in interactively, re-snapshot.
(`codex` below is an example — substitute the user's chosen agent's login/status verbs, e.g. `claude`.)

```bash
vercel sandbox create --name "$auth" --snapshot "$snapshot_id" --timeout 30m --publish-port "$port" "${vercel_args[@]}" >&2

**阶段3 —— Agent认证快照(第7b节):** 启动基础快照,交互式登录Agent,重新生成快照。(以下`codex`是示例——替换为用户选择的Agent的登录/状态命令,例如`claude`。)

```bash
vercel sandbox create --name "$auth" --snapshot "$snapshot_id" --timeout 30m --publish-port "$port" "${vercel_args[@]}" >&2

INTERACTIVE — the USER runs this in their own terminal (you have no interactive TTY) and completes the

交互式操作——用户需在自己的终端中运行此命令(你没有交互式TTY)并在

URL/code on the HOST. --device-auth is MANDATORY on a headless VM: plain
codex login
binds a loopback

主机上完成URL/验证码操作。在无头虚拟机上必须使用--device-auth:普通
codex login
绑定的环回

callback port the host browser can't reach and hangs. Ask the user to report back when login finishes.

回调端口主机浏览器无法访问,导致挂起。请用户完成后告知你。

vercel sandbox exec --interactive --tty "$auth" "${vercel_args[@]}" -- bash -lc 'codex login --device-auth'
vercel sandbox exec --interactive --tty "$auth" "${vercel_args[@]}" -- bash -lc 'codex login --device-auth'

refuse to snapshot an unauthenticated VM — fold stderr, match codex's exact success line (§4)

拒绝快照未认证的虚拟机——合并stderr,匹配codex的准确成功行(第4节)

vercel sandbox exec "$auth" "${vercel_args[@]}" --timeout 30s -- bash -lc 'codex login status 2>&1' | grep -Eqi 'Logged in using ChatGPT|Logged in via device'
|| { echo "agent not logged in; not snapshotting" >&2; exit 1; } out="$(vercel sandbox snapshot "$auth" --stop --expiration 30d "${vercel_args[@]}" 2>&1)"; printf '%s\n' "$out" >&2 new_id="$(printf '%s\n' "$out" | sed -nE 's/.(snap_[A-Za-z0-9]+)./\1/p' | tail -1)"
vercel sandbox exec "$auth" "${vercel_args[@]}" --timeout 30s -- bash -lc 'codex login status 2>&1' | grep -Eqi 'Logged in using ChatGPT|Logged in via device' \ || { echo "agent not logged in; not snapshotting" >&2; exit 1; } out="$(vercel sandbox snapshot "$auth" --stop --expiration 30d "${vercel_args[@]}" 2>&1)"; printf '%s
' "$out" >&2 new_id="$(printf '%s
' "$out" | sed -nE 's/.(snap_[A-Za-z0-9]+)./\1/p' | tail -1)"

overwrite state.snapshotId = new_id, record authSourceSnapshotId = snapshot_id; remove the auth sandbox

将state.snapshotId覆盖为new_id,记录authSourceSnapshotId = snapshot_id;删除认证沙箱


**Per-workspace `create`** (the fast path):

```bash
#!/usr/bin/env bash
set -euo pipefail

**工作区`create`脚本**(快速路径):

```bash
#!/usr/bin/env bash
set -euo pipefail

resolve from env→state→fallback: snapshot_id, scope, project, port, repo_url, repo_ref, project_root

从环境变量→状态文件→默认值解析:snapshot_id, scope, project, port, repo_url, repo_ref, project_root

vercel_args=(); [ -n "$scope" ] && vercel_args+=(--scope "$scope"); [ -n "$project" ] && vercel_args+=(--project "$project") [ -n "$snapshot_id" ] || { echo "snapshotId missing — run Phases 2–3 first" >&2; exit 1; } gh_token="${GH_TOKEN:-${GITHUB_TOKEN:-$(command -v gh >/dev/null 2>&1 && gh auth token 2>/dev/null || true)}}" name="orca-${ORCA_VM_RECIPE_ID:-vercel-sandbox}-${ORCA_VM_INSTANCE_ID:-$(date +%s)}" # sanitize+cap to 63 chars
vercel_args=(); [ -n "$scope" ] && vercel_args+=(--scope "$scope"); [ -n "$project" ] && vercel_args+=(--project "$project") [ -n "$snapshot_id" ] || { echo "snapshotId missing — run Phases 2–3 first" >&2; exit 1; } gh_token="${GH_TOKEN:-${GITHUB_TOKEN:-$(command -v gh >/dev/null 2>&1 && gh auth token 2>/dev/null || true)}}" name="orca-${ORCA_VM_RECIPE_ID:-vercel-sandbox}-${ORCA_VM_INSTANCE_ID:-$(date +%s)}" # sanitize并限制为63字符

Arm cleanup BEFORE create so a failing create can't leak a half-built paid sandbox.

在创建前设置清理操作,避免创建失败时留下半构建的付费沙箱。

cleanup_on_error() { [ "$?" -ne 0 ] && vercel sandbox remove "$name" "${vercel_args[@]}" >/dev/null 2>&1 || true; } trap cleanup_on_error EXIT
cleanup_on_error() { [ "$?" -ne 0 ] && vercel sandbox remove "$name" "${vercel_args[@]}" >/dev/null 2>&1 || true; } trap cleanup_on_error EXIT

1. boot from the authenticated snapshot, publish the serve port

1. 从已认证的快照启动沙箱,发布serve端口

create_output="$(vercel sandbox create --name "$name" --snapshot "$snapshot_id"
--timeout 30m --publish-port "$port" "${vercel_args[@]}" 2>&1)"; printf '%s\n' "$create_output" >&2
create_output="$(vercel sandbox create --name "$name" --snapshot "$snapshot_id" \ --timeout 30m --publish-port "$port" "${vercel_args[@]}" 2>&1)"; printf '%s
' "$create_output" >&2

Vercel prints the published https URL; derive the external wss:// pairing address from it

Vercel会打印发布的https URL;从中导出外部wss://配对地址

public_url="$(printf '%s\n' "$create_output" | sed -nE 's#.(https://[^[:space:]]+.vercel.run).#\1#p' | head -1)" [ -n "$public_url" ] || { echo "no published URL in create output" >&2; exit 1; } pairing_ws="${public_url/https:///wss://}"
public_url="$(printf '%s
' "$create_output" | sed -nE 's#.(https://[^[:space:]]+\.vercel\.run).#\1#p' | head -1)" [ -n "$public_url" ] || { echo "no published URL in create output" >&2; exit 1; } pairing_ws="${public_url/https:\/\//wss://}"

2. (remote) ensure the repo is at the right commit; rebuild only if the commit changed (cache marker)

2. (远程)确保仓库处于指定提交;仅在提交变更时重新构建(缓存标记)

vercel sandbox exec "$name" "${vercel_args[@]}" --timeout 20m
--env "GH_TOKEN=$gh_token" --env "ORCA_PROJECT_ROOT=$project_root"
--env "ORCA_REPO_URL=$repo_url" --env "ORCA_REPO_REF=$repo_ref"
-- bash -lc 'set -euo pipefail; cd "$ORCA_PROJECT_ROOT";
# Re-establish git auth for the private-repo fetch (why + full rationale: §5); else it hangs on a prompt. # Load-bearing escaping: $1 and $GH_TOKEN must land LITERALLY and resolve at git-runtime. Test after # any edit here — reformatting the nested printf/node quoting silently breaks the fetch or leaks the token. if [ -n "${GH_TOKEN:-}" ]; then
printf "%s\n" "#!/usr/bin/env bash" "case "$1" in Username) echo x-access-token;; Password) echo "$GH_TOKEN";; esac" > /tmp/askpass.sh;
chmod 700 /tmp/askpass.sh; export GIT_ASKPASS=/tmp/askpass.sh GIT_TERMINAL_PROMPT=0; fi;
git fetch origin "$ORCA_REPO_REF";
git checkout -B "$ORCA_REPO_REF" FETCH_HEAD;
rm -f /tmp/askpass.sh;
c="$(git rev-parse HEAD)"; [ -f .orca-built ] && [ "$(cat .orca-built)" = "$c" ] || {
pnpm install --prefer-offline && pnpm run build:cli &&
node config/scripts/run-electron-vite-build.mjs --config config/electron-vite.vm-serve.config.ts &&
printf "%s" "$c" > .orca-built; }' >&2
vercel sandbox exec "$name" "${vercel_args[@]}" --timeout 20m \ --env "GH_TOKEN=$gh_token" --env "ORCA_PROJECT_ROOT=$project_root" \ --env "ORCA_REPO_URL=$repo_url" --env "ORCA_REPO_REF=$repo_ref" \ -- bash -lc 'set -euo pipefail; cd "$ORCA_PROJECT_ROOT"; \ # 为私有仓库获取重新建立Git认证(原因及完整说明:第5节);否则会挂起在提示上。 # 必须转义:\$1和\$GH_TOKEN必须按字面量写入并在Git运行时解析。修改后需测试——嵌套printf/node引用的格式变更会静默破坏获取或泄露令牌。 if [ -n "${GH_TOKEN:-}" ]; then \ printf "%s
" "#!/usr/bin/env bash" "case \"\$1\" in Username) echo x-access-token;; Password) echo \"\$GH_TOKEN\";; esac" > /tmp/askpass.sh; \ chmod 700 /tmp/askpass.sh; export GIT_ASKPASS=/tmp/askpass.sh GIT_TERMINAL_PROMPT=0; fi; \ git fetch origin "$ORCA_REPO_REF"; \ git checkout -B "$ORCA_REPO_REF" FETCH_HEAD; \ rm -f /tmp/askpass.sh; \ c="$(git rev-parse HEAD)"; [ -f .orca-built ] && [ "$(cat .orca-built)" = "$c" ] || { \ pnpm install --prefer-offline && pnpm run build:cli && \ node config/scripts/run-electron-vite-build.mjs --config config/electron-vite.vm-serve.config.ts && \ printf "%s" "$c" > .orca-built; }' >&2

3. (remote) start orca serve in the background, writing recipe JSON to a file; poll until it parses

3. (远程)在后台启动orca serve,将配方JSON写入文件;轮询直到可解析

recipe_json="$(vercel sandbox exec "$name" "${vercel_args[@]}" --timeout 60s
--env "ORCA_PORT=$port" --env "ORCA_PROJECT_ROOT=$project_root" --env "ORCA_PAIRING_ADDRESS=$pairing_ws"
-- bash -lc 'set -euo pipefail; cd "$ORCA_PROJECT_ROOT"; rm -f /tmp/orca-recipe.json /tmp/orca-serve.log;
nohup pnpm exec orca-dev serve --port "$ORCA_PORT" --project-root "$ORCA_PROJECT_ROOT"
--pairing-address "$ORCA_PAIRING_ADDRESS" --recipe-json >/tmp/orca-recipe.json 2>/tmp/orca-serve.log </dev/null &
pid=$!; for _ in $(seq 1 80); do
node -e "JSON.parse(require("node:fs").readFileSync("/tmp/orca-recipe.json","utf8"))" >/dev/null 2>&1 && { cat /tmp/orca-recipe.json; exit 0; };
kill -0 "$pid" 2>/dev/null || { cat /tmp/orca-serve.log >&2; exit 1; }; sleep 0.25;
done; cat /tmp/orca-serve.log >&2; echo "serve recipe JSON timed out" >&2; exit 1')"
recipe_json="$(vercel sandbox exec "$name" "${vercel_args[@]}" --timeout 60s \ --env "ORCA_PORT=$port" --env "ORCA_PROJECT_ROOT=$project_root" --env "ORCA_PAIRING_ADDRESS=$pairing_ws" \ -- bash -lc 'set -euo pipefail; cd "$ORCA_PROJECT_ROOT"; rm -f /tmp/orca-recipe.json /tmp/orca-serve.log; \ nohup pnpm exec orca-dev serve --port "$ORCA_PORT" --project-root "$ORCA_PROJECT_ROOT" \ --pairing-address "$ORCA_PAIRING_ADDRESS" --recipe-json >/tmp/orca-recipe.json 2>/tmp/orca-serve.log </dev/null & \ pid=$!; for _ in $(seq 1 80); do \ node -e "JSON.parse(require(\"node:fs\").readFileSync(\"/tmp/orca-recipe.json\",\"utf8\"))" >/dev/null 2>&1 && { cat /tmp/orca-recipe.json; exit 0; }; \ kill -0 "$pid" 2>/dev/null || { cat /tmp/orca-serve.log >&2; exit 1; }; sleep 0.25; \ done; cat /tmp/orca-serve.log >&2; echo "serve recipe JSON timed out" >&2; exit 1')"

4. print serve's JSON enriched with userData (single object on stdout)

4. 输出包含userData的serve JSON(标准输出仅输出单个对象)

node -e 'const p=JSON.parse(process.argv[1]); console.log(JSON.stringify({...p, schemaVersion:1, userData:{...p.userData, provider:"vercel-sandbox", resourceId:process.argv[2], snapshotId:process.argv[3]}}))'
"$recipe_json" "$name" "$snapshot_id" trap - EXIT

`suspend`/`resume`/`destroy` use `vercel sandbox stop|...|remove "$resource_id"` reading
`userData.resourceId` from stdin (§7d). This is the **Orca-server** connection mode (the recipe emits a
pairing URL). If the user chose **SSH** in the §1 interview, use §7g instead.
node -e 'const p=JSON.parse(process.argv[1]); console.log(JSON.stringify({...p, schemaVersion:1, userData:{...p.userData, provider:"vercel-sandbox", resourceId:process.argv[2], snapshotId:process.argv[3]}}))' \ "$recipe_json" "$name" "$snapshot_id" trap - EXIT

`suspend`/`resume`/`destroy`脚本使用`vercel sandbox stop|...|remove "$resource_id"`,从标准输入读取`userData.resourceId`(第7d节)。这是**Orca-server**连接模式(配方生成配对URL)。如果用户在第1节沟通中选择**SSH**模式,请使用第7g节的脚本。

7g. Worked example — existing SSH host (SSH connection mode)

7g. 示例 —— 现有SSH主机(SSH连接模式)

SSH mode is fundamentally different from §7c/§7f, not a relabeling of them:
  • create
    does NOT run
    orca serve
    and does NOT emit a
    pairingCode
    .
    Orca itself connects to the host over its SSH relay, brings up the git + filesystem providers, and imports the repo. The script's only job is to make the host ready and print SSH connection details Orca will dial.
  • The result uses a
    connection
    block with
    type: "ssh"
    and a
    target
    , not the flat
    pairingCode
    /
    projectRoot
    shape. Exact shape (Orca rejects anything else):
json
{
  "schemaVersion": 1,
  "connection": {
    "type": "ssh",
    "projectRoot": "/abs/path/to/repo/on/host",
    "target": {
      "label": "my-box",
      "host": "192.0.2.10",
      "port": 22,
      "username": "ubuntu",
      "identityFile": "~/.ssh/id_ed25519",
      "jumpHost": "bastion.example.com",
      "proxyCommand": "cloudflared access ssh --hostname %h",
      "relayGracePeriodSeconds": 0,
      "portForwards": []
    }
  }
}
label
,
host
,
port
,
username
are required; the rest are optional — omit any you don't need.
Networking → which
target
fields to set
(how your desktop reaches the box — there is no
orca serve
URL in SSH mode):
  • Public IP / DNS, or a Tailscale/VPN address →
    host
    ; SSH port →
    port
    (usually 22).
  • Key auth →
    identityFile
    (add
    identitiesOnly: true
    if the agent has many keys).
  • Through a bastion →
    jumpHost
    (a
    user@host
    ProxyJump) or a full
    proxyCommand
    (e.g. an access proxy). Use one, not both.
  • A service port the workspace needs → add entries to
    portForwards
    .
  • relayGracePeriodSeconds
    (optional): how long Orca keeps the SSH relay alive after the workspace detaches before tearing it down;
    0
    = tear down immediately. Leave it off unless the user wants a reconnect grace window.
Toolchain & agent auth on a persistent (no-snapshot) host — do this ONCE, by hand, before wiring the recipe (there's no base image to bake; the host is the base). Run the §7f Phase-2 install steps and the §7f Phase-3
<agent> login --device-auth
directly over SSH on the host (interactive, e.g.
ssh -t user@host '<agent> login --device-auth'
). After that the host stays ready across workspaces.
bash
#!/usr/bin/env bash
set -euo pipefail
SSH模式与第7c/7f节完全不同,并非简单重命名:
  • create
    命令不运行
    orca serve
    ,也不生成
    pairingCode
    Orca通过SSH中继连接到主机,启动Git和文件系统提供商,并导入仓库。脚本的唯一任务是确保主机就绪并打印Orca将连接的SSH详细信息
  • 结果使用
    connection
    块,包含
    type: "ssh"
    target
    而非扁平的
    pairingCode
    /
    projectRoot
    格式。准确格式(Orca会拒绝其他格式):
json
{
  "schemaVersion": 1,
  "connection": {
    "type": "ssh",
    "projectRoot": "/abs/path/to/repo/on/host",
    "target": {
      "label": "my-box",
      "host": "192.0.2.10",
      "port": 22,
      "username": "ubuntu",
      "identityFile": "~/.ssh/id_ed25519",
      "jumpHost": "bastion.example.com",
      "proxyCommand": "cloudflared access ssh --hostname %h",
      "relayGracePeriodSeconds": 0,
      "portForwards": []
    }
  }
}
label
host
port
username
是必填项;其余为可选项——不需要的可省略。
网络设置 → 需设置哪些
target
字段
(你的桌面如何连接到主机——SSH模式下没有
orca serve
URL):
  • 公网IP / DNS,或Tailscale/VPN地址 →
    host
    ;SSH端口 →
    port
    (通常为22)。
  • 密钥认证 →
    identityFile
    (如果Agent有多个密钥,添加
    identitiesOnly: true
    )。
  • 通过堡垒机连接 →
    jumpHost
    user@host
    格式的ProxyJump)完整的
    proxyCommand
    (例如访问代理)。二选一,不要同时使用。
  • 工作区需要的服务端口 → 添加到
    portForwards
    数组中。
  • relayGracePeriodSeconds
    (可选):Orca在工作区断开连接后,保持SSH中继存活的时间;
    0
    = 立即销毁。除非用户需要重连宽限期,否则留空。
持久主机(无快照)上的工具链与Agent认证 —— 在配置配方前需手动执行一次(没有基础镜像可构建;主机本身就是基础)。直接通过SSH在主机上运行第7f节的阶段2安装步骤和阶段3的
<agent> login --device-auth
命令(交互式,例如
ssh -t user@host '<agent> login --device-auth'
)。之后主机即可为所有工作区就绪。
bash
#!/usr/bin/env bash
set -euo pipefail

resolve from env→state→fallback (default unset optionals to ""): ssh_username, host,

从环境变量→状态文件→默认值解析(可选默认值设为""):ssh_username, host,

ssh_port (default 22), identity_file, jump_host, proxy_command, project_root, repo_url, repo_ref

ssh_port(默认22), identity_file, jump_host, proxy_command, project_root, repo_url, repo_ref

: "${identity_file:=}"; : "${jump_host:=}"; : "${proxy_command:=}" # avoid set -u aborts on optionals gh_token="${GH_TOKEN:-${GITHUB_TOKEN:-$(command -v gh >/dev/null 2>&1 && gh auth token 2>/dev/null || true)}}" ssh_target="${ssh_username}@${host}" ssh_opts=(-p "$ssh_port"); [ -n "$identity_file" ] && ssh_opts+=(-i "$identity_file")
: "${identity_file:=}"; : "${jump_host:=}"; : "${proxy_command:=}" # 避免set -u在可选变量上中止 gh_token="${GH_TOKEN:-${GITHUB_TOKEN:-$(command -v gh >/dev/null 2>&1 && gh auth token 2>/dev/null || true)}}" ssh_target="${ssh_username}@${host}" ssh_opts=(-p "$ssh_port"); [ -n "$identity_file" ] && ssh_opts+=(-i "$identity_file")

Why: a fresh host's key isn't in known_hosts; a StrictHostKeyChecking prompt would HANG a

原因:新主机的密钥不在known_hosts中;StrictHostKeyChecking提示会导致

non-interactive create. Pre-add the key (or set the option) so it can't block.

非交互式create命令挂起。预先添加密钥(或设置选项)以避免阻塞。

ssh-keyscan -p "$ssh_port" "$host" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true
ssh-keyscan -p "$ssh_port" "$host" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true

1. ensure the repo is present and at the right commit on the host (NO orca serve here)

1. 确保主机上存在仓库且处于指定提交(此处不运行orca serve)

ssh "${ssh_opts[@]}" "$ssh_target"
"GH_TOKEN='$gh_token' GIT_TERMINAL_PROMPT=0 bash -lc ' set -euo pipefail [ -d "$project_root/.git" ] || git clone "$repo_url" "$project_root" cd "$project_root" && git fetch origin "$repo_ref" && git checkout -B "$repo_ref" FETCH_HEAD '" >&2
ssh "${ssh_opts[@]}" "$ssh_target" \ "GH_TOKEN='$gh_token' GIT_TERMINAL_PROMPT=0 bash -lc ' set -euo pipefail [ -d \"$project_root/.git\" ] || git clone \"$repo_url\" \"$project_root\" cd \"$project_root\" && git fetch origin \"$repo_ref\" && git checkout -B \"$repo_ref\" FETCH_HEAD '" >&2

2. print the SSH connection block (NO pairingCode, NO orca serve). host/port/username tell Orca's

2. 打印SSH连接块(无pairingCode,无orca serve)。host/port/username告知Orca的

relay how to dial in; identityFile/jumpHost/proxyCommand/portForwards are emitted when set.

中继如何连接;identityFile/jumpHost/proxyCommand/portForwards在设置时输出。

node -e 'const [host,port,user,idf,jh,pc,root]=process.argv.slice(1); const target={ label:"per-workspace-host", host, port:Number(port), username:user }; if(idf) target.identityFile=idf; if(jh) target.jumpHost=jh; if(pc) target.proxyCommand=pc; // add target.portForwards=[...] here if the workspace needs forwarded service ports console.log(JSON.stringify({ schemaVersion:1, connection:{ type:"ssh", projectRoot:root, target } }))'
"$host" "$ssh_port" "$ssh_username" "$identity_file" "$jump_host" "$proxy_command" "$project_root"

`suspend`/`resume`/`destroy`: on a persistent host there's usually nothing to tear down — set
`destroy: none` and omit suspend/resume. (Orca still disconnects/reconnects its own SSH relay on
sleep/wake/delete — that's separate from these scripts.)

If the SSH host is instead an **ephemeral/snapshot-capable VM** (your hypervisor, or a cloud VM with
image support), keep the §7f Phase-2/3 base-image model for provisioning, but still emit the
`connection.type:"ssh"` block above instead of starting `orca serve`.
node -e 'const [host,port,user,idf,jh,pc,root]=process.argv.slice(1); const target={ label:"per-workspace-host", host, port:Number(port), username:user }; if(idf) target.identityFile=idf; if(jh) target.jumpHost=jh; if(pc) target.proxyCommand=pc; // 如果工作区需要转发服务端口,在此添加target.portForwards=[...] console.log(JSON.stringify({ schemaVersion:1, connection:{ type:"ssh", projectRoot:root, target } }))' \ "$host" "$ssh_port" "$ssh_username" "$identity_file" "$jump_host" "$proxy_command" "$project_root"

`suspend`/`resume`/`destroy`:在持久主机上通常无需销毁——设置`destroy: none`并省略暂停/恢复脚本。(Orca仍会在休眠/唤醒/删除时断开/重新连接其SSH中继——这与这些脚本无关。)

如果SSH主机是**临时/支持快照的虚拟机**(你的虚拟机管理程序,或支持镜像的云虚拟机),请保留第7f节的阶段2/3基础镜像模型进行部署,但仍输出上述`connection.type:"ssh"`块,而非启动`orca serve`。

7h. Worked example — local Docker SSH (SSH connection mode)

7h. 示例 —— 本地Docker SSH(SSH连接模式)

Local Docker can model an ephemeral SSH VM without cloud cost: build a base image with
sshd
, tools, repo prerequisites, and the agent CLI; run an interactive auth container once; then
docker commit
that container as the authenticated image used by per-workspace
create
.
Key points:
  • Publish container SSH to a random localhost port (
    -p 127.0.0.1::22
    ) and emit
    connection.type:"ssh"
    with
    host:"127.0.0.1"
    , that port,
    username
    ,
    identityFile
    , and
    identitiesOnly:true
    .
  • Generate a repo-local SSH key if needed, but gitignore the private/public key files.
  • Bake SSH host keys into the base image (
    ssh-keygen -A
    at build time; at runtime only generate if absent). Ephemeral containers all present the same host key, so
    known_hosts
    on
    127.0.0.1
    doesn't churn as the published port rotates across workspaces (otherwise every container's freshly generated key collides on
    localhost
    and trips host-key-changed warnings).
  • The auth image is the Docker equivalent of Phase 3: the user runs the agent login inside the container (you can't drive it — you have no interactive TTY), configures proxy env/config, approves hooks, and you commit once they report it's done. On a headless container use the device-auth flow (§4). Verify login before committing — exit code, or fold stderr and match the exact success line (§4).
  • Do not bind-mount or copy the host's full agent home into the image. Let each container have writable agent state; only the committed auth image should carry reusable authenticated state.
  • If committing from an interactive shell, force the runtime entrypoint back to
    sshd
    :
    docker commit --change='ENTRYPOINT ["/usr/local/bin/orca-docker-ssh-entrypoint"]' …
    .
  • destroy
    should read
    recipeResult.userData.resourceId
    and run
    docker rm -f "$resource_id"
    .
Validation before wiring/live use:
bash
docker image inspect "$auth_image" --format '{{json .Config.Entrypoint}}'
docker run -d --name "$name" -p 127.0.0.1::22 -e "ORCA_SSH_PUBLIC_KEY=$pubkey" "$auth_image"
docker ps -a --filter "name=$name"
docker logs "$name"
ssh -i "$key" -p "$port" -o IdentitiesOnly=yes user@127.0.0.1 'codex --version'
If the container exits immediately, inspect logs before the cleanup trap removes it; a committed interactive image with
ENTRYPOINT ["bash"]
is a common cause.
Also confirm the host key is stable across containers: the SSH
ssh -i … 127.0.0.1
dial should not trigger a host-key-changed warning when a second container reuses the port. If it does, the host keys weren't baked into the base image (see the
ssh-keygen -A
point above).
本地Docker可模拟临时SSH虚拟机而无需云成本:构建包含
sshd
、工具、仓库前置条件和Agent CLI的基础镜像;运行一次交互式认证容器;然后
docker commit
该容器作为工作区
create
命令使用的认证镜像。
关键点:
  • 将容器SSH端口发布到本地随机端口(
    -p 127.0.0.1::22
    )并输出
    connection.type:"ssh"
    块,包含
    host:"127.0.0.1"
    、该端口、
    username
    identityFile
    identitiesOnly:true
  • 若需要,生成仓库本地SSH密钥,但将私钥/公钥文件添加到.gitignore。
  • 将SSH主机密钥嵌入基础镜像(在构建时运行
    ssh-keygen -A
    ;仅在运行时缺失时生成)。临时容器都使用相同的主机密钥,因此当发布端口在工作区之间轮换时,
    127.0.0.1
    known_hosts
    不会频繁变更(否则每个容器新生成的密钥会在
    localhost
    上冲突,触发主机密钥变更警告)。
  • 认证镜像是第3阶段的Docker等效物:用户需在容器内部运行Agent登录(你无法驱动此操作——没有交互式TTY),配置代理环境/设置,批准钩子,用户完成后你再提交。在无头容器上使用设备认证流程(第4节)。提交前验证登录状态——使用退出码,或合并stderr并匹配准确成功行(第4节)。
  • 不要将主机的完整Agent目录绑定挂载或复制到镜像中。让每个容器拥有可写的Agent状态;只有提交的认证镜像应携带可复用的认证状态。
  • 如果从交互式Shell提交,强制将运行时入口点重置为
    sshd
    docker commit --change='ENTRYPOINT ["/usr/local/bin/orca-docker-ssh-entrypoint"]' …
  • destroy
    脚本应读取
    recipeResult.userData.resourceId
    并运行
    docker rm -f "$resource_id"
配置/在线使用前验证:
bash
docker image inspect "$auth_image" --format '{{json .Config.Entrypoint}}'
docker run -d --name "$name" -p 127.0.0.1::22 -e "ORCA_SSH_PUBLIC_KEY=$pubkey" "$auth_image"
docker ps -a --filter "name=$name"
docker logs "$name"
ssh -i "$key" -p "$port" -o IdentitiesOnly=yes user@127.0.0.1 'codex --version'
如果容器立即退出,在清理陷阱删除它之前检查日志;常见原因是从交互式Shell提交的镜像将
ENTRYPOINT ["bash"]
设置为入口点。
还要确认主机密钥稳定:当第二个容器复用端口时,
ssh -i … 127.0.0.1
连接不应触发主机密钥变更警告。如果触发,说明主机密钥未嵌入基础镜像(见上述
ssh-keygen -A
要点)。

7i. Windows local-side scripts

7i. Windows本地端脚本

The local-side scripts run on the user's desktop. On Windows, a bare
.sh
won't execute. Either require WSL/Git-Bash (and point
orca.yaml
at e.g.
bash ./scripts/orca-vm/<name>.sh
via a
.cmd
launcher), or scaffold PowerShell equivalents. Minimal PowerShell shape:
powershell
#requires -Version 5
$ErrorActionPreference = 'Stop'
本地端脚本运行在用户桌面。在Windows上,纯
.sh
文件无法执行。要么要求用户使用WSL/Git-Bash(并在
orca.yaml
中指向例如
bash ./scripts/orca-vm/<name>.sh
,通过
.cmd
启动器),要么生成PowerShell等效脚本。最小PowerShell格式:
powershell
#requires -Version 5
$ErrorActionPreference = 'Stop'

resolve env→state→fallback; run the provider CLI / ssh the same way;

解析环境变量→状态文件→默认值;以相同方式运行提供商CLI / ssh;

capture provider output; build the result object for the chosen mode and write ONE line of JSON to stdout.

捕获提供商输出;为所选模式构建结果对象并将一行JSON写入标准输出。

Orca-server mode: @{ schemaVersion=1; pairingCode=$pairingCode; projectRoot=$projectRoot; userData=@{...} }

Orca-server模式:@{ schemaVersion=1; pairingCode=$pairingCode; projectRoot=$projectRoot; userData=@{...} }

SSH mode: @{ schemaVersion=1; connection=@{ type="ssh"; projectRoot=$projectRoot;

SSH模式: @{ schemaVersion=1; connection=@{ type="ssh"; projectRoot=$projectRoot;

target=@{ label=$label; host=$host; port=$port; username=$user } } } (see §7g/§7h)

target=@{ label=$label; host=$host; port=$port; username=$user } } } (见第7g/7h节)

($result | ConvertTo-Json -Compress -Depth 6)
($result | ConvertTo-Json -Compress -Depth 6)

progress/errors → Write-Error / the error stream, never stdout.

进度/错误 → 写入Write-Error / 错误流,绝不要写入标准输出。


The remote-side commands you run *inside* the Linux VM stay bash regardless of the desktop OS.

---

在Linux虚拟机内部运行的远程端命令,无论桌面操作系统如何,都使用bash。

---

8. Per-workspace recipe contract (the fast path)

8. 工作区配方协议(快速路径)

Once the authenticated snapshot exists, this runs on every workspace create. Define recipes in
orca.yaml
:
yaml
environmentRecipes:
  - id: cloud-sandbox
    name: Cloud Sandbox
    create: ./scripts/orca-vm/cloud-sandbox-create.sh
    suspend: ./scripts/orca-vm/cloud-sandbox-suspend.sh
    resume: ./scripts/orca-vm/cloud-sandbox-resume.sh
    destroy: ./scripts/orca-vm/cloud-sandbox-destroy.sh
create
runs locally from the repo root and prints one JSON object to stdout. Its shape depends on the connection mode chosen in §1:
Orca-server mode — boot the env, start
orca serve
in it, and print serve's result:
json
{
  "schemaVersion": 1,
  "pairingCode": "orca-pairing-code-or-url",
  "projectRoot": "/absolute/path/to/repo/on/remote",
  "userData": { "provider": "example", "resourceId": "provider-resource-id" }
}
Here
pairingCode
(from
orca serve --recipe-json
) and
projectRoot
are required;
schemaVersion
(
1
) and
userData
are optional.
SSH mode — do not run
orca serve
; print the
connection.type:"ssh"
block instead (full shape + worked script in §7g).
pairingCode
is not used in SSH mode.
Lifecycle hooks (all run locally):
  • create
    : required. Prints recipe result JSON.
  • suspend
    : optional. Sleep; reads lifecycle payload on stdin.
  • resume
    : optional. Wake; reads payload on stdin and prints fresh recipe JSON (pairing may change).
  • destroy
    : optional unless
    destroy: none
    . Delete/cleanup; reads payload on stdin.
Start Orca remotely with
orca serve --port "$PORT" --project-root "$ABS_ROOT" --pairing-address "$EXTERNAL_WSS_URL" --recipe-json
(exact flags + output in §7c). Set
--pairing-address
to the externally reachable address so the emitted
pairingCode
is reachable; tunneling/port mapping is the script's job.
Backward compatibility:
command
create
,
cleanup
destroy
,
cleanup: none
destroy: none
. Prefer the lifecycle names.

一旦认证快照存在,每次创建工作区时都会执行此流程。在
orca.yaml
中定义配方:
yaml
environmentRecipes:
  - id: cloud-sandbox
    name: Cloud Sandbox
    create: ./scripts/orca-vm/cloud-sandbox-create.sh
    suspend: ./scripts/orca-vm/cloud-sandbox-suspend.sh
    resume: ./scripts/orca-vm/cloud-sandbox-resume.sh
    destroy: ./scripts/orca-vm/cloud-sandbox-destroy.sh
create
命令从仓库根目录本地运行,并向标准输出打印一个JSON对象。其格式取决于第1节中选择的连接模式:
Orca-server模式 —— 启动环境,在其中运行
orca serve
,并输出serve的结果:
json
{
  "schemaVersion": 1,
  "pairingCode": "orca-pairing-code-or-url",
  "projectRoot": "/absolute/path/to/repo/on/remote",
  "userData": { "provider": "example", "resourceId": "provider-resource-id" }
}
其中
pairingCode
(来自
orca serve --recipe-json
)和
projectRoot
是必填项;
schemaVersion
1
)和
userData
是可选项。
SSH模式 —— 不运行
orca serve
;输出
connection.type:"ssh"
块(完整格式+示例脚本见第7g节)。SSH模式不使用
pairingCode
生命周期钩子(均本地运行):
  • create
    :必填。输出配方结果JSON。
  • suspend
    :可选。休眠;从标准输入读取生命周期负载。
  • resume
    :可选。唤醒;从标准输入读取负载并输出最新的配方JSON(配对信息可能变更)。
  • destroy
    :可选,除非设置
    destroy: none
    。删除/清理;从标准输入读取负载。
使用
orca serve --port "$PORT" --project-root "$ABS_ROOT" --pairing-address "$EXTERNAL_WSS_URL" --recipe-json
远程启动Orca(准确参数+输出见第7c节)。将
--pairing-address
设置为可外部访问的地址,使生成的
pairingCode
可访问;隧道/端口映射是脚本的职责。
向后兼容性:
command
create
cleanup
destroy
cleanup: none
destroy: none
。优先使用生命周期名称。

9. Doctor and validation

9. Doctor验证

Validate in two stages — the cheap dry run first, then the live self-test.
分两个阶段验证——先执行低成本的dry run,再执行在线自测。

Dry run (free, non-destructive) — always do this first

Dry run(免费、非破坏性)—— 始终先执行此步骤

orca vm recipe doctor <recipe-id> --repo-path <repo> --json
validates static wiring only — it does not boot anything. It checks: local-host execution (v1), repo path, recipe id exists, create/destroy/suspend/resume command paths resolve, suspend/resume are paired, and each script is executable (POSIX exec bit; skipped on Windows). Fix every failure here before spending any cloud money.
orca vm recipe doctor <recipe-id> --repo-path <repo> --json
仅验证静态配置——不启动任何资源。它检查:本地主机执行(v1)、仓库路径、配方ID存在、创建/销毁/暂停/恢复命令路径存在、暂停/恢复配对、每个脚本可执行(POSIX执行位;Windows上跳过)。在产生任何云费用前修复所有失败项。

Live self-test (
--provision
) — diagnose and iterate yourself

在线自测(
--provision
)—— 自行诊断并迭代

orca vm recipe doctor <recipe-id> --repo-path <repo> --provision --json
actually runs the recipe end to end: it executes
create
, validates the returned recipe JSON, then runs
destroy
to tear the environment back down (so the test leaves nothing running, as long as
destroy
works). It spends real cloud money, so get the user's OK once before starting — that one approval covers the whole loop below; do not re-ask before each run.
On failure, the JSON result includes a
provisionTranscript
with the complete captured output of each stage so you can self-diagnose without asking the user to relay logs:
json
{
  "ok": false,
  "checks": [ { "id": "recipe.provision", "status": "fail", "message": "…" } ],
  "provisionTranscript": {
    "provision": { "exitCode": 0, "signal": null, "stdout": "…", "stderr": "…", "parseError": "…" },
    "destroy":   { "exitCode": 0, "signal": null, "stdout": "…", "stderr": "…" }
  }
}
Run it as a loop: read
provisionTranscript.provision.stderr
/
.stdout
/
.parseError
(and
destroy.*
), fix the script, and re-run
--provision
until
ok
is
true
— iterating on your own rather than waiting for the user to paste errors. Common reads: a non-empty
stderr
with
exitCode 0
plus a
parseError
means
create
ran but printed something other than the single recipe-result JSON on stdout (often a stray
echo
— route it to stderr, see §10); a non-zero
exitCode
is a provider/script failure described in
stderr
. Each stream is redacted and capped (head+tail) — large logs keep both the setup context and the failure.
The self-test cannot see provider-side truth beyond what the scripts print, so still confirm: state has a populated authenticated
snapshotId
(Phases 2–3 done), and
destroy
is implemented/tested (or explicitly
none
— in which case the self-test won't tear down, so clean up manually).
For SSH recipes, also smoke-test the exact emitted target before declaring success: dial the host/port with the identity/proxy settings, run
pwd
, verify the repo path, check the agent binary, and confirm
destroy
removes the provider resource/container. For Docker, inspect the auth image entrypoint and do a startup-only
docker run
before the full clone/install path.

orca vm recipe doctor <recipe-id> --repo-path <repo> --provision --json
会实际端到端运行配方:执行
create
,验证返回的配方JSON,然后运行
destroy
销毁环境(因此只要
destroy
正常工作,测试不会留下任何运行中的资源)。此操作会产生真实云费用,因此在开始前需获得用户一次许可——该许可覆盖以下整个循环;无需每次运行前重新询问。
失败时,JSON结果包含
provisionTranscript
,其中包含每个阶段的完整捕获输出,你可自行诊断,无需用户转发日志:
json
{
  "ok": false,
  "checks": [ { "id": "recipe.provision", "status": "fail", "message": "…" } ],
  "provisionTranscript": {
    "provision": { "exitCode": 0, "signal": null, "stdout": "…", "stderr": "…", "parseError": "…" },
    "destroy":   { "exitCode": 0, "signal": null, "stdout": "…", "stderr": "…" }
  }
}
循环执行: 读取
provisionTranscript.provision.stderr
/
.stdout
/
.parseError
(以及
destroy.*
),修复脚本并重新执行
--provision
,直到
ok
true
——自行迭代,无需等待用户粘贴错误。常见问题:
exitCode 0
stderr
非空且有
parseError
,说明
create
运行但标准输出打印了配方结果JSON以外的内容(通常是多余的
echo
——需将其重定向到stderr,见第10节);非零
exitCode
是提供商/脚本失败,错误信息在
stderr
中。每个流都会被脱敏并截断(头部+尾部)——大日志会保留设置上下文和失败信息。
自测无法看到脚本输出以外的提供商端真实信息,因此仍需确认:状态文件中已填充已认证
snapshotId
(已完成第2-3阶段),且
destroy
已实现/测试(或明确设置为
none
——此时自测不会销毁资源,需手动清理)。
对于SSH配方,在宣布成功前还需测试输出的目标:使用身份/代理设置连接主机/端口,运行
pwd
,验证仓库路径,检查Agent二进制文件,并确认
destroy
会删除提供商资源/容器。对于Docker,在完整克隆/安装前检查认证镜像的入口点并执行仅启动的
docker run

10. Failure modes

10. 失败模式

  • Build exceeds plan timeout (e.g. Hobby 45m). Use enough vCPUs and a timeout covering the build; else split work or use a higher plan. The cap also limits per-workspace runtime — surface it.
  • Build exceeds plan RAM. Build the headless main only (drop the renderer) — the biggest fitter.
  • Private-repo clone hangs/fails. Wrong/missing token. Use
    GIT_ASKPASS
    +
    GIT_TERMINAL_PROMPT=0
    so it fails fast instead of prompting.
  • GIT_ASKPASS
    helper aborts the clone with "
    $1: unbound variable
    ".
    The
    printf
    /heredoc that writes the helper inside
    bash -lc
    under
    set -u
    expanded
    $1
    /
    $GH_TOKEN
    at write time. Escape them (
    \$1
    ,
    \$GH_TOKEN
    ) so they land literally and resolve at git-runtime; this also keeps the real token out of the file.
    rm -f
    the helper afterward (§5, §7f).
  • Agent verified as "not logged in" despite a good login.
    codex login status
    (and similar) print "Logged in …" to stderr; an stdout-only
    grep
    misses it. Prefer the status exit code; if you grep, fold stderr first (
    status 2>&1 | grep …
    ) and match the exact success line — not
    grep -qi 'logged in'
    , which also matches "not logged in".
  • Headless agent login hangs. Plain OAuth
    login
    starts a loopback callback server on a VM/container port the host browser can't reach. Use the device-auth flow (
    login --device-auth
    ) — it prints a URL + code the user opens on the host.
  • known_hosts
    host-key churn on local Docker.
    Each ephemeral container regenerating its SSH host key collides on
    127.0.0.1
    as the published port rotates. Bake host keys into the base image at build time (
    ssh-keygen -A
    ; runtime generates only if absent) so all containers share one stable key (§7h).
  • Snapshot expired/evicted. If
    create
    hits an unknown snapshot id, rerun Phases 2–3 and update
    snapshotId
    .
  • Agent auth didn't persist. Confirm
    snapshotId
    points at the authenticated snapshot; re-run Phase 3. Warn that short-lived tokens may need periodic re-auth.
  • Agent auth copied from the host breaks. Do not bind-mount/copy a full host agent home; sqlite files can be unwritable or host-specific, hooks may need approval again, and config may reference local-only env vars. Authenticate inside the runtime and snapshot/commit that layer.
  • Docker auth image exits immediately. Inspect
    docker image inspect … .Config.Entrypoint
    and
    docker logs
    . If the image was committed from an interactive shell, reset the entrypoint to the SSH entrypoint during
    docker commit
    .
  • Leaked paid resource. Every long script must trap errors and remove the sandbox it created.
  • create
    emits non-JSON on stdout.
    A stray
    echo
    corrupts the result — stdout is for the final JSON only; everything else to stderr. The
    --provision
    self-test surfaces this as
    exitCode 0
    + a
    parseError
    with the offending stdout in
    provisionTranscript
    (§9).

  • 构建超出套餐超时限制(例如Hobby套餐45分钟)。 使用足够的vCPU并设置覆盖构建的超时时间;否则拆分工作或使用更高套餐。此限制也会影响工作区运行时间——需告知用户。
  • 构建超出套餐RAM限制。 仅构建无头主进程(移除渲染进程)——这是最有效的方式。
  • 私有仓库克隆挂起/失败。 令牌错误/缺失。使用
    GIT_ASKPASS
    +
    GIT_TERMINAL_PROMPT=0
    使其快速失败而非提示。
  • GIT_ASKPASS
    助手因"
    $1: unbound variable
    "中止克隆。
    bash -lc
    下使用
    set -u
    编写助手时,
    printf
    /heredoc在写入时展开了
    $1
    /
    $GH_TOKEN
    。需转义它们(
    \\$1
    \\$GH_TOKEN
    )使其按字面量写入并在Git运行时解析;这也可避免真实令牌写入文件。之后
    rm -f
    助手文件(第5节、第7f节)。
  • 尽管登录成功,但Agent被验证为"未登录"。
    codex login status
    (及类似命令)将"Logged in …"打印到stderr;仅读取stdout的
    grep
    会错过。优先使用状态退出码;若必须使用grep,先合并stderr(
    status 2>&1 | grep …
    )并匹配准确成功行——不要使用
    grep -qi 'logged in'
    ,因为它也会匹配"未登录"。
  • 无头Agent登录挂起。 普通OAuth
    login
    会在虚拟机/容器端口启动环回回调服务器,主机浏览器无法访问。使用设备认证流程(
    login --device-auth
    )——它会打印URL+验证码,用户在主机上打开即可。
  • 本地Docker的
    known_hosts
    主机密钥频繁变更。
    每个临时容器重新生成SSH主机密钥,当发布端口轮换时会在
    127.0.0.1
    上冲突。在构建时将主机密钥嵌入基础镜像(
    ssh-keygen -A
    ;仅在运行时缺失时生成),使所有容器共享一个稳定密钥(第7h节)。
  • 快照过期/被回收。 如果
    create
    命令遇到未知快照ID,重新执行第2-3阶段并更新
    snapshotId
  • Agent认证未持久化。 确认
    snapshotId
    指向已认证的快照;重新执行第3阶段。提醒用户短期令牌可能需要定期重新认证。
  • 从主机复制的Agent认证失效。 不要绑定挂载/复制完整的主机Agent目录;sqlite文件可能不可写或主机特定,钩子可能需要重新批准,配置可能引用仅本地的环境变量。在运行时内完成Agent认证并快照/提交该层。
  • Docker认证镜像立即退出。 检查
    docker image inspect … .Config.Entrypoint
    docker logs
    。如果镜像是从交互式Shell提交的,在
    docker commit
    时将入口点重置为SSH入口点。
  • 付费资源泄露。 每个长脚本必须捕获错误并删除其创建的沙箱。
  • create
    命令在标准输出中输出非JSON内容。
    多余的
    echo
    会破坏结果——标准输出仅用于最终JSON;其他内容需输出到标准错误。
    --provision
    自测会将此显示为
    exitCode 0
    +
    parseError
    ,并在
    provisionTranscript
    中显示有问题的标准输出(第9节)。

11. Boundaries

11. 边界规则

  • Don't create accounts, choose plans/regions, or invent scope/project/org/image/billing ids.
  • Don't invent or store credentials; no secrets in
    userData
    , state, comments, docs, or commits.
  • Don't run paid/long phases (base snapshot, auth, live test) without an explicit OK.
  • Don't hide provider errors behind generic messages — preserve actionable stderr.
  • Don't make Orca own provider lifecycle beyond invoking the configured scripts.
  • Don't commit or create an Orca workspace unless asked.
  • 不要创建账户、选择套餐/区域,或生成范围/项目/组织/镜像/账单ID。
  • 不要生成或存储凭证;不要在
    userData
    、状态文件、注释、文档或提交中包含密钥。
  • 不要在未获得明确许可的情况下执行付费/耗时较长的阶段(基础快照、认证、在线测试)。
  • 不要用通用消息掩盖提供商错误——保留可操作的stderr信息。
  • 不要让Orca除了调用配置的脚本外,还掌控提供商生命周期。
  • 除非用户要求,否则不要提交代码或创建Orca工作区。",