Merge branch 'skills-fix-env' into skills-optimization

# Conflicts:
#	config/scripts/generate-bundled-skill-guides.test.mjs
#	skill-stubs/orca-per-workspace-env.md
This commit is contained in:
Jinwoo-H
2026-09-04 17:34:30 -04:00
12 changed files with 740 additions and 699 deletions
@@ -26,6 +26,13 @@ const temporaryDirectories = []
const execFileAsync = promisify(execFile)
const GUIDE_REFERENCES = {
'orca-cli': ['automations.md', 'browser.md', 'publishing.md'],
'orca-per-workspace-env': [
'docker-ssh.md',
'failure-modes.md',
'provider-vercel.md',
'ssh-host.md',
'windows-scripts.md'
],
orchestration: [
'coordinator-loop.md',
'legacy-contract-migration.md',
@@ -40,6 +47,17 @@ const GUIDE_REFERENCE_PATHS = Object.entries(GUIDE_REFERENCES).flatMap(([guide,
references.map((reference) => [guide, reference])
)
async function readPerWorkspaceEnvCorpus() {
const guideRoot = path.join(projectDir, 'skill-guides')
const files = [
path.join(guideRoot, 'orca-per-workspace-env.md'),
...GUIDE_REFERENCES['orca-per-workspace-env'].map((reference) =>
path.join(guideRoot, 'orca-per-workspace-env', 'references', reference)
)
]
return (await Promise.all(files.map((file) => readFile(file, 'utf8')))).join('\n')
}
async function createFixture() {
const root = await mkdtemp(path.join(tmpdir(), 'orca-bundled-skill-guides-'))
temporaryDirectories.push(root)
@@ -116,16 +134,27 @@ describe('bundled skill guide generator', () => {
})
it('uses the exported recipe id variable in per-workspace environment examples', async () => {
const source = await readFile(
path.join(projectDir, 'skill-guides', 'orca-per-workspace-env.md'),
// The guide is a kernel plus conditional references, so the env-var contract is asserted over
// the whole corpus while the name-building recipe is pinned in the file that now carries it.
const corpus = await readPerWorkspaceEnvCorpus()
const vercelReference = await readFile(
path.join(
projectDir,
'skill-guides',
'orca-per-workspace-env',
'references',
'provider-vercel.md'
),
'utf8'
)
expect(source).toContain('ORCA_RECIPE_ID')
expect(source).not.toContain('ORCA_VM_RECIPE_ID')
expect(source).toContain('recipe_id="${recipe_id//./-}"')
expect(source).toContain('max_recipe_id_length=$((128 - ${#instance_id} - 6))')
expect(source).toContain('name="orca-${recipe_id:0:max_recipe_id_length}-${instance_id}"')
expect(corpus).toContain('ORCA_RECIPE_ID')
expect(corpus).not.toContain('ORCA_VM_RECIPE_ID')
expect(vercelReference).toContain('recipe_id="${recipe_id//./-}"')
expect(vercelReference).toContain('max_recipe_id_length=$((128 - ${#instance_id} - 6))')
expect(vercelReference).toContain(
'name="orca-${recipe_id:0:max_recipe_id_length}-${instance_id}"'
)
})
it.skipIf(process.platform === 'win32')(
@@ -167,7 +196,13 @@ describe('bundled skill guide generator', () => {
'keeps Vercel sandbox names valid while preserving the instance suffix',
async () => {
const source = await readFile(
path.join(projectDir, 'skill-guides', 'orca-per-workspace-env.md'),
path.join(
projectDir,
'skill-guides',
'orca-per-workspace-env',
'references',
'provider-vercel.md'
),
'utf8'
)
const startMarker = 'recipe_id="${ORCA_RECIPE_ID:-vercel-sandbox}"'
+7 -7
View File
@@ -112,18 +112,18 @@
{
"name": "orca-per-workspace-env",
"sourcePath": "skills/orca-per-workspace-env",
"releaseRevision": 5,
"packageDigest": "9c96ed37a89d4959d05ab1565a81fc80d68f00174c2873b2efb81e20daef8e1d",
"gitTreeSha": "942b9397139f9d5b6cd4164339c965c35494985d",
"releaseRevision": 6,
"packageDigest": "ee28be70b1ae470eb5f40e60d9958b4c7d67538daede95c01f393f3df540cfae",
"gitTreeSha": "6d7468eca7a27f6a378b1390b7a61115bcce5ffc",
"files": [
{
"path": "SKILL.md",
"size": 4222,
"size": 3404,
"executable": false,
"classification": "text",
"exactSha256": "a7ae9a0d22b8bc14a6cb3bdb6fc6ebf1f11cc25ab489d1cc63928bd025d7dddc",
"textNormalizedSha256": "a7ae9a0d22b8bc14a6cb3bdb6fc6ebf1f11cc25ab489d1cc63928bd025d7dddc",
"identitySha256": "a7ae9a0d22b8bc14a6cb3bdb6fc6ebf1f11cc25ab489d1cc63928bd025d7dddc"
"exactSha256": "f1f74bc372e7ac9a85e393ceba4f9ae9c6aae96311515546b16c0c05b07327ac",
"textNormalizedSha256": "f1f74bc372e7ac9a85e393ceba4f9ae9c6aae96311515546b16c0c05b07327ac",
"identitySha256": "f1f74bc372e7ac9a85e393ceba4f9ae9c6aae96311515546b16c0c05b07327ac"
}
]
},
+16
View File
@@ -1857,6 +1857,22 @@
"identitySha256": "a7ae9a0d22b8bc14a6cb3bdb6fc6ebf1f11cc25ab489d1cc63928bd025d7dddc"
}
]
},
{
"releaseRevision": 6,
"packageDigest": "ee28be70b1ae470eb5f40e60d9958b4c7d67538daede95c01f393f3df540cfae",
"gitTreeSha": "6d7468eca7a27f6a378b1390b7a61115bcce5ffc",
"files": [
{
"path": "SKILL.md",
"size": 3404,
"executable": false,
"classification": "text",
"exactSha256": "f1f74bc372e7ac9a85e393ceba4f9ae9c6aae96311515546b16c0c05b07327ac",
"textNormalizedSha256": "f1f74bc372e7ac9a85e393ceba4f9ae9c6aae96311515546b16c0c05b07327ac",
"identitySha256": "f1f74bc372e7ac9a85e393ceba4f9ae9c6aae96311515546b16c0c05b07327ac"
}
]
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
# Local Docker over SSH
Load this when the environment is a local Docker container reached over SSH. It models 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 per-workspace `create` boots from. The emitted result is the SSH shape in
`references/ssh-host.md`.
- Publish container SSH to a random localhost port with `-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, and gitignore the private and public key files.
- **Bake SSH host keys into the base image**, with `ssh-keygen -A` at build time and a runtime step
that generates them only if absent. Every ephemeral container then presents the same host key, so
`known_hosts` on `127.0.0.1` does not churn as the published port rotates across workspaces.
Without this, each container's freshly generated key collides on localhost and trips host-key
changed warnings.
- The auth image is the Docker form of the agent-auth snapshot: the user runs the agent login inside
the container, configures proxy env and config, approves hooks, and you commit once they report it
finished.
- Do not bind-mount or copy the host's full agent home into the image. Let each container keep
writable agent state; only the committed auth image carries reusable authenticated state.
- When committing from an interactive shell, force the runtime entrypoint back to `sshd`:
`docker commit --change='ENTRYPOINT ["/usr/local/bin/orca-docker-ssh-entrypoint"]' …`.
- `destroy` reads `recipeResult.userData.resourceId` and runs `docker rm -f "$resource_id"`.
## Validation before wiring or 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'
```
Inspect the auth image entrypoint and do this startup-only `docker run` before the full clone and
install path. If the container exits immediately, read its logs before the cleanup trap removes it;
an image committed from an interactive shell with `ENTRYPOINT ["bash"]` is a common cause.
Confirm the host key is stable across containers as well: dialing `127.0.0.1` should not trigger a
host-key changed warning when a second container reuses the port. If it does, the host keys were not
baked into the base image.
@@ -0,0 +1,67 @@
# Failure modes
Load this when a doctor, provision, clone, login, or snapshot step failed. Each entry maps an
observed signal to its cause; the rule that prevents it is in the guide next to the action it
protects.
## Reading a failed `--provision` result
The JSON result carries a `provisionTranscript` with the complete captured output of each stage, so
you can 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": "…" }
}
}
```
Each stream is redacted and capped at both ends, so a large log keeps the setup context and the
failure. Two 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 object on stdout. The offending stdout is in the
transcript; the usual cause is a stray `echo`.
- A non-zero `exitCode` is a provider or script failure, described in `stderr`.
## Build and clone
- **Build exceeds the plan timeout**, for example Vercel Hobby's 45 minutes. Use enough vCPUs and a
timeout that covers the build, or split the work, or move to a higher plan. The same cap limits
per-workspace runtime, so surface it to the user.
- **Build exceeds plan RAM.** Building the headless main only, dropping the renderer, is the single
biggest fit.
- **Private-repo clone hangs or fails.** The token is wrong or missing. `GIT_ASKPASS` plus
`GIT_TERMINAL_PROMPT=0` makes it fail fast instead of prompting.
- **The `GIT_ASKPASS` helper aborts the clone with `$1: unbound variable`.** The `printf` or heredoc
that wrote the helper inside `bash -lc` under `set -u` expanded `$1` and `$GH_TOKEN` at write time
instead of leaving them for git-runtime. The same mistake writes the real token into the file.
## Agent auth
- **The agent verifies as "not logged in" despite a good login.** `codex login status` and similar
print their success line to stderr, so a check that reads stdout only misses it.
- **A headless agent login hangs.** Plain OAuth `login` started a loopback callback server on a port
the host browser cannot reach.
- **Agent auth did not persist.** Confirm `snapshotId` points at the authenticated snapshot rather
than the base, and re-run the auth phase. If the agent's credentials are short-lived, the snapshot
needs periodic re-auth; warn the user.
- **Agent auth copied from the host breaks.** A bind-mounted or copied host agent home carries sqlite
files that can be unwritable or host-specific, hooks that need approval again, and config that
references local-only environment variables. Authenticate inside the runtime and snapshot or commit
that layer instead.
## Environment lifecycle
- **`known_hosts` host-key churn on local Docker.** Each ephemeral container regenerated its own SSH
host key, and they collide on `127.0.0.1` as the published port rotates.
- **Snapshot expired or evicted.** `create` hit an unknown snapshot id. Re-run the base and auth
snapshot phases and update `snapshotId` in state.
- **Docker auth image exits immediately.** Read `docker image inspect … .Config.Entrypoint` and
`docker logs`. An image committed from an interactive shell keeps that shell as its entrypoint.
- **A paid resource leaked.** A long script created an environment and then failed without a trap
that removes it.
@@ -0,0 +1,141 @@
# Worked example — Vercel Sandbox
Load this when you are writing the base-snapshot, auth, or per-workspace `create` script for a
snapshot-capable cloud provider. It grounds the generic skeletons in section 7 of the guide with a
real provider surface: `vercel sandbox create|exec|snapshot|remove`. Adapt the names, and verify the
flags against `vercel sandbox --help` for the user's CLI version before relying on them.
This is the Orca-server connection mode: the recipe emits a pairing URL. If the user chose SSH in
the interview, use `references/ssh-host.md` instead.
## Base snapshot
Provision, install tools and clone, build headless, then snapshot.
```bash
# 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
# remote build (long timeout): install pkgs+gh+pnpm+agent CLI, clone with GIT_ASKPASS (the helper's
# \$1/\$GH_TOKEN escaping is load-bearing — see the guide's Credentials section — then
# `rm -f /tmp/askpass.sh`), write the headless main-only build config (drop the renderer), dev setup,
# build CLI + headless main, smoke-check
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)
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)"
# merge { baseName, snapshotId, scope, project, port, repoUrl, repoRef, projectRoot } into state; print state JSON
```
## Agent-auth snapshot
Boot the base, let the user log the agent in, verify, then re-snapshot. `codex` here is an example;
substitute the user's chosen agent's login and status verbs.
```bash
vercel sandbox create --name "$auth" --snapshot "$snapshot_id" --timeout 30m --publish-port "$port" "${vercel_args[@]}" >&2
# The USER runs this in their own terminal and completes the URL/code on the HOST.
vercel sandbox exec --interactive --tty "$auth" "${vercel_args[@]}" -- bash -lc 'codex login --device-auth'
```
Verify by exit code. The remote command turns the status command's exit code into a sentinel because
a provider CLI does not necessarily propagate a remote exit code, and the check is a plain command
with no pipeline, so `set -o pipefail` cannot turn a successful login into a failure:
```bash
verdict="$(vercel sandbox exec "$auth" "${vercel_args[@]}" --timeout 30s \
-- bash -lc 'if codex login status >/dev/null 2>&1; then echo ORCA_AGENT_LOGGED_IN; else echo ORCA_AGENT_LOGGED_OUT; fi')"
case "$verdict" in
*ORCA_AGENT_LOGGED_IN*) ;;
*) echo "agent not logged in; not snapshotting" >&2; exit 1 ;;
esac
```
Fallback, for an agent CLI whose `status` verb does not signal auth through its exit code. Capture
the output with stderr folded in, then match the exact success line the agent prints. The match runs
against a shell variable rather than through a pipe, so no upstream provider process can take
SIGPIPE:
```bash
status="$(vercel sandbox exec "$auth" "${vercel_args[@]}" --timeout 30s -- bash -lc 'codex login status 2>&1')"
grep -Eq 'Logged in using ChatGPT|Logged in via device' <<<"$status" \
|| { echo "agent not logged in; not snapshotting" >&2; exit 1; }
```
Then re-snapshot and record the new id:
```bash
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)"
# overwrite state.snapshotId = new_id, record authSourceSnapshotId = snapshot_id; remove the auth sandbox
```
## Per-workspace `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
vercel_args=(); [ -n "$scope" ] && vercel_args+=(--scope "$scope"); [ -n "$project" ] && vercel_args+=(--project "$project")
[ -n "$snapshot_id" ] || { echo "snapshotId missing — build the base and auth snapshots first" >&2; exit 1; }
gh_token="${GH_TOKEN:-${GITHUB_TOKEN:-$(command -v gh >/dev/null 2>&1 && gh auth token 2>/dev/null || true)}}"
recipe_id="${ORCA_RECIPE_ID:-vercel-sandbox}"
recipe_id="${recipe_id//./-}" # Vercel names forbid dots.
instance_id="${ORCA_VM_INSTANCE_ID:-$(date +%s)}"
max_recipe_id_length=$((128 - ${#instance_id} - 6)) # Preserve the unique instance suffix.
[ "$max_recipe_id_length" -gt 0 ] || { echo "ORCA_VM_INSTANCE_ID is too long for a Vercel sandbox name" >&2; exit 1; }
name="orca-${recipe_id:0:max_recipe_id_length}-${instance_id}"
# 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
# 1. boot from the authenticated snapshot, publish the serve port
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
# Vercel prints the published https URL; derive the external wss:// pairing address from it
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://}"
# 2. (remote) ensure the repo is at the right commit; rebuild only if the commit changed (cache marker)
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"; \
# Escaping is load-bearing here: re-test the fetch after any edit to the nested quoting.
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
# 3. (remote) start orca serve in the background, writing recipe JSON to a file; poll until it parses
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)
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`, and `destroy` run `vercel sandbox stop|...|remove "$resource_id"`, reading
`userData.resourceId` from the lifecycle payload on stdin.
The `128` in `max_recipe_id_length` is Vercel's sandbox name cap. Confirm it against
`vercel sandbox create --help` or Vercel's docs for the user's CLI version before relying on it; a
wrong cap silently truncates recipe ids in resource names.
@@ -0,0 +1,138 @@
# SSH connection mode, including provisioned root
Load this when the recipe connects over SSH instead of starting `orca serve`, and when the user has
explicitly asked for `checkoutMode: provisioned-root`.
SSH mode is not a relabeling of the Orca-server templates. `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 and filesystem providers, and imports the repo. The script's only job is to make the host ready
and print the SSH connection details Orca dials.
## The result shape
Orca rejects anything else. This carries only the required fields; add optionals from the next
section as the network actually needs them.
```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"
}
}
}
```
`label`, `host`, `port`, and `username` are required. `projectRoot` is an absolute path on the host.
## Which optional `target` fields to set
These describe how the user's desktop reaches the box; there is no `orca serve` URL in SSH mode.
- A public IP or DNS name, or a Tailscale or VPN address, is the `host`; the SSH port is `port`,
usually 22.
- Key auth sets `identityFile`. Add `"identitiesOnly": true` when the agent holds many keys.
- Reaching the box through a bastion is either `jumpHost`, a `user@host` ProxyJump, or
`proxyCommand`, a full command such as an access proxy. **Set one, never both.** The schema
accepts both, and the two consumers then disagree: one pushes `-J` and `-o ProxyCommand=` into the
same argv, the other resolves `proxyCommand` and ignores `jumpHost` entirely.
- A service port the workspace needs is an entry in `portForwards`. Each entry requires
`localPort`, `remoteHost`, and `remotePort`, and takes an optional `label`. The entry schema is
strict, so an invented key such as `local` or `remote` fails validation.
- `relayGracePeriodSeconds` bounds how long Orca keeps the SSH relay alive after the workspace
detaches. **`0` means unbounded**: the relay stays up until something explicitly terminates it, so
it is the wrong value for a disposable runtime. Any other value must be between 60 and 604800
seconds; a number in between, such as `30`, is rejected and takes the whole recipe result with it.
Omit the field unless the user asked for a specific reconnect grace window.
## Toolchain and agent auth on a persistent host
A no-snapshot host has no base image to bake, because the host is the base. Run the install steps
and the agent's device-auth login directly over SSH on the host once, by hand, before wiring the
recipe. The login is interactive, for example `ssh -t user@host '<agent> login --device-auth'`, so
the user runs it. After that the host stays ready across workspaces.
## The create script
```bash
#!/usr/bin/env bash
set -euo pipefail
# resolve from env→state→fallback (default unset optionals to ""): ssh_username, host,
# ssh_port (default 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")
# A fresh host's key isn't in known_hosts, and a StrictHostKeyChecking prompt HANGS a
# non-interactive create. Pre-add the key (or set the option) so it can't block.
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)
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
# relay how to dial in; identityFile/jumpHost/proxyCommand/portForwards are emitted when set.
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=[{localPort,remoteHost,remotePort}] here if the workspace needs them
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"
```
On a persistent host there is usually nothing to tear down, so set `destroy: none` and omit suspend
and resume. Orca still disconnects and reconnects its own SSH relay on sleep, wake, and delete, which
is separate from these scripts.
If the SSH host is instead an ephemeral, snapshot-capable VM — the user's hypervisor, or a cloud VM
with image support — keep the base-image model from `references/provider-vercel.md` for
provisioning, but still emit the `connection.type:"ssh"` block above instead of starting
`orca serve`.
## Provisioned root
For an explicitly requested one-VM-per-workspace checkout, the create script reads
`ORCA_RECIPE_RESULT_SCHEMA_VERSION`, `ORCA_REPO_URL`, `ORCA_REPO_REF`, `ORCA_REPO_REF_HEAD`, and
`ORCA_REPO_BRANCH`. Use `ORCA_REPO_REF` to fetch the selected source, but create `ORCA_REPO_BRANCH`
at the exact `ORCA_REPO_REF_HEAD` commit, because resolving the symbolic ref again can race with an
upstream update. `ORCA_REPO_URL` and `ORCA_REPO_REF` are a matched fetch pair, and the URL is the
remote Orca resolved the base ref against, which is not necessarily named `origin` on the desktop.
Fetch from the URL the pair supplies:
```bash
[ -n "${ORCA_REPO_REF_HEAD:-}" ] || { echo "missing pinned source commit" >&2; exit 1; }
git fetch "$ORCA_REPO_URL" "$ORCA_REPO_REF"
git cat-file -e "${ORCA_REPO_REF_HEAD}^{commit}"
git checkout -B "$ORCA_REPO_BRANCH" "$ORCA_REPO_REF_HEAD"
```
Return that primary checkout at `projectRoot` and emit schema version 2:
```json
{
"schemaVersion": 2,
"checkoutMode": "provisioned-root",
"connection": {
"type": "ssh",
"projectRoot": "/abs/repo",
"target": { "label": "my-box", "host": "192.0.2.10", "port": 22, "username": "ubuntu" }
}
}
```
## Before declaring an SSH recipe done
The `--provision` self-test only sees what the scripts print, so smoke-test the exact emitted target
as well: dial the host and port with the identity or proxy settings, run `pwd`, verify the repo path,
check the agent binary, and confirm `destroy` removes the provider resource.
@@ -0,0 +1,23 @@
# Windows local-side scripts
Load this when the user's desktop is Windows and you are scaffolding the local-side scripts. A bare
`.sh` will not execute there. Either require WSL or Git Bash and point `orca.yaml` at a launcher such
as `bash ./scripts/orca-vm/<name>.sh` through a `.cmd` file, or scaffold PowerShell equivalents.
The remote-side commands you run inside the Linux environment stay bash regardless of the desktop OS.
```powershell
#requires -Version 5
$ErrorActionPreference = 'Stop'
# resolve env→state→fallback; run the provider CLI / ssh the same way;
# capture provider output; build the result object for the chosen mode and write ONE line of JSON to stdout.
# Orca-server mode: @{ schemaVersion=1; pairingCode=$pairingCode; projectRoot=$projectRoot; userData=@{...} }
# SSH mode: @{ schemaVersion=1; connection=@{ type="ssh"; projectRoot=$projectRoot;
# target=@{ label=$label; host=$host; port=$port; username=$user } } }
($result | ConvertTo-Json -Compress -Depth 6)
# progress/errors → Write-Error / the error stream, never stdout.
```
The doctor's executable-bit check is a POSIX concept and is skipped on Windows, so a script that is
unusable on the user's machine for a different reason still has to be caught by the `--provision`
self-test.
+1 -11
View File
@@ -4,16 +4,6 @@ This file is a discovery stub, not the usage guide. The full, version-matched pe
environment reference is served by the `orca` binary itself — kept out of this file on
purpose so it can never drift from the binary that will actually run your commands.
Engage Orca whenever you set up, review, debug, or validate a per-workspace environment
recipe — the on-demand, disposable runtimes (cloud sandboxes, VMs, or local) created fresh
for each workspace. This covers first-time setup (provider prerequisites, the reusable base
snapshot, the coding-agent auth snapshot, credentials, and state), not just the
per-workspace lifecycle scripts. Use it to stand up per-workspace environments, fix an
`environmentRecipes` entry in `orca.yaml`, scaffold provider lifecycle scripts, or resolve
an `orca vm recipe doctor` failure. Orca is a thin wrapper: you guide, detect, and scaffold;
you never own the user's cloud account, billing, images, or credentials, and never spend
money without an explicit user OK.
<!-- shared: resolver -->
## Load the full guide before running Orca commands
@@ -37,6 +27,6 @@ ORCA vm recipe doctor <recipe-id> --repo-path <repo> --json
```
The doctor command above is the free static check. Never add `--provision` without the
user's explicit approval because it creates provider resources and may spend money.
user's explicit approval: it creates provider resources and spends the user's cloud money.
<!-- shared: older-binary-outro -->
+7 -18
View File
@@ -1,13 +1,12 @@
---
name: orca-per-workspace-env
description: >-
Set up, review, debug, or validate Orca per-workspace environment recipes —
on-demand, disposable runtimes (cloud sandboxes, VMs, or local) created fresh
for each workspace. Covers first-time setup (provider prerequisites, the
reusable base snapshot, the coding-agent auth snapshot, credentials, and
state), not just the per-workspace lifecycle scripts. Use to stand up
per-workspace environments, fix an `environmentRecipes` entry in `orca.yaml`, scaffold
provider lifecycle scripts, or resolve an `orca vm recipe doctor` failure.
Set up, review, debug, or validate an Orca per-workspace environment recipe: the
on-demand, disposable runtime (cloud sandbox, VM, SSH host, or local container)
Orca creates fresh for each workspace. Use to stand up a new recipe end to end,
fix an `environmentRecipes` entry in `orca.yaml`, scaffold provider lifecycle
scripts, or resolve an `orca vm recipe doctor` failure. Use `orca-cli` for
ordinary worktree and workspace creation with no recipe involved.
---
# Per-Workspace Environments
@@ -16,16 +15,6 @@ This file is a discovery stub, not the usage guide. The full, version-matched pe
environment reference is served by the `orca` binary itself — kept out of this file on
purpose so it can never drift from the binary that will actually run your commands.
Engage Orca whenever you set up, review, debug, or validate a per-workspace environment
recipe — the on-demand, disposable runtimes (cloud sandboxes, VMs, or local) created fresh
for each workspace. This covers first-time setup (provider prerequisites, the reusable base
snapshot, the coding-agent auth snapshot, credentials, and state), not just the
per-workspace lifecycle scripts. Use it to stand up per-workspace environments, fix an
`environmentRecipes` entry in `orca.yaml`, scaffold provider lifecycle scripts, or resolve
an `orca vm recipe doctor` failure. Orca is a thin wrapper: you guide, detect, and scaffold;
you never own the user's cloud account, billing, images, or credentials, and never spend
money without an explicit user OK.
## Resolve the CLI for this session
Choose the executable once and reuse it for every later command:
@@ -74,7 +63,7 @@ ORCA vm recipe doctor <recipe-id> --repo-path <repo> --json
```
The doctor command above is the free static check. Never add `--provision` without the
user's explicit approval because it creates provider resources and may spend money.
user's explicit approval: it creates provider resources and spends the user's cloud money.
Then tell the user that updating Orca restores the full, version-matched guide via
`ORCA skills get orca-per-workspace-env`. Beyond these commands, ask the user rather than
File diff suppressed because one or more lines are too long