mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
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:
@@ -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}"'
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user