orca-per-workspace-env
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePer-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? passing?), scaffold provider-templated scripts the user fills in, drive the slow snapshot/auth phases with the user, and always show the next action.
doctor - 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:
- Prerequisites — cloud account, provider CLI, scope/project, plan limits, git token (§2).
- Base snapshot — reusable image: tools + repo + headless build, snapshotted once (§3).
- Agent-auth snapshot — boot the base, run interactive device-auth, re-snapshot (§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 ( runs
in the env and emits a ; §7c/§7f) vs SSH ( runs no server and emits a
block Orca dials into; §7g/§7h). Settle this first — it changes the
output shape and half the templates.
createorca servepairingCodecreateconnection.type:"ssh"createQuick-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 from §7 → run the
base-snapshot script, then the auth script (you invoke these by hand; not via ) → wire
in → (free) → then the
self-test loop (§9) until it passes.
scripts/orca-vm/orca.yamlenvironmentRecipesorca.yamlorca vm recipe doctor <id> --json--provision协助用户端到端地搭建和维护仓库专属的工作区环境配方。每个工作区都会拥有自己的按需、一次性运行时(云沙箱、虚拟机或本地运行时),使用后即被销毁。
Orca是一个轻量封装层:你仅需提供引导、检测和模板生成服务;绝不掌控用户的云账户、账单、镜像或凭证。
- 你需要做的: 规划设置流程,检测可识别项(提供商CLI是否已安装/登录?配方是否存在?命令是否通过?),生成提供商模板化脚本供用户填充,引导用户完成耗时的快照/认证阶段,并始终明确下一步操作。
doctor - 你不能做的: 创建账户、选择套餐/区域、生成组织/项目/范围ID、存储或打印密钥,或在未获得用户明确许可的情况下执行任何产生费用的操作。
首次设置包含四个阶段,需按顺序执行,否则容易遗漏:
- 前置条件 —— 云账户、提供商CLI、范围/项目、套餐限制、Git令牌(第2节)。
- 基础快照 —— 可复用镜像:包含工具、仓库和无头构建,只需创建一次(第3节)。
- Agent认证快照 —— 启动基础镜像,运行交互式设备认证,重新生成快照(第4节)。
- 状态管理 —— 通过状态文件在各阶段传递快照ID/范围/项目/端口等信息(第6节)。
完成上述阶段后,工作区生命周期操作(创建/暂停/恢复/销毁)即可快速执行(第8节)。
决定所有流程的核心分支——连接模式: Orca-server(命令在环境中运行并生成;第7c/7f节)与SSH(命令不运行服务器,而是生成块供Orca连接;第7g/7h节)。需先确定此模式,它会改变命令的输出格式和半数模板内容。
createorca servepairingCodecreateconnection.type:"ssh"create快速入门(理想流程): 与用户沟通确认(连接模式:Orca-server或SSH、提供商、Agent CLI、Git认证——第1.2节)+ 查阅提供商CLI文档 → 基于第7节生成目录下的脚本 → 手动运行基础快照脚本,再运行认证脚本(不通过调用) → 在中配置 → 执行(免费) → 重复执行自测循环(第9节)直至通过。
scripts/orca-vm/orca.yamlorca.yamlenvironmentRecipesorca vm recipe doctor <id> --json--provision1. 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.
- Inspect the repo for an existing entry,
environmentRecipes, a state file, or setup notes. If a working recipe exists, jump to Doctor (§9) instead of rebuilding.scripts/orca-vm/ - 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
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.
orca serve - 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
) before scaffolding — you need its exact create/exec/snapshot/remove verbs. If a provider advertises
<cli> --help, 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.ssh - Coding-agent CLI + account: which agent runs in the VM (,
codex, …) and that the user has an account for it — it gets logged in during the Phase-3 auth snapshot (§4).claude - Git auth: the token source for cloning a private repo (/
GH_TOKENorGITHUB_TOKEN; §5).gh auth token
- Connection mode: how Orca attaches to the environment — an Orca server (the VM runs
- Check prerequisites (§2) — detect the provider CLI + auth and confirm the items above are in place before any paid step.
- 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.
- [CHECKPOINT] Build the base snapshot (§3) — paid, slow.
- [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 -itto prompt against. The user runs the Phase-3 login in their own terminal (or via the Claude Code harness bang-prefix —ssh -t, with the required space after! <cmd>); 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).! - Wire the recipe so points create/suspend/resume/destroy at the scripts (§8). The workspace composer reads
orca.yamlfrom the project's primary checkout ofenvironmentRecipes, not from a feature branch or worktree. So a recipe added only on a branch won't appear as a "Run on" option until thatorca.yamlchange is committed and merged to the project's primary branch. Tell the user this up front:orca.yaml/doctorvalidate the scripts from the working copy on any branch, but creating a workspace from the recipe in the picker needs it on primary.--provision - Dry-run doctor — (free, static; §9). Fix every failure before going live.
orca vm recipe doctor <recipe-id> --repo-path <repo> --json - [CHECKPOINT] Live self-test — get the user's OK once, then run
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.
orca vm recipe doctor <recipe-id> --provision --json - [CHECKPOINT] Optional workspace test — only if asked: create a workspace via the picker, then verify sleep/wake/delete.
引导用户完成以下步骤。[检查点] 步骤需要用户明确确认——这些操作会产生费用、耗时较长,或需要用户在场操作。除非用户要求,否则不要创建Orca工作区或提交代码。
- 检查仓库 是否存在现有条目、
environmentRecipes目录、状态文件或设置说明。若已有可用配方,直接跳至Doctor验证环节(第9节),无需重新构建。scripts/orca-vm/ - 提前与用户沟通 —— 收集以下选项并确认后再生成模板。不要替用户做选择(第11节);不要猜测。
- 连接模式: Orca连接环境的方式——Orca server(虚拟机运行,Orca通过配对URL连接;示例见第7f节)或SSH(Orca通过SSH连接主机;第7g节)。此模式决定配方的连接格式,需优先确定。
orca serve - 提供商: Vercel Sandbox、Fly、Modal、现有SSH主机等。对于不常见的提供商,还需询问范围/项目/区域和套餐限制(第2节)。然后查阅该提供商的CLI/SDK文档(或)再生成模板——你需要了解其创建/执行/快照/删除的准确命令。 如果提供商支持
<cli> --help,需确认其是否提供可直接连接的SSH目标(主机/端口/用户/密钥或代理命令),还是仅提供提供商介导的交互式Shell;Orca的SSH模式需要前者。ssh - 编码Agent CLI + 账户: 虚拟机中运行的Agent(、
codex等),以及用户是否拥有对应账户——在第3阶段的认证快照中会完成登录。claude - Git认证: 克隆私有仓库的令牌来源(/
GH_TOKEN或GITHUB_TOKEN;第5节)。gh auth token
- 连接模式: Orca连接环境的方式——Orca server(虚拟机运行
- 检查前置条件(第2节) —— 检测提供商CLI是否已安装并完成认证,确认上述所有项已准备就绪后,再执行任何付费操作。
- 基于第7节生成脚本 + 状态文件(Vercel示例:第7f节;SSH主机:第7g节;Docker SSH:第7h节;Windows:第7i节),填充提供商的实际命令。确保脚本可执行。
- [检查点] 构建基础快照(第3节) —— 付费且耗时较长。
- [检查点] Agent认证(第4节) —— 交互式操作;用户需跟随URL/验证码完成。你无法自行驱动此步骤——你执行的是非交互式命令,没有TTY供/
docker exec -it弹出提示。用户需在自己的终端中执行第3阶段的登录操作(或通过Claude Code harness的前缀ssh -t,注意! <cmd>后需加空格);你负责生成模板并驱动非交互阶段。启动后,请用户在登录完成后告知你——你无法观察到登录完成,需要用户确认后再继续执行非交互步骤(基础/认证提交、Doctor验证、部署)。! - 配置配方 使指向创建/暂停/恢复/销毁脚本(第8节)。工作区组合器从项目主分支的
orca.yaml中读取orca.yaml,而非功能分支或工作树。因此,仅在分支中添加的配方不会显示为“运行于”选项,直到environmentRecipes的变更提交并合并到项目主分支。请提前告知用户:orca.yaml/doctor可验证任何分支工作副本中的脚本,但从选择器创建工作区需要配方已在主分支中。--provision - Dry-run Doctor验证 —— 执行(免费、静态检查;第9节)。在正式使用前修复所有失败项。
orca vm recipe doctor <recipe-id> --repo-path <repo> --json - [检查点] 在线自测 —— 获得用户一次许可后,循环执行:它会执行创建→验证→销毁,失败时返回完整记录。读取记录、修复脚本并重新执行,直至通过(第9节)。此操作会产生云费用;一次许可覆盖整个循环。
orca vm recipe doctor <recipe-id> --provision --json - [检查点] 可选工作区测试 —— 仅在用户要求时执行:通过选择器创建工作区,然后验证休眠/唤醒/删除功能。
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 (), check auth (e.g.
command -v <cli>). If missing, point at the provider's docs; don't log them in.vercel whoami - 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, or the provider's git auth; can fall back toGITHUB_TOKEN). See §5.gh auth token - Coding-agent CLI choice (,
codex…) and that the user has an account — it gets authenticated into the VM in Phase 3.claude
这些是用户的责任;验证可核实的项,询问其余项,不要自行编造。说明哪些项是你已验证的,哪些是用户确认的。
- 连接模式(Orca server或SSH)已与用户确认——见第1步第2节;它决定配方的形态。
- 云账户 + 套餐 支持沙箱/虚拟机。需询问用户。
- 提供商CLI已安装 + 已认证 —— 检测(),检查认证状态(例如
command -v <cli>)。若缺失,指向提供商文档;不要替用户登录。vercel whoami - 沙箱所属的范围 / 项目 / 区域。需询问用户;这些信息会通过状态文件流入每个脚本。
- 套餐 / 超时 / RAM限制。记录这些信息——例如Vercel Hobby套餐的沙箱超时限制为45分钟,这会限制基础构建和工作区运行时间(见第10节)。
- 私有仓库的Git令牌(/
GH_TOKEN,或提供商的Git认证;可 fallback 到GITHUB_TOKEN)。见第5节。gh auth token - 编码Agent CLI选择(、
codex等)以及用户是否拥有对应账户——在第3阶段会完成虚拟机中的Agent认证。claude
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, per the base distro — not the provider brand).apk - Clone with the git token via (§5).
GIT_ASKPASS - 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令牌克隆仓库(第5节)。
GIT_ASKPASS - 捕获错误并删除半构建的沙箱,避免崩溃后留下付费资源持续运行。
- 快照已停止的沙箱,解析快照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:
- Boot a sandbox from the base (from state).
snapshotId - Run the agent's login interactively (); the user completes the URL/code in their browser. On a headless VM this must be the device-auth flow (e.g.
--interactive --tty), not plaincodex login --device-auth: 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.codex login - 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. prints "Logged in using ChatGPT" there), so fold stderr first (
codex login status) and match the agent's exact success line — never... 2>&1 | grep …, which also matches "not logged in" and would commit an unauthenticated image.grep -qi 'logged in' - Re-snapshot, parse the new id, and overwrite in state to the authenticated image (recording
snapshotId). Remove the auth sandbox.authSourceSnapshotId
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 (, 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.
! <cmd>!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 ) 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.
~/.codex基础快照已安装Agent CLI但未登录,且工作区虚拟机是临时的——因此只需认证一次并将其嵌入到第二个快照层中。脚本格式见第7b节:
- 从基础(来自状态文件)启动沙箱。
snapshotId - 交互式运行Agent登录命令();用户在浏览器中完成URL/验证码操作。在无头虚拟机上必须使用设备认证流程(例如
--interactive --tty),不能使用普通的codex login --device-auth:默认OAuth登录会在容器端口启动环回回调服务器,主机浏览器无法访问,导致挂起。设备认证会打印URL+验证码,用户在主机上打开即可。codex login - 验证登录状态;拒绝快照未认证的虚拟机。优先使用状态命令的退出码(大多数Agent CLI在未认证时会返回非零退出码)。若必须使用grep,Agent状态通常输出到stderr(例如会在stderr中打印"Logged in using ChatGPT"),因此需先合并stderr(
codex login status)并匹配Agent的准确成功行——绝不要使用... 2>&1 | grep …,因为它也会匹配"未登录",导致提交未认证的镜像。grep -qi 'logged in' - 重新生成快照,解析新ID,并将状态文件中的覆盖为认证后的镜像ID(同时记录
snapshotId)。删除认证沙箱。authSourceSnapshotId
你无法自行驱动第2步(你执行的是非交互式命令——没有TTY)。用户需在自己的终端中执行,或通过Claude Code harness的前缀(注意后需加空格)。你负责生成模板/启动沙箱并执行第3-4步,但你无法观察到交互式登录完成——因此请用户在完成后告知你,再进行验证和重新快照。
! <cmd>!如果Agent凭证有效期较短,需提醒用户快照可能需要定期重新认证(第10节)。
对于一次性运行时,不要将主机Agent配置目录(例如)通过绑定挂载或整体复制作为认证快照。Agent目录通常包含sqlite状态、钩子审批状态、缓存、日志和主机特定的环境/配置。相反,应在一次性运行时内完成Agent的认证/配置,并快照/提交该运行时层。
~/.codex5. Credentials
5. 凭证管理
- Never commit secrets or put them in , recipe JSON, comments, docs, or the state file.
userData - Git token: read from env (/
GH_TOKEN), falling back toGITHUB_TOKEN. Pass to the VM only via the provider's ephemeralgh auth token. Inside the VM, use a--envhelper withGIT_ASKPASS(not the token in the clone URL) andx-access-tokenso a missing token fails fast instead of hanging. When you write the helper from insideGIT_TERMINAL_PROMPT=0underbash -lc, escape the positional arg and the token (set -u,\$1) so they land literally and resolve at git-runtime — an unescaped\$GH_TOKENaborts with "unbound variable", and a literal$1keeps the real token out of the written file.$GH_TOKENthe helper after the clone/fetch.rm -f - 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).
- 绝不提交密钥或将其放入、配方JSON、注释、文档或状态文件中。
userData - Git令牌: 从环境变量读取(/
GH_TOKEN),fallback到GITHUB_TOKEN。仅通过提供商的临时gh auth token参数传递给虚拟机。在虚拟机内部,使用--env助手和GIT_ASKPASS(不要在克隆URL中直接使用令牌),并设置x-access-token,确保令牌缺失时快速失败而非挂起。当你在GIT_TERMINAL_PROMPT=0下使用bash -lc编写助手时,需转义位置参数和令牌(set -u、\\$1),使其按字面量写入并在Git运行时解析——未转义的\\$GH_TOKEN会因“未绑定变量”而中止,字面量$1可避免真实令牌写入文件。克隆/获取完成后$GH_TOKEN助手文件。rm -f - 提供商认证: 依赖提供商CLI的已登录会话,而非已签入的密钥。
- Agent认证: 存储在认证快照中(第3阶段)——绝不存储在你编写或提交的文件中。
- 状态文件仅存储非机密配置(快照ID、范围、项目、端口、仓库URL/引用)。
6. State file
6. 状态文件
A repo-local JSON file (e.g. ) 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 ; Phase 3 overwrites it with the authenticated snapshot;
per-workspace boots from .
scripts/orca-vm/<provider>-state.jsonsnapshotIdcreatesnapshotIdjson
{
"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文件(例如)用于在各阶段传递非机密值。每个脚本按环境变量 → 状态文件 → 内置默认值的顺序解析值,并将输出合并回状态文件。第2阶段写入基础;第3阶段将其覆盖为认证后的快照ID;工作区命令从启动。
scripts/orca-vm/<provider>-state.jsonsnapshotIdcreatesnapshotIdjson
{
"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 . 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 /
reader (env → state → fallback) in each.
scripts/orca-vm/json_value <key>env_value <NAME>Where each script runs:
- Local-side (/
create/suspend/resume+ the base-snapshot/auth scripts the user invokes) runs on the user's desktop, so it must run on their OS. macOS/Linux:destroy,#!/usr/bin/env bash, quoted paths. Windows: a bareset -euo pipefailwon't run — scaffold.sh/.ps1or require WSL/Git-Bash and point.cmdat the right launcher.orca.yaml - Remote-side (commands you inside the Linux VM) always runs in the VM's Linux shell, so bash is fine there regardless of the user's OS.
exec
在目录下生成模板。这些是格式框架——需填充提供商的实际命令。所有脚本将标准输出保留给最终JSON,进度日志输出到标准错误。每个脚本包含共享的/读取函数(环境变量→状态文件→默认值)。
scripts/orca-vm/json_value <key>env_value <NAME>各脚本的运行位置:
- 本地端(/
create/suspend/resume+ 用户调用的基础快照/认证脚本)运行在用户桌面,因此必须适配用户的操作系统。macOS/Linux:使用destroy、#!/usr/bin/env bash、带引号的路径。Windows: 纯set -euo pipefail文件无法运行——生成.sh/.ps1脚本,或要求用户使用WSL/Git-Bash并在.cmd中指向正确的启动器。orca.yaml - 远程端(在Linux虚拟机内部执行的命令)始终运行在虚拟机的Linux Shell中,因此无论用户操作系统如何,使用bash即可。
7a. Base-snapshot (<provider>-base-snapshot.sh
) — Phase 2
<provider>-base-snapshot.sh7a. 基础快照(<provider>-base-snapshot.sh
)—— 阶段2
<provider>-base-snapshot.shbash
#!/usr/bin/env bash
set -euo pipefailbash
#!/usr/bin/env bash
set -euo pipefailresolve 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 auth token解析gh令牌:GH_TOKEN | GITHUB_TOKEN | gh auth token
gh auth token1. 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
<provider>-base-auth.sh7b. 认证(<provider>-base-auth.sh
)—— 阶段3
<provider>-base-auth.shbash
#!/usr/bin/env bash
set -euo pipefailbash
#!/usr/bin/env bash
set -euo pipefailread 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设备认证流程(例如codex login --device-auth
)——普通OAuth登录绑定的环回回调
codex login --device-authport 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节。
status 2>&1 | grep …grep -qi 'logged in'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
status 2>&1 | grep …5. 将{ snapshotId:<new>, authSourceSnapshotId:<source> }合并到状态文件;删除认证沙箱
success line; never grep -qi 'logged in'
, which also matches "not logged in". Codex example: §7f.
grep -qi 'logged in'仅将状态JSON输出到标准输出
4. snapshot; parse new id
—
5. merge { snapshotId:<new>, authSourceSnapshotId:<source> } into state; remove auth sandbox
—
print only the state JSON to stdout
—
undefinedundefined7c. Create (<provider>-create.sh
) — per workspace
<provider>-create.sh7c. 创建(<provider>-create.sh
)—— 每个工作区
<provider>-create.shbash
#!/usr/bin/env bash
set -euo pipefailbash
#!/usr/bin/env bash
set -euo pipefailread 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-jsonBinary name: in a VM built from source (the Phase-2 flow), run it as
from the repo root — is the in-repo entrypoint and is what the §7f example uses. Plain
is the same command when the built CLI is installed on the VM's PATH. The flags/output
are identical either way.
pnpm exec orca-dev serve …orca-devorca serve …There is no flag. must be an absolute directory on the remote. With
the server stays running and prints exactly this single object to stdout, then
keeps serving:
--host--project-root--recipe-jsonjson
{ "schemaVersion": 1, "pairingCode": "<orca pairing URL>", "projectRoot": "<the --project-root you passed>" }pairingCode--pairing-address--pairing-addresspairingCodecreateuserData
**`orca serve`的准确调用方式及其输出(已验证——不要自行修改参数)。** 在虚拟机内部运行:
```bash
orca serve \\
--port "$PORT" \\
--project-root "$ABS_REPO_PATH_ON_REMOTE" \\
--pairing-address "$EXTERNAL_WSS_URL" \\
--recipe-json二进制名称: 在从源码构建的虚拟机中(第2阶段流程),从仓库根目录运行——是仓库内的入口点,第7f节示例使用此命令。当构建后的CLI已安装在虚拟机PATH中时,是相同的命令。两种方式的参数/输出完全一致。
pnpm exec orca-dev serve …orca-devorca serve …没有**参数**。必须是远程端的绝对目录。使用参数时,服务器会保持运行并向标准输出打印以下单个对象,然后持续提供服务:
--host--project-root--recipe-jsonjson
{ "schemaVersion": 1, "pairingCode": "<orca pairing URL>", "projectRoot": "<the --project-root you passed>" }pairingCode--pairing-address--pairing-addresspairingCodecreateuserData7d. 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)
undefinedundefined7e. 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: ). Adapt
names; verify flags against for the user's CLI version before relying on them.
These ground §7a (base snapshot) and §7b (auth), which are otherwise generic skeletons.
vercel sandbox create|exec|snapshot|removevercel sandbox --helpPhase 2 — base snapshot (§7a): provision → install tools + clone + headless build → snapshot.
bash
undefined真实可用的格式(Vercel提供CLI:)。调整名称;在依赖前需根据用户CLI版本验证参数(通过)。这些示例是第7a节(基础快照)和第7b节(认证)的具体实现,否则它们只是通用框架。
vercel sandbox create|exec|snapshot|removevercel sandbox --help阶段2 —— 基础快照(第7a节): 部署 → 安装工具 + 克隆 + 无头构建 → 快照。
bash
undefinedprovision 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
--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.shrm -f /tmp/askpass.sh
),编写仅无头主进程的构建配置(移除渲染进程),开发环境设置,
rm -f /tmp/askpass.shbuild 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)"
' "$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[@]}" >&2INTERACTIVE — 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
codex login主机上完成URL/验证码操作。在无头虚拟机上必须使用--device-auth:普通codex login
绑定的环回
codex logincallback 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)"
|| { 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)"
' "$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 pipefailresolve 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
--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
' "$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://}"
' "$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
--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
" "#!/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')"
--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
"$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:
- does NOT run
createand does NOT emit aorca serve. 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.pairingCode - The result uses a block with
connectionand atype: "ssh", not the flattarget/pairingCodeshape. Exact shape (Orca rejects anything else):projectRoot
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": []
}
}
}labelhostportusernameNetworking → which fields to set (how your desktop reaches the box — there is no
URL in SSH mode):
targetorca serve- Public IP / DNS, or a Tailscale/VPN address → ; SSH port →
host(usually 22).port - Key auth → (add
identityFileif the agent has many keys).identitiesOnly: true - Through a bastion → (a
jumpHostProxyJump) or a fulluser@host(e.g. an access proxy). Use one, not both.proxyCommand - A service port the workspace needs → add entries to .
portForwards - (optional): how long Orca keeps the SSH relay alive after the workspace detaches before tearing it down;
relayGracePeriodSeconds= tear down immediately. Leave it off unless the user wants a reconnect grace window.0
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 directly over SSH on the host (interactive, e.g.
). After that the host stays ready across workspaces.
<agent> login --device-authssh -t user@host '<agent> login --device-auth'bash
#!/usr/bin/env bash
set -euo pipefailSSH模式与第7c/7f节完全不同,并非简单重命名:
- 命令不运行
create,也不生成orca serve。 Orca通过SSH中继连接到主机,启动Git和文件系统提供商,并导入仓库。脚本的唯一任务是确保主机就绪并打印Orca将连接的SSH详细信息。pairingCode - 结果使用块,包含
connection和type: "ssh",而非扁平的target/pairingCode格式。准确格式(Orca会拒绝其他格式):projectRoot
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": []
}
}
}labelhostportusername网络设置 → 需设置哪些字段(你的桌面如何连接到主机——SSH模式下没有URL):
targetorca serve- 公网IP / DNS,或Tailscale/VPN地址 → ;SSH端口 →
host(通常为22)。port - 密钥认证 → (如果Agent有多个密钥,添加
identityFile)。identitiesOnly: true - 通过堡垒机连接 → (
jumpHost格式的ProxyJump)或完整的user@host(例如访问代理)。二选一,不要同时使用。proxyCommand - 工作区需要的服务端口 → 添加到数组中。
portForwards - (可选):Orca在工作区断开连接后,保持SSH中继存活的时间;
relayGracePeriodSeconds= 立即销毁。除非用户需要重连宽限期,否则留空。0
持久主机(无快照)上的工具链与Agent认证 —— 在配置配方前需手动执行一次(没有基础镜像可构建;主机本身就是基础)。直接通过SSH在主机上运行第7f节的阶段2安装步骤和阶段3的命令(交互式,例如)。之后主机即可为所有工作区就绪。
<agent> login --device-authssh -t user@host '<agent> login --device-auth'bash
#!/usr/bin/env bash
set -euo pipefailresolve 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
"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"
"$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 , tools,
repo prerequisites, and the agent CLI; run an interactive auth container once; then
that container as the authenticated image used by per-workspace .
sshddocker commitcreateKey points:
- Publish container SSH to a random localhost port () and emit
-p 127.0.0.1::22withconnection.type:"ssh", that port,host:"127.0.0.1",username, andidentityFile.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 (at build time; at runtime only generate if absent). Ephemeral containers all present the same host key, so
ssh-keygen -Aonknown_hostsdoesn't churn as the published port rotates across workspaces (otherwise every container's freshly generated key collides on127.0.0.1and trips host-key-changed warnings).localhost - 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"]' … - should read
destroyand runrecipeResult.userData.resourceId.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 is a common cause.
ENTRYPOINT ["bash"]Also confirm the host key is stable across containers: the SSH 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 point above).
ssh -i … 127.0.0.1ssh-keygen -A本地Docker可模拟临时SSH虚拟机而无需云成本:构建包含、工具、仓库前置条件和Agent CLI的基础镜像;运行一次交互式认证容器;然后该容器作为工作区命令使用的认证镜像。
sshddocker commitcreate关键点:
- 将容器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.1ssh-keygen -A7i. Windows local-side scripts
7i. Windows本地端脚本
The local-side scripts run on the user's desktop. On Windows, a bare won't execute. Either
require WSL/Git-Bash (and point at e.g. via a
launcher), or scaffold PowerShell equivalents. Minimal PowerShell shape:
.shorca.yamlbash ./scripts/orca-vm/<name>.sh.cmdpowershell
#requires -Version 5
$ErrorActionPreference = 'Stop'本地端脚本运行在用户桌面。在Windows上,纯文件无法执行。要么要求用户使用WSL/Git-Bash(并在中指向例如,通过启动器),要么生成PowerShell等效脚本。最小PowerShell格式:
.shorca.yamlbash ./scripts/orca-vm/<name>.sh.cmdpowershell
#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.yamlyaml
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.shcreateOrca-server mode — boot the env, start in it, and print serve's result:
orca servejson
{
"schemaVersion": 1,
"pairingCode": "orca-pairing-code-or-url",
"projectRoot": "/absolute/path/to/repo/on/remote",
"userData": { "provider": "example", "resourceId": "provider-resource-id" }
}Here (from ) and are required; ()
and are optional.
pairingCodeorca serve --recipe-jsonprojectRootschemaVersion1userDataSSH mode — do not run ; print the block instead (full shape +
worked script in §7g). is not used in SSH mode.
orca serveconnection.type:"ssh"pairingCodeLifecycle hooks (all run locally):
- : required. Prints recipe result JSON.
create - : optional. Sleep; reads lifecycle payload on stdin.
suspend - : optional. Wake; reads payload on stdin and prints fresh recipe JSON (pairing may change).
resume - : optional unless
destroy. Delete/cleanup; reads payload on stdin.destroy: none
Start Orca remotely with (exact flags + output in §7c). Set to the
externally reachable address so the emitted is reachable; tunneling/port mapping is the
script's job.
orca serve --port "$PORT" --project-root "$ABS_ROOT" --pairing-address "$EXTERNAL_WSS_URL" --recipe-json--pairing-addresspairingCodeBackward compatibility: →, →, →.
Prefer the lifecycle names.
commandcreatecleanupdestroycleanup: nonedestroy: none一旦认证快照存在,每次创建工作区时都会执行此流程。在中定义配方:
orca.yamlyaml
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.shcreateOrca-server模式 —— 启动环境,在其中运行,并输出serve的结果:
orca servejson
{
"schemaVersion": 1,
"pairingCode": "orca-pairing-code-or-url",
"projectRoot": "/absolute/path/to/repo/on/remote",
"userData": { "provider": "example", "resourceId": "provider-resource-id" }
}其中(来自)和是必填项;()和是可选项。
pairingCodeorca serve --recipe-jsonprojectRootschemaVersion1userDataSSH模式 —— 不运行;输出块(完整格式+示例脚本见第7g节)。SSH模式不使用。
orca serveconnection.type:"ssh"pairingCode生命周期钩子(均本地运行):
- :必填。输出配方结果JSON。
create - :可选。休眠;从标准输入读取生命周期负载。
suspend - :可选。唤醒;从标准输入读取负载并输出最新的配方JSON(配对信息可能变更)。
resume - :可选,除非设置
destroy。删除/清理;从标准输入读取负载。destroy: none
使用远程启动Orca(准确参数+输出见第7c节)。将设置为可外部访问的地址,使生成的可访问;隧道/端口映射是脚本的职责。
orca serve --port "$PORT" --project-root "$ABS_ROOT" --pairing-address "$EXTERNAL_WSS_URL" --recipe-json--pairing-addresspairingCode向后兼容性:→,→,→。优先使用生命周期名称。
commandcreatecleanupdestroycleanup: nonedestroy: none9. 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> --jsonorca vm recipe doctor <recipe-id> --repo-path <repo> --jsonLive self-test (--provision
) — diagnose and iterate yourself
--provision在线自测(--provision
)—— 自行诊断并迭代
--provisionorca vm recipe doctor <recipe-id> --repo-path <repo> --provision --jsoncreatedestroydestroyOn failure, the JSON result includes a with the complete captured output of
each stage so you can self-diagnose without asking the user to relay logs:
provisionTranscriptjson
{
"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 / / (and
), fix the script, and re-run until is — iterating on your own
rather than waiting for the user to paste errors. Common reads: a non-empty with
plus a means ran but printed something other than the single recipe-result JSON on
stdout (often a stray — route it to stderr, see §10); a non-zero is a provider/script
failure described in . Each stream is redacted and capped (head+tail) — large logs keep both the
setup context and the failure.
provisionTranscript.provision.stderr.stdout.parseErrordestroy.*--provisionoktruestderrexitCode 0parseErrorcreateechoexitCodestderrThe self-test cannot see provider-side truth beyond what the scripts print, so still confirm: state has a
populated authenticated (Phases 2–3 done), and is implemented/tested (or
explicitly — in which case the self-test won't tear down, so clean up manually).
snapshotIddestroynoneFor SSH recipes, also smoke-test the exact emitted target before declaring success: dial the host/port
with the identity/proxy settings, run , verify the repo path, check the agent binary, and confirm
removes the provider resource/container. For Docker, inspect the auth image entrypoint and do a
startup-only before the full clone/install path.
pwddestroydocker runorca vm recipe doctor <recipe-id> --repo-path <repo> --provision --jsoncreatedestroydestroy失败时,JSON结果包含,其中包含每个阶段的完整捕获输出,你可自行诊断,无需用户转发日志:
provisionTranscriptjson
{
"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": "…" }
}
}循环执行: 读取//(以及),修复脚本并重新执行,直到为——自行迭代,无需等待用户粘贴错误。常见问题:但非空且有,说明运行但标准输出打印了配方结果JSON以外的内容(通常是多余的——需将其重定向到stderr,见第10节);非零是提供商/脚本失败,错误信息在中。每个流都会被脱敏并截断(头部+尾部)——大日志会保留设置上下文和失败信息。
provisionTranscript.provision.stderr.stdout.parseErrordestroy.*--provisionoktrueexitCode 0stderrparseErrorcreateechoexitCodestderr自测无法看到脚本输出以外的提供商端真实信息,因此仍需确认:状态文件中已填充已认证的(已完成第2-3阶段),且已实现/测试(或明确设置为——此时自测不会销毁资源,需手动清理)。
snapshotIddestroynone对于SSH配方,在宣布成功前还需测试输出的目标:使用身份/代理设置连接主机/端口,运行,验证仓库路径,检查Agent二进制文件,并确认会删除提供商资源/容器。对于Docker,在完整克隆/安装前检查认证镜像的入口点并执行仅启动的。
pwddestroydocker run10. 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_ASKPASSso it fails fast instead of prompting.GIT_TERMINAL_PROMPT=0 - helper aborts the clone with "
GIT_ASKPASS". The$1: unbound variable/heredoc that writes the helper insideprintfunderbash -lcexpandedset -u/$1at write time. Escape them ($GH_TOKEN,\$1) so they land literally and resolve at git-runtime; this also keeps the real token out of the file.\$GH_TOKENthe helper afterward (§5, §7f).rm -f - Agent verified as "not logged in" despite a good login. (and similar) print "Logged in …" to stderr; an stdout-only
codex login statusmisses it. Prefer the status exit code; if you grep, fold stderr first (grep) and match the exact success line — notstatus 2>&1 | grep …, which also matches "not logged in".grep -qi 'logged in' - Headless agent login hangs. Plain OAuth starts a loopback callback server on a VM/container port the host browser can't reach. Use the device-auth flow (
login) — it prints a URL + code the user opens on the host.login --device-auth - host-key churn on local Docker. Each ephemeral container regenerating its SSH host key collides on
known_hostsas the published port rotates. Bake host keys into the base image at build time (127.0.0.1; runtime generates only if absent) so all containers share one stable key (§7h).ssh-keygen -A - Snapshot expired/evicted. If hits an unknown snapshot id, rerun Phases 2–3 and update
create.snapshotId - Agent auth didn't persist. Confirm points at the authenticated snapshot; re-run Phase 3. Warn that short-lived tokens may need periodic re-auth.
snapshotId - 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 and
docker image inspect … .Config.Entrypoint. If the image was committed from an interactive shell, reset the entrypoint to the SSH entrypoint duringdocker logs.docker commit - Leaked paid resource. Every long script must trap errors and remove the sandbox it created.
- emits non-JSON on stdout. A stray
createcorrupts the result — stdout is for the final JSON only; everything else to stderr. Theechoself-test surfaces this as--provision+ aexitCode 0with the offending stdout inparseError(§9).provisionTranscript
- 构建超出套餐超时限制(例如Hobby套餐45分钟)。 使用足够的vCPU并设置覆盖构建的超时时间;否则拆分工作或使用更高套餐。此限制也会影响工作区运行时间——需告知用户。
- 构建超出套餐RAM限制。 仅构建无头主进程(移除渲染进程)——这是最有效的方式。
- 私有仓库克隆挂起/失败。 令牌错误/缺失。使用+
GIT_ASKPASS使其快速失败而非提示。GIT_TERMINAL_PROMPT=0 - 助手因"
GIT_ASKPASS"中止克隆。 在$1: unbound variable下使用bash -lc编写助手时,set -u/heredoc在写入时展开了printf/$1。需转义它们($GH_TOKEN、\\$1)使其按字面量写入并在Git运行时解析;这也可避免真实令牌写入文件。之后\\$GH_TOKEN助手文件(第5节、第7f节)。rm -f - 尽管登录成功,但Agent被验证为"未登录"。 (及类似命令)将"Logged in …"打印到stderr;仅读取stdout的
codex login status会错过。优先使用状态退出码;若必须使用grep,先合并stderr(grep)并匹配准确成功行——不要使用status 2>&1 | grep …,因为它也会匹配"未登录"。grep -qi 'logged in' - 无头Agent登录挂起。 普通OAuth 会在虚拟机/容器端口启动环回回调服务器,主机浏览器无法访问。使用设备认证流程(
login)——它会打印URL+验证码,用户在主机上打开即可。login --device-auth - 本地Docker的主机密钥频繁变更。 每个临时容器重新生成SSH主机密钥,当发布端口轮换时会在
known_hosts上冲突。在构建时将主机密钥嵌入基础镜像(127.0.0.1;仅在运行时缺失时生成),使所有容器共享一个稳定密钥(第7h节)。ssh-keygen -A - 快照过期/被回收。 如果命令遇到未知快照ID,重新执行第2-3阶段并更新
create。snapshotId - Agent认证未持久化。 确认指向已认证的快照;重新执行第3阶段。提醒用户短期令牌可能需要定期重新认证。
snapshotId - 从主机复制的Agent认证失效。 不要绑定挂载/复制完整的主机Agent目录;sqlite文件可能不可写或主机特定,钩子可能需要重新批准,配置可能引用仅本地的环境变量。在运行时内完成Agent认证并快照/提交该层。
- Docker认证镜像立即退出。 检查和
docker image inspect … .Config.Entrypoint。如果镜像是从交互式Shell提交的,在docker logs时将入口点重置为SSH入口点。docker commit - 付费资源泄露。 每个长脚本必须捕获错误并删除其创建的沙箱。
- 命令在标准输出中输出非JSON内容。 多余的
create会破坏结果——标准输出仅用于最终JSON;其他内容需输出到标准错误。echo自测会将此显示为--provision+exitCode 0,并在parseError中显示有问题的标准输出(第9节)。provisionTranscript
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 , state, comments, docs, or commits.
userData - 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工作区。",