Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ macos: macos-doctor macos-apply
./scripts/macos-bootstrap.sh
osascript -e 'tell app "loginwindow" to «event aevtrrst»'

# One command for a new exe.dev VM as an @nonreagent host; see the script.
exe-box:
@test -n "$(NAME)" || { echo "usage: make exe-box NAME=<name>"; exit 1; }
./scripts/new-exe-box.sh "$(NAME)"

macos-reset-dock:
defaults write com.apple.dock persistent-apps -array
killall Dock
Expand Down Expand Up @@ -88,6 +93,7 @@ test:
./test/test_kitty.sh
./test/test_fish.sh
./test/test_macos_defaults.sh
./test/test_exe_box.sh

deploy:
./deploy.sh apply
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ make init-submodules
make deploy
```

# New exe.dev box

`make exe-box NAME=<name>` (run from the Mac, with `op` unlocked). It creates the VM or reuses one that answers ssh, hands @nonreagent's GitHub token to `gh` over ssh stdin, then clones or pulls [nonreagent/dotfiles](https://github.com/nonreagent/dotfiles) and runs its `install.sh` and `sync-plugins.sh`; design: `docs/superpowers/specs/2026-10-29-exe-box-design.md`.

# Clipboard bridge (exe.dev VMs)

`make clipboard-bridge` loads `org.nonrational.clipboard-bridge`, a socket-activated launch agent on `127.0.0.1:2224` that answers image requests from the pasteboard and nothing else. `home/.ssh/config.d/exe.conf` reverse-forwards that port into every `*.exe.xyz` session and shares connections between windows; add `Include config.d/*.conf` as the first line of `~/.ssh/config` once. Inside a VM, Claude Code's ctrl+v then pastes whatever image is on the mac clipboard. The VM half is the `wl-paste` shim in [nonreagent/dotfiles](https://github.com/nonreagent/dotfiles); design and trust model: [clipboard bridge design](https://github.com/nonreagent/dotfiles/blob/main/docs/superpowers/specs/2026-09-07-clipboard-bridge-design.md). The include is global, so OpenSSH's permission check on it gates every `ssh`: if they all start failing with `Bad owner or permissions on ~/.ssh/config.d/exe.conf`, `chmod 644` the repo's copy.
Expand Down
45 changes: 45 additions & 0 deletions docs/superpowers/specs/2026-10-29-exe-box-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# One command for a new exe.dev box

## `scripts/new-exe-box.sh`: one command for a new VM

Lives in this repo because it runs from the Mac with the human's exe.dev key and 1Password session. exe.dev's `new` command offers `--setup-script` for first boot, but a first-boot run cannot be watched; the wrapper runs the provisioning itself over ssh instead, sequentially.

```
scripts/new-exe-box.sh <name> # or make exe-box NAME=<name>
```

1. Probe `ssh -o StrictHostKeyChecking=accept-new <name>.exe.xyz true`. If the box answers, print `using existing <name>.exe.xyz` and skip steps 2 and 3, so a re-run converges an existing box.
2. `ssh exe.dev new --name <name> --json`. The box comes up bare; no `--setup-script`, so nothing runs before the wrapper can watch it.
3. Poll the same `true` probe until the box answers, within `EXE_BOX_WAIT` seconds (default 300), every `EXE_BOX_POLL` seconds (default 5).
4. Inject the token over stdin, never through `--env` or an argument, so it lands only in `gh`'s config on the VM, the same state an interactive login leaves:

```
op read "op://<vault>/<item>/token" \
| ssh <name>.exe.xyz 'gh auth login --with-token && gh auth setup-git'
```

5. Clone or update the dotfiles and install them, in one ssh call:

```
ssh <name>.exe.xyz '{ git -C ~/.dotfiles pull --ff-only || git clone <repo URL> ~/.dotfiles; } && ~/.dotfiles/install.sh && ~/.claude/sync-plugins.sh'
```

The run is sequential and as the login user, so there is no first-boot race. A re-run pulls first and converges on the upstream of the VM checkout's current branch (not necessarily `main`); a pull that fails on an existing clone sends `git clone` into a non-empty directory, which fails and stops the chain.
The wrapper assumes the exeuntu image ships `git`, `gh` and `claude` (the exe.dev docs do not list the image's tools); B2's probe confirms it.
6. Print `ready: <name>.exe.xyz https://<name>.exe.xyz/`, then `optional: ssh <name>.exe.xyz ~/bin/setup-review-watcher`.

The script reads the token before creating anything: a bad reference costs a retry, not a half-provisioned VM.

Configuration, all environment variables with defaults in the script:

- `EXE_BOX_REPO_URL`: the repo to clone (default `https://github.com/nonreagent/dotfiles`).
- `EXE_BOX_GH_TOKEN_REF`: the 1Password item path (default `op://Private/nonreagent-github/token`). `op` unlocking is the only human touch.
- `EXE_BOX_WAIT` (300) and `EXE_BOX_POLL` (5): the wait loop's deadline and interval, in seconds.

The same pipe can carry a `claude setup-token` value into the VM's environment file, which would close the expired-login failure the review watcher records. That is a separate decision: a long-lived Claude token is a larger blast radius than a fine-grained PAT scoped to nonreagent's repositories. This spec leaves it out; the wrapper is written so adding a second injected secret is one more line.

Every ssh call passes `-o StrictHostKeyChecking=accept-new`: non-interactive ssh to the `exe.dev` lobby hangs on host-key prompts otherwise, as exe.dev's own skill file warns.

## Tests

`test/test_exe_box.sh` runs the script under a mktemp sandbox with stub `ssh` and `op` on `PATH`. It covers the create-wait-inject-install flow, reuse of an existing box, a bad token reference creating nothing, the token travelling on stdin only, the wait timeout, and the usage error.
71 changes: 71 additions & 0 deletions scripts/new-exe-box.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/bin/bash
# new-exe-box.sh <name>: create (or reuse) an exe.dev VM and bring it up as an
# @nonreagent host in one go. Waits for ssh, hands @nonreagent's GitHub token
# from 1Password to gh over ssh stdin (never as an argument, never through
# exe.dev's --env or setup-script), then clones or pulls nonreagent/dotfiles
# and runs its install.sh and sync-plugins.sh. Runs from the Mac: it needs the
# human's exe.dev ssh key and an unlocked `op`.
# Spec: docs/superpowers/specs/2026-10-29-exe-box-design.md
set -euf -o pipefail

name="${1:-}"
[ -n "$name" ] || { echo "usage: new-exe-box.sh <name>" >&2; exit 2; }

EXE_BOX_REPO_URL="${EXE_BOX_REPO_URL:-https://github.com/nonreagent/dotfiles}"
EXE_BOX_GH_TOKEN_REF="${EXE_BOX_GH_TOKEN_REF:-op://Private/nonreagent-github/token}"
EXE_BOX_WAIT="${EXE_BOX_WAIT:-300}"
EXE_BOX_POLL="${EXE_BOX_POLL:-5}"

host="$name.exe.xyz"
# accept-new: a fresh box has an unknown host key, and a non-interactive ssh
# would otherwise hang on the prompt (exe.dev's own skill file warns of this).
ssh_opts="-o StrictHostKeyChecking=accept-new"

# Read the token before creating anything: a bad reference should cost a
# retry, not a half-provisioned VM. Empty stdin would also send gh into its
# interactive device flow.
token="$(op read "$EXE_BOX_GH_TOKEN_REF")"
[ -n "$token" ] || { echo "error: $EXE_BOX_GH_TOKEN_REF is empty" >&2; exit 1; }

# An existing box skips creation and the wait, so a re-run converges it.
# shellcheck disable=SC2086
if ssh $ssh_opts -o ConnectTimeout=5 "$host" true 2>/dev/null; then
echo "using existing $host"
else
echo "creating $name"
# exe.dev reports errors (duplicate name, lobby) on stdout, so keep both
# streams and show them on failure.
# shellcheck disable=SC2086
if ! new_out="$(ssh $ssh_opts exe.dev new --name "$name" --json 2>&1)"; then
echo "error: exe.dev new failed for $name:" >&2
printf '%s\n' "$new_out" >&2
exit 1
fi

echo "waiting for $host"
deadline=$(( $(date +%s) + EXE_BOX_WAIT ))
# shellcheck disable=SC2086
until ssh $ssh_opts -o ConnectTimeout=5 "$host" true 2>/dev/null; do
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "error: $host did not answer within ${EXE_BOX_WAIT}s" >&2
exit 1
fi
sleep "$EXE_BOX_POLL"
done
fi

echo "authenticating gh as @nonreagent"
# shellcheck disable=SC2086
printf '%s\n' "$token" | ssh $ssh_opts "$host" 'gh auth login --with-token && gh auth setup-git'

echo "installing dotfiles on $host"
# Pull first so a re-run converges on current main. A pull that fails on an
# existing clone falls into `git clone` on a non-empty directory, which fails
# and stops the chain.
# The URL is quoted for the remote shell.
repo_q="$(printf '%q' "$EXE_BOX_REPO_URL")"
# shellcheck disable=SC2086
ssh $ssh_opts "$host" "{ git -C ~/.dotfiles pull --ff-only || git clone $repo_q ~/.dotfiles; } && ~/.dotfiles/install.sh && ~/.claude/sync-plugins.sh"

echo "ready: $host https://$host/"
echo "optional: ssh $host ~/bin/setup-review-watcher"
175 changes: 175 additions & 0 deletions test/test_exe_box.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/bin/bash
# Tests for scripts/new-exe-box.sh. Everything runs under a mktemp sandbox with
# stub commands on PATH, so no test touches the real $HOME, op, or exe.dev.
set -euf -o pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BASE="$(mktemp -d "${TMPDIR:-/tmp}/test-exe-box.XXXXXX")"
BASE="$(cd "$BASE" && pwd)"
trap 'rm -rf "$BASE"' EXIT

pass=0
fail=0
sb_count=0

ok() { pass=$((pass + 1)); echo "PASS: $1"; }
bad() { fail=$((fail + 1)); echo "FAIL: $1"; }

# Fresh sandbox: $FAKEHOME is $HOME, $SB/bin holds stubs, $SB/calls.log records
# every stub invocation as "<name> <args>".
sandbox() {
sb_count=$((sb_count + 1))
SB="$BASE/$sb_count"
FAKEHOME="$SB/home"
LOG="$SB/calls.log"
mkdir -p "$FAKEHOME" "$SB/bin"
: > "$LOG"
}

# stub <name> [stdout]: a command on PATH that logs its invocation, prints the
# given stdout, and exits $STUB_EXIT (default 1) when "<name> <args...>" starts
# with any '|'-separated entry of $STUB_FAIL (e.g. STUB_FAIL='make -C /r
# macos-doctor|gh auth status').
stub() {
cat > "$SB/bin/$1" <<EOF
#!/bin/bash
me="\$(basename "\$0")"
call="\$me \$*"
echo "\$call" >> "$LOG"
IFS='|'
for f in \${STUB_FAIL:-}; do
case "\$call" in "\$f"*) exit "\${STUB_EXIT:-1}" ;; esac
done
printf '%s\n' '${2:-}'
EOF
chmod +x "$SB/bin/$1"
}


# An ssh stub that logs each call, saves whatever arrived on stdin for the
# call that ran `gh auth login`, and fails `<host> true` the first
# $STUB_SSH_NOT_READY times (one counter across the existence probe and the
# wait loop). The default 0 answers at once, which takes the reuse path.
# $STUB_NEW_FAIL makes `exe.dev new` print that text on stdout and exit 1.
stub_ssh() {
cat > "$SB/bin/ssh" <<EOS
#!/bin/bash
echo "ssh \$*" >> "$LOG"
case "\$*" in
*" true")
n=\$(cat "$SB/notready" 2>/dev/null || echo 0)
if [ "\$n" -lt "\${STUB_SSH_NOT_READY:-0}" ]; then echo \$((n + 1)) > "$SB/notready"; exit 255; fi
;;
*"exe.dev new"*)
if [ -n "\${STUB_NEW_FAIL:-}" ]; then printf '%s\n' "\$STUB_NEW_FAIL"; exit 1; fi
;;
*"gh auth login"*) cat > "$SB/token.stdin" ;;
esac
exit 0
EOS
chmod +x "$SB/bin/ssh"
}

exe_box() {
set +e
out="$(HOME="$FAKEHOME" PATH="$SB/bin:$PATH" EXE_BOX_POLL=0 EXE_BOX_WAIT="${EXE_BOX_WAIT:-5}" \
EXE_BOX_REPO_URL="https://example.invalid/repo" \
EXE_BOX_GH_TOKEN_REF="op://Vault/item/token" "$ROOT/scripts/new-exe-box.sh" "$@" 2>&1)"
status=$?
set -e
}

INSTALL_CMD='{ git -C ~/.dotfiles pull --ff-only || git clone https://example.invalid/repo ~/.dotfiles; } && ~/.dotfiles/install.sh && ~/.claude/sync-plugins.sh'

test_exe_box_creates_waits_injects_and_installs() {
sandbox; stub_ssh; stub op "tok-123"
STUB_SSH_NOT_READY=2 exe_box mybox
if [ "$status" -eq 0 ] \
&& grep -q '^ssh .*exe.dev new --name mybox --json$' "$LOG" \
&& [ "$(grep -c '^ssh .*mybox.exe.xyz true$' "$LOG")" -eq 3 ] \
&& grep -q '^op read op://Vault/item/token$' "$LOG" \
&& grep -q '^ssh .*mybox.exe.xyz gh auth login --with-token && gh auth setup-git$' "$LOG" \
&& grep -qxF "ssh -o StrictHostKeyChecking=accept-new mybox.exe.xyz $INSTALL_CMD" "$LOG" \
&& [ "$(tail -2 <<<"$out" | head -1)" = "ready: mybox.exe.xyz https://mybox.exe.xyz/" ] \
&& [ "$(tail -1 <<<"$out")" = "optional: ssh mybox.exe.xyz ~/bin/setup-review-watcher" ]; then
ok "new-exe-box creates the VM, waits for ssh, injects the token, installs, prints the URL"
else
bad "exe-box flow: status=$status out=$out log=$(cat "$LOG")"
fi
}

test_exe_box_reuses_existing_box() {
sandbox; stub_ssh; stub op "tok-123"
exe_box mybox
if [ "$status" -eq 0 ] \
&& echo "$out" | grep -q 'using existing mybox.exe.xyz' \
&& ! grep -q 'exe.dev new' "$LOG" \
&& grep -qF "$INSTALL_CMD" "$LOG"; then
ok "new-exe-box reuses a box that already answers and still installs"
else
bad "reuse: status=$status out=$out log=$(cat "$LOG")"
fi
}

test_bad_token_ref_creates_nothing() {
sandbox; stub_ssh; stub op
STUB_FAIL="op read" exe_box mybox
if [ "$status" -ne 0 ] && ! grep -q '^ssh' "$LOG"; then
ok "an unreadable token reference fails before any ssh call"
else
bad "bad token ref: status=$status out=$out log=$(cat "$LOG")"
fi
}

test_token_goes_over_stdin_only() {
sandbox; stub_ssh; stub op "tok-123"
exe_box mybox
if [ "$(cat "$SB/token.stdin")" = "tok-123" ] && ! grep -q 'tok-123' "$LOG" && ! echo "$out" | grep -q 'tok-123'; then
ok "the token reaches ssh on stdin and appears in no argument or output"
else
bad "token: stdin=$(cat "$SB/token.stdin" 2>&1) log=$(cat "$LOG") out=$out"
fi
}

test_exe_box_times_out_waiting() {
sandbox; stub_ssh; stub op "tok-123"
STUB_SSH_NOT_READY=1000000 EXE_BOX_WAIT=1 exe_box mybox
if [ "$status" -eq 1 ] && echo "$out" | grep -q 'did not answer within 1s' \
&& ! grep -q 'gh auth' "$LOG"; then
ok "a box that never answers ssh fails with a timeout without sending the secret"
else
bad "timeout: status=$status out=$out log=$(cat "$LOG")"
fi
}

test_exe_box_new_failure_is_reported() {
sandbox; stub_ssh; stub op "tok-123"
STUB_SSH_NOT_READY=1000000 STUB_NEW_FAIL='{"error":"name taken"}' exe_box mybox
if [ "$status" -eq 1 ] && echo "$out" | grep -q 'name taken' && ! grep -q 'gh auth' "$LOG"; then
ok "a failed exe.dev new prints its output and stops before the secret"
else
bad "new failure: status=$status out=$out log=$(cat "$LOG")"
fi
}

test_exe_box_requires_name() {
sandbox; stub_ssh; stub op
exe_box
if [ "$status" -ne 0 ] && echo "$out" | grep -q 'usage: new-exe-box.sh <name>' && ! grep -q '^ssh' "$LOG"; then
ok "new-exe-box without a name prints usage and creates nothing"
else
bad "usage: status=$status out=$out log=$(cat "$LOG")"
fi
}

test_exe_box_creates_waits_injects_and_installs
test_exe_box_reuses_existing_box
test_bad_token_ref_creates_nothing
test_token_goes_over_stdin_only
test_exe_box_times_out_waiting
test_exe_box_new_failure_is_reported
test_exe_box_requires_name

echo
echo "$pass passed, $fail failed"
[ "$fail" -eq 0 ]
Loading