ubicloud: pull destinations are rechecked at pull time, verify parses options by arity, a finished command keeps its status, the remote sid outlives the wrapper; docs audit

Pass-2 (Codex adversarial on the delta): an earlier pull could plant a symlink
where a later config pull lands, so under verify every destination is resolved
again (symlinks followed) right before its pull and a refused pull exits 1;
'--dest --trust-config' made an option value act as the trust flag, so verify
parses by arity; a command that finished in the instant before the watchdog's
kill exited 0 and was still reported as 124; the wrapper removed the sid file
while a background child kept the channel open, so the file now stays until
remote_cancel reads it. Docs audit (document-release, edit mode): TTL suffix and
owner rule in TESTING_INTERNALS, the reaper as the exception to 'no sweep', the
docs/skills.md transcript uses a neutral owner; CHANGELOG and the runner header
corrected on the two points it deferred.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 19:11:00 +00:00
1 parent e79209c47e
commit 7c92dbeece
7 files changed
+92 -28

No files matched your search

+3 -3
View File
@@ -7,10 +7,10 @@
### Added
- **`ubicloud/SKILL.md.tmpl`, `bin/gstack-ubi`**: the skill: set/unset prerequisite checks (never `env`), `UBI_OWNER` on every machine, a watched launch (a waited-on background operation or `gstack-detach`), the quoted recovery line, one committed verify per repository, how to read exit 124 and a quota refusal, and the resume checklist. Classified `CLAUDE_ONLY` in `lib/doctor-components.ts`; routed from the root `SKILL.md`; listed in README, AGENTS.md, docs/skills.md and docs/PROJECT_STRUCTURE.md.
- **TTL names and a reaper**: a generated VM name ends in `-t<hours>` (`UBI_TTL_HOURS`, default 4; 0 omits it). `reap` destroys every VM in the project whose TTL ended more than ten minutes ago, whichever owner created it, and every `up` runs it first (`UBI_REAP=0` skips it). Names without a TTL, including the ones `pregate-remote.sh` builds, are never reaped.
- **Quota preflight**: `up` adds the size's vCPUs to the project's running VMs and refuses before the create request when that exceeds the known ceiling (`UBI_QUOTA_VCPUS`, or the maximum the last API refusal printed, remembered in the state root); `UBI_QUOTA_WAIT=<seconds>` polls for room instead of refusing. The per-owner `usage` table is printed either way.
- **Quota preflight**: `up` adds the size's vCPUs to the project's running VMs and refuses before the create request when that exceeds the known ceiling (`UBI_QUOTA_VCPUS`, or the maximum the last API refusal printed, remembered in the state root); `UBI_QUOTA_WAIT=<seconds>` polls for room instead of refusing. Both refusals (the preflight's and the API's) print the per-owner `usage` table.
- **Watchdog and heartbeat**: `run`, `verify` and `ssh NAME COMMAND` stream the remote output (both streams kept in the VM's state dir) through a watchdog: a heartbeat every `UBI_HEARTBEAT` seconds (300), and a command silent for `UBI_IDLE_TIMEOUT` seconds (1800) or older than `UBI_RUN_TIMEOUT` (0, unlimited) is killed in its own session with exit 124 and its last 40 lines on stderr. `run` also prints the VM's TTL and its `down` recovery command as soon as the VM is ready.
- **`verify`**: runs the repository's committed `.gstack/ubicloud.json` (setup, command, size, location, storage, image, env, pass, pull; flags and a trailing `-- COMMAND` override it) and refuses without the file or a command, so every lane runs one definition instead of its own wrapper. The file is repository content and gets no reach beyond the checkout: `setup` and each `pull` destination must be relative paths that resolve inside `--src` (absolute, `~` and `..` paths are refused before any API call), and a name under `pass` is forwarded only when the command line grants it (`--pass NAME`, or every listed name with `--trust-config`). `--pass` never forwards `UBICLOUD_*`, from the file or the command line.
- **Watched commands keep their stdin and exit codes**: `ssh NAME COMMAND` hands the caller's stdin to the remote command (`printf payload | gstack-ubi ssh NAME 'cat > file'` wrote an empty file once the watchdog backgrounded the client), a command that finished while the watchdog read the clock is never reported as a timeout, a hung setup script exits 124 like a hung command instead of 1, Ctrl-C on a watched command takes the ssh session down with it, and when the watchdog fires the runner also ends the command's session on the VM (`setsid` there, leader pid in `~/.ubirun-<phase>-<runner pid>.sid`, `pkill -s` bounded to 20 s), since killing the local client alone leaves a silent remote command running until it next writes.
- **`verify`**: runs the repository's committed `.gstack/ubicloud.json` (setup, command, size, location, storage, image, env, pass, pull; flags and a trailing `-- COMMAND` override it) and refuses without the file or a command, so every lane runs one definition instead of its own wrapper. The file is repository content and gets no reach beyond the checkout: `setup` and each `pull` destination must be relative paths that resolve inside `--src` (absolute, `~` and `..` paths are refused before any API call, and under `verify` every pull destination is resolved again, symlinks followed, right before its pull, so a symlink an earlier pull planted cannot redirect a later one; a refused pull makes the run exit 1), and a name under `pass` is forwarded only when the command line grants it (`--pass NAME`, or every listed name with `--trust-config`; `verify` parses its options by arity, so an option value can never act as that flag). `--pass` never forwards `UBICLOUD_*`, from the file or the command line.
- **Watched commands keep their stdin and exit codes**: `ssh NAME COMMAND` hands the caller's stdin to the remote command (`printf payload | gstack-ubi ssh NAME 'cat > file'` wrote an empty file once the watchdog backgrounded the client), a command that finished while the watchdog read the clock, or in the instant before its kill, keeps its own exit status instead of 124, a hung setup script exits 124 like a hung command instead of 1, Ctrl-C on a watched command takes the ssh session down with it, and when the watchdog fires the runner also ends the command's session on the VM (`setsid` there, leader pid in `~/.ubirun-<phase>-<runner pid>.sid`, kept until the cancel reads it so a background child the command left behind is still reachable, `pkill -s` bounded to 20 s), since killing the local client alone leaves a silent remote command running until it next writes.
- **Quota ceiling and reaper outcomes**: the maximum a refusal cached is trusted for a day, then the API's own answer decides again; the refusal line says where the ceiling came from and names `UBI_QUOTA_VCPUS`, and its `vCPU quota exhausted` wording is the one `pregate-remote.sh` retries once on. `reap` returns 1 when the list fails (nothing destroyed) or an expired VM could not be destroyed, instead of 0 either way; a TTL suffix is read in base 10 (`UBI_TTL_HOURS` takes 0 to 999 with no leading zero).
### Tests
+10 -7
View File
@@ -962,16 +962,18 @@ the CSO private-state checks), AppArmor blocks the unprivileged user
namespaces Chromium's sandbox needs, and `clang` and `python3-venv` are absent
(required by the Dia readiness and Python runner tests).
VMs are named `ubirun-<owner>-<epoch>-<hex>`; `<owner>` is `UBI_OWNER` (set it
to the thread id on a Capy machine), else a stable per-machine id. Every exit
VMs are named `ubirun-<owner>-<epoch>-<hex>`, plus a `-t<hours>` TTL suffix
(below); `<owner>` is `UBI_OWNER` (set it to the thread id on a Capy machine;
lowercase letters and digits, at most 12), else a stable per-machine id. Every exit
path destroys the client's own VM (`trap` on EXIT, INT, TERM, HUP and QUIT), and
`down` fails loudly (`FAILED to destroy <location>/<name> (HTTP <code>)`) unless
the VM is confirmed gone. `list --mine` shows this owner's VMs, `usage` the
project's, and `records` the VM-id scratch file (`UBI_VM_RECORD`, default
`~/.capy/work/ubi-runner/vms.tsv`, else `<state root>/vms.tsv`): every VM is
recorded there before the create call and forgotten only after a confirmed
destroy, so a killed runner leaves a name to clean up. Nothing sweeps stale VMs
by default, because the project quota is shared with other agents' runners; set
destroy, so a killed runner leaves a name to clean up. No age-based sweep runs
by default, because the project quota is shared with other agents' runners (the
TTL reaper below destroys only a VM whose own `-t<hours>` TTL has ended); set
`UBI_GC_HOURS` to a positive number (or run `gc HOURS`; `gc 0` is a no-op) to
sweep only this owner's VMs older than that. For other commands, use the runner
directly: `scripts/ubicloud/ubi-runner.sh run --setup <script> -- '<command>'`,
@@ -1017,9 +1019,10 @@ machine swap, in this order:
on the base and on the candidate from the same VM size and location, never
one side local and the other remote.
4. Profiling very large imports (a whole-repo ingest, the full free suite under
a profiler) is warned about before it starts: it holds the VM past
`UBI_GC_HOURS` and the sentinel arrives hours later; name the expected
duration in the thread before launching.
a profiler) is warned about before it starts: it holds the VM for hours, and
any other lane's `up` reaps it once its TTL (`UBI_TTL_HOURS`, default 4)
plus ten minutes has passed, so set `UBI_TTL_HOURS` above the expected
duration and name that duration in the thread before launching.
## Cloud sandboxes (Vercel / Conductor cloud workspaces)
+4 -4
View File
@@ -894,14 +894,14 @@ runner destroys its VM on every exit path only while it is alive.
```
You: /ubicloud run the full verify
Claude: UBICLOUD_API_KEY: set; UBI_OWNER=gsta114
Claude: UBICLOUD_API_KEY: set; UBI_OWNER=lane7
gstack-ubi verify (.gstack/ubicloud.json: bootstrap.sh, "bash scripts/verify.sh", standard-16)
ubi-runner: reap: no expired VMs
ubi-runner: creating eu-central-h1/ubirun-gsta114-1761500000-9f3a2c10-t4 (standard-16)
ubi-runner: ubirun-gsta114-1761500000-9f3a2c10-t4 carries a 4h TTL (any later run of this script reaps it); if this process dies first: gstack-ubi down ubirun-gsta114-1761500000-9f3a2c10-t4
ubi-runner: creating eu-central-h1/ubirun-lane7-1761500000-9f3a2c10-t4 (standard-16)
ubi-runner: ubirun-lane7-1761500000-9f3a2c10-t4 carries a 4h TTL (any later run of this script reaps it); if this process dies first: gstack-ubi down ubirun-lane7-1761500000-9f3a2c10-t4
ubi-runner: heartbeat: command 300s elapsed, last output 2s ago
ubi-runner: command exited 0 after 1140s
ubi-runner: destroyed eu-central-h1/ubirun-gsta114-1761500000-9f3a2c10-t4
ubi-runner: destroyed eu-central-h1/ubirun-lane7-1761500000-9f3a2c10-t4
```
---
+43 -10
View File
@@ -23,7 +23,7 @@
# Needs only bash, curl, python3, ssh, ssh-keygen, and tar locally, so it works
# from dev boxes, containers, and cloud sandboxes.
#
# Ownership: VMs are named ubirun-<owner>-<epoch>-<suffix>. <owner> is UBI_OWNER
# Ownership: VMs are named ubirun-<owner>-<epoch>-<suffix>[-t<hours>]. <owner> is UBI_OWNER
# (lowercased, letters and digits only, at most 12 characters, starting with a
# letter) or a random per-machine id kept in $STATE_ROOT/owner-id. Ubicloud names
# allow 63 characters of [a-z0-9-], so a full name stays under 40.
@@ -188,7 +188,7 @@ sq() { printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g")"; }
# writes to the closed channel.
remote_cancel() {
local name=$1 sidf=$2 cpid i=0
remote "$name" "sid=\$(cat $sidf 2>/dev/null); [ -z \"\$sid\" ] || { pkill -TERM -s \"\$sid\"; sleep 2; pkill -KILL -s \"\$sid\"; }; true" >/dev/null 2>&1 </dev/null &
remote "$name" "sid=\$(cat $sidf 2>/dev/null); rm -f $sidf; [ -z \"\$sid\" ] || { pkill -TERM -s \"\$sid\"; sleep 2; pkill -KILL -s \"\$sid\"; }; true" >/dev/null 2>&1 </dev/null &
cpid=$!
while kill -0 "$cpid" 2>/dev/null && [ "$i" -lt 20 ]; do sleep 1; i=$(( i + 1 )); done
kill "$cpid" 2>/dev/null || true
@@ -211,7 +211,10 @@ watched() {
out="$(state_dir "$name")/$phase.log"
: >"$out"
inner=$(sq "printf %s \$\$ >$sidf; $cmd")
cmd="if command -v setsid >/dev/null 2>&1; then setsid -w bash -c $inner; else bash -c $inner; fi; rc=\$?; rm -f $sidf; exit \$rc"
# The sid file outlives the command on purpose: a background child it left
# behind keeps the ssh channel open after the wrapper exits, and remote_cancel
# still needs the session id to end it. remote_cancel removes the file.
cmd="if command -v setsid >/dev/null 2>&1; then setsid -w bash -c $inner; else bash -c $inner; fi"
# The caller's stdin is handed to the background ssh explicitly (a background
# job in a non-interactive shell would otherwise read /dev/null), so piped
# input reaches the remote command as it did before the watchdog.
@@ -245,8 +248,14 @@ watched() {
log "watchdog: $phase $why; killing it. Last output:"
tail -n 40 "$out" >&2
kill -TERM -- "-$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || rc=$?
trap - INT TERM; eval "$old_traps"
# A command killed by the watchdog never exits 0; a 0 here means it
# finished in the instant before the kill, and that result stands.
if [ "$rc" -eq 0 ]; then
log "watchdog: $phase finished before it could be killed; its exit 0 stands"
return 0
fi
remote_cancel "$name" "$sidf"
return 124
fi
@@ -632,6 +641,19 @@ pass_name() {
case $1 in UBICLOUD_*) die "--pass $1: the runner never forwards its own Ubicloud credentials to a VM" ;; esac
}
# inside_dir DIR ROOT: DIR exists (created if needed) and resolves, symlinks
# followed, to ROOT or below it.
inside_dir() {
local a b
mkdir -p "$1" 2>/dev/null || return 1
a=$(cd "$1" 2>/dev/null && pwd -P) || return 1
b=$(cd "$2" 2>/dev/null && pwd -P) || return 1
[ "$a" = "$b" ] || [[ "$a" = "$b"/* ]]
}
# PULL_ROOT: set by verify; every pull destination must resolve inside it at
# pull time (an earlier pull may have placed a symlink where a later one lands).
PULL_ROOT=""
cmd_run() {
local up_args=() src=. dest="" setup="" keep=0 pulls=() envs=()
while [ $# -gt 0 ]; do
@@ -696,6 +718,11 @@ cmd_run() {
local p
for p in ${pulls[@]+"${pulls[@]}"}; do
if [ -n "$PULL_ROOT" ] && ! inside_dir "${p#*:}" "$PULL_ROOT"; then
log "pull refused: ${p#*:} resolves outside $PULL_ROOT"
[ "$rc" -ne 0 ] || rc=1
continue
fi
cmd_pull "$name" "${p%%:*}" "${p#*:}" || log "pull failed: $p"
done
return "$rc"
@@ -762,12 +789,16 @@ print("cfg_cmd=" + q(c.get("command", "")))
rm -f "$err"
eval "$parsed"
while [ $# -gt 0 ]; do
if [ "$1" = -- ]; then shift; user_cmd=("$@"); break; fi
case $1 in
--trust-config) trust=1; shift; continue ;;
--pass) granted+=("${2:-}") ;;
--) shift; user_cmd=("$@"); break ;;
--trust-config) trust=1; shift ;;
--keep) user_pre+=("$1"); shift ;;
--pass|-S|--storage|-s|--size|-l|--location|-n|--name|-b|--image|--src|--dest|--setup|--env|--pull)
[ $# -ge 2 ] || die "verify: $1 needs a value"
[ "$1" != --pass ] || granted+=("$2")
user_pre+=("$1" "$2"); shift 2 ;;
*) die "verify: unknown option $1" ;;
esac
user_pre+=("$1"); shift
done
for n in ${cfg_pass[@]+"${cfg_pass[@]}"}; do
pass_name "$n"
@@ -779,6 +810,7 @@ print("cfg_cmd=" + q(c.get("command", "")))
[ ${#user_cmd[@]} -gt 0 ] || [ -n "$cfg_cmd" ] || die "verify: $cfg names no command and none was given after --"
[ ${#user_cmd[@]} -gt 0 ] || user_cmd=("$cfg_cmd")
log "verify: $cfg"
PULL_ROOT=$(cd "$src" && pwd)
cmd_run ${cfg_args[@]+"${cfg_args[@]}"} ${cfg_setup:+--setup "$cfg_setup"} --src "$src" ${user_pre[@]+"${user_pre[@]}"} -- "${user_cmd[@]}"
}
@@ -791,8 +823,9 @@ usage: ubi-runner.sh <command> [args]
up + sync + setup + command + pull + destroy; exits with COMMAND's status
verify [run opts] [--trust-config] [-- COMMAND]
run the repository's committed verify (.gstack/ubicloud.json under --src);
its setup and pull paths stay inside --src, and its pass names are
forwarded only when granted here (--pass NAME, or all: --trust-config)
its setup script and every pull destination stay inside --src (checked
again at pull time), and its pass names are forwarded only when granted
here (--pass NAME, or all: --trust-config)
up [-s SIZE] [-l LOCATION] [-n NAME] [-S STORAGE_GIB] [-b IMAGE]
create a VM and wait for SSH; prints its name
(STORAGE_GIB: one of $ALLOWED_STORAGE)
+28 -2
View File
@@ -6,7 +6,7 @@
* call when the token is absent.
*/
import { describe, expect, test } from 'bun:test';
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, utimesSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
@@ -333,6 +333,7 @@ describe('ubi-runner run, verify and quota against a fake API and a fake ssh', (
' *"cat > ~/.ubirun-env"*) cat > "$root/ubirun-env" ;;\n' +
' *"cat > ~/.ubirun-setup.sh"*) cat > "$root/ubirun-setup.sh" ;;\n' +
' *"tar -xzf -"*) cat > /dev/null ;;\n' +
' *"tar -czf -"*) base=${cmd##* }; tar -czf - -C "$root/remote-fs" "$base" ;;\n' +
' *"bash -l ~/.ubirun-setup.sh"*) echo "setup start"; if [ "$(cat "$root/setup-mode")" = hang ]; then sleep 20; fi; echo "setup done" ;;\n' +
' *"bash -lc "*) if [ -f "$root/command-mode" ]; then bash -c "$(cat "$root/command-mode")"; else echo "ran: $cmd"; fi ;;\n' +
' *) : ;;\n' +
@@ -372,7 +373,8 @@ describe('ubi-runner run, verify and quota against a fake API and a fake ssh', (
expect(err).toContain('setup failed (exit 124)');
expect(f.ssh()).not.toContain('bash -lc');
expect(readFileSync(join(f.root, 'cancel.log'), 'utf8')).toMatch(/cat ~\/\.ubirun-setup-\d+\.sid/);
expect(f.ssh()).toMatch(/setsid -w bash -c 'printf %s \$\$ >~\/\.ubirun-setup-\d+\.sid; cd work\/[^ ]+ && \. ~\/\.ubirun-env && bash -l ~\/\.ubirun-setup\.sh'; else bash -c '[^']*'; fi; rc=\$\?; rm -f ~\/\.ubirun-setup-\d+\.sid; exit \$rc/);
expect(f.ssh()).toMatch(/setsid -w bash -c 'printf %s \$\$ >~\/\.ubirun-setup-\d+\.sid; cd work\/[^ ]+ && \. ~\/\.ubirun-env && bash -l ~\/\.ubirun-setup\.sh'; else bash -c '[^']*'; fi$/m);
expect(readFileSync(join(f.root, 'cancel.log'), 'utf8')).toMatch(/rm -f ~\/\.ubirun-setup-\d+\.sid; \[ -z "\$sid" \] \|\| \{ pkill -TERM -s "\$sid"; sleep 2; pkill -KILL -s "\$sid"; \}/);
expect(f.calls()).toContain('destroy');
expect(err).toMatch(/destroyed eu-central-h1\/ubirun-thrd1-\d{10}-[0-9a-f]{8}-t4/);
expect(f.listed()).toBe('');
@@ -440,6 +442,13 @@ describe('ubi-runner run, verify and quota against a fake API and a fake ssh', (
const trusted = run(f, ['verify', '--src', dir, '--trust-config'], { UBI_TEST_TOKEN: 'trusted value' });
expect(trusted.exitCode).toBe(0);
expect(readFileSync(join(f.root, 'ubirun-env'), 'utf8')).toContain('export UBI_TEST_TOKEN=trusted\\ value');
rmSync(join(f.root, 'ubirun-env'));
const asValue = run(f, ['verify', '--src', dir, '--dest', '--trust-config', '--keep', '--dest', 'work/safe'], { UBI_TEST_TOKEN: 'x' });
expect(asValue.exitCode).toBe(1);
expect(asValue.stderr.toString()).toContain('asks to forward local UBI_TEST_TOKEN');
expect(existsSync(join(f.root, 'ubirun-env'))).toBe(false);
expect(run(f, ['verify', '--src', dir, '--pass']).stderr.toString()).toContain('verify: --pass needs a value');
expect(run(f, ['verify', '--src', dir, '--bogus']).stderr.toString()).toContain('verify: unknown option --bogus');
writeFileSync(join(dir, '.gstack/ubicloud.json'), JSON.stringify({ command: 'make verify', pass: ['UBICLOUD_API_KEY'] }));
const before = f.calls();
const key = run(f, ['verify', '--src', dir, '--trust-config']);
@@ -478,6 +487,23 @@ describe('ubi-runner run, verify and quota against a fake API and a fake ssh', (
} finally { rmSync(f.root, { recursive: true, force: true }); }
}, 90_000);
test('verify rechecks every pull destination at pull time: a symlink an earlier pull planted cannot redirect a later pull outside --src', () => {
const f = fakeVm();
try {
const dir = src(f, { command: 'make verify', pull: ['work/repo/escape:.', 'work/repo/artifacts:escape'] });
const victim = join(f.root, 'victim'); mkdirSync(victim);
mkdirSync(join(f.root, 'remote-fs/artifacts'), { recursive: true });
writeFileSync(join(f.root, 'remote-fs/artifacts/authorized_keys'), 'ssh-ed25519 AAAA attacker\n');
symlinkSync(victim, join(f.root, 'remote-fs/escape'));
const r = run(f, ['verify', '--src', dir]);
expect(r.exitCode).toBe(1);
expect(r.stderr.toString()).toMatch(/pull refused: .*\/repo\/escape resolves outside .*\/repo/);
expect(lstatSync(join(dir, 'escape')).isSymbolicLink()).toBe(true);
expect(readdirSync(victim)).toEqual([]);
expect(f.calls()).toContain('destroy');
} finally { rmSync(f.root, { recursive: true, force: true }); }
}, 60_000);
test('quota: a refusal caches the maximum, the cache drives the preflight for a day and is dropped after; UBI_QUOTA_WAIT waits before refusing', () => {
const refuse = fakeVm({ refuse: 'Validation failed for following fields: Requested vCPU count: 16, currently used vCPU count: 32, maximum allowed vCPU count: 32' });
try {
+2 -1
View File
@@ -468,7 +468,8 @@ $U verify -- 'pnpm e2e' # a different command on the same bootstrap
The file is repository content, so it gets no reach beyond the checkout on its
own: `setup` and the local side of each `pull` must be relative paths that
resolve inside `--src` (absolute, `~` and `..` paths are refused before any
API call), and a name under `pass` is forwarded only when your command line
API call, and each destination is resolved again right before its pull), and a
name under `pass` is forwarded only when your command line
grants it with `--pass NAME` or `--trust-config`. Run `verify` only on a tree
you have reviewed: its bootstrap and command run on the VM with whatever you
granted.
+2 -1
View File
@@ -144,7 +144,8 @@ $U verify -- 'pnpm e2e' # a different command on the same bootstrap
The file is repository content, so it gets no reach beyond the checkout on its
own: `setup` and the local side of each `pull` must be relative paths that
resolve inside `--src` (absolute, `~` and `..` paths are refused before any
API call), and a name under `pass` is forwarded only when your command line
API call, and each destination is resolved again right before its pull), and a
name under `pass` is forwarded only when your command line
grants it with `--pass NAME` or `--trust-config`. Run `verify` only on a tree
you have reviewed: its bootstrap and command run on the VM with whatever you
granted.