Launch a tmux workspace and carry dotfiles into the sandbox

AI_SBX_LAUNCH=tmux (or run --launch tmux) attaches to a three-window tmux
session - agent, edit, shell - instead of the bare agent. The launcher is
vendored at tasks/ai/workspace and installed into the sandbox, so a custom
image and this task cannot drift. It is invoked through a login shell because
/etc/sandbox-persistent.sh is where PATH, the mise shims, the AWS credentials
and every secret placeholder live, and the tmux server hands that environment
to all three windows.

AI_SBX_DOTFILES=chezmoi renders the host chezmoi target state and unpacks it
into the sandbox, so no dotfiles repository, decryption key or network access
is needed inside. chezmoi archive decrypts as it renders, so the target list
is an allowlist, encrypted files resolving inside it are refused, and the
rendered archive is scanned for credential shapes before it enters the
sandbox.

Both default to off; with neither set, run behaves exactly as before.
This commit is contained in:
2026-08-03 13:49:24 -05:00
parent ad2b33f984
commit 14165503d5
7 changed files with 842 additions and 9 deletions
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -euo pipefail
# shellcheck source-path=SCRIPTDIR
# shellcheck source=tasks/ai/sbx
source "$(dirname "${BASH_SOURCE[0]}")/../tasks/ai/sbx"
failures=0
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
failures=$((failures + 1))
}
HOME="$work/home"
CONFIG_ROOT="$work/config"
SANDBOX_NAME=ai-test
mkdir -p "$HOME" "$CONFIG_ROOT" "$work/source/dot_config/nvim"
: >"$work/source/dot_config/nvim/encrypted_private_secrets.lua.age"
: >"$work/source/encrypted_private_dot_npmrc.age"
chezmoi_calls=0
chezmoi() {
chezmoi_calls=$((chezmoi_calls + 1))
case "$1" in
source-path)
printf '%s\n' "$work/source"
;;
target-path)
case "$2" in
*nvim*) printf '%s/.config/nvim/secrets.lua\n' "$HOME" ;;
*) printf '%s/.npmrc\n' "$HOME" ;;
esac
;;
archive)
printf 'archive\n'
;;
esac
}
sbx() {
cat >/dev/null 2>&1 || true
printf '%s\n' "$HOME"
}
printf '%s\n' '.tmux.conf' '# a comment' '' '.gitconfig' >"$CONFIG_ROOT/dotfiles"
mapfile -t entries < <(read_dotfiles_allowlist)
[[ "${entries[*]}" == ".tmux.conf .gitconfig" ]] ||
fail "the allowlist should drop comments and blanks, got: ${entries[*]}"
if (assert_no_encrypted_targets .config/nvim) 2>/dev/null; then
fail "an allowlist entry containing an encrypted target was accepted"
fi
if (assert_no_encrypted_targets .npmrc) 2>/dev/null; then
fail "an allowlist entry that is itself an encrypted target was accepted"
fi
(assert_no_encrypted_targets .tmux.conf .gitconfig) 2>/dev/null ||
fail "an allowlist with no encrypted targets was rejected"
chezmoi_calls=0
DEFAULT_DOTFILES="" install_sandbox_dotfiles >/dev/null
((chezmoi_calls == 0)) ||
fail "AI_SBX_DOTFILES unset must not call chezmoi at all"
if (DEFAULT_DOTFILES=stow install_sandbox_dotfiles) >/dev/null 2>&1; then
fail "an unknown AI_SBX_DOTFILES value was accepted"
fi
printf 'token: github_pat_%s\n' "$(printf 'a%.0s' {1..30})" >"$work/archive"
if (scan_dotfiles_archive "$work/archive") 2>/dev/null; then
fail "a rendered archive holding a GitHub token was accepted"
fi
printf 'set -g mouse on\n' >"$work/archive"
(scan_dotfiles_archive "$work/archive") 2>/dev/null ||
fail "a clean archive was rejected"
if ((failures)); then
printf '%d assertion(s) failed\n' "$failures" >&2
exit 1
fi
printf 'All dotfiles assertions passed.\n'
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
set -euo pipefail
TASK="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/tasks/ai/sbx"
# shellcheck source-path=SCRIPTDIR
# shellcheck source=tasks/ai/sbx
source "$TASK"
failures=0
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
failures=$((failures + 1))
}
mkdir -p "$work/bin"
cat >"$work/bin/sbx" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"$SBX_LOG"
EOF
chmod 755 "$work/bin/sbx"
PATH="$work/bin:$PATH"
export PATH
SANDBOX_NAME=ai-test
REPO_ROOT=/workspace/repo
CONFIG_AGENT=claude
SBX_LOG="$work/log"
export SBX_LOG
dispatch() {
: >"$SBX_LOG"
(
load_config() { :; }
sandbox_exists() { :; }
install_sandbox_aws_files() { :; }
install_sandbox_mise() { :; }
install_sandbox_workspace() { :; }
run_command "$@"
) >/dev/null 2>&1 || true
cat "$SBX_LOG"
}
[[ "$DEFAULT_LAUNCH" == agent ]] ||
fail "AI_SBX_LAUNCH unset should default to agent, got: $DEFAULT_LAUNCH"
(
AI_SBX_LAUNCH=tmux
export AI_SBX_LAUNCH
# shellcheck source=tasks/ai/sbx
source "$TASK"
[[ "$DEFAULT_LAUNCH" == tmux ]]
) || fail "AI_SBX_LAUNCH=tmux was not read into DEFAULT_LAUNCH"
DEFAULT_LAUNCH=agent
[[ "$(dispatch)" == *"run ai-test"* ]] ||
fail "agent mode should dispatch to sbx run: $(dispatch)"
DEFAULT_LAUNCH=tmux
tmux_dispatch="$(dispatch)"
[[ "$tmux_dispatch" == *"exec -it -w /workspace/repo ai-test"* ]] ||
fail "tmux mode should dispatch to sbx exec -it: $tmux_dispatch"
[[ "$tmux_dispatch" == *"bash -lc ai-sbx-workspace claude"* ]] ||
fail "tmux mode must use a login shell and pass the agent: $tmux_dispatch"
[[ "$tmux_dispatch" != *"run ai-test"* ]] ||
fail "tmux mode should not also call sbx run: $tmux_dispatch"
DEFAULT_LAUNCH=tmux
[[ "$(dispatch --launch agent)" == *"run ai-test"* ]] ||
fail "--launch agent should beat AI_SBX_LAUNCH=tmux"
DEFAULT_LAUNCH=agent
[[ "$(dispatch --launch tmux)" == *"exec -it"* ]] ||
fail "--launch tmux should beat AI_SBX_LAUNCH=agent"
DEFAULT_LAUNCH=agent
[[ "$(dispatch -- --resume)" == *"run ai-test -- --resume"* ]] ||
fail "agent arguments should still reach sbx run"
DEFAULT_LAUNCH=agent
[[ "$(dispatch -- --launch tmux)" == *"run ai-test -- --launch tmux"* ]] ||
fail "--launch after -- belongs to the agent, not to the task"
if (validate_launch_mode bogus) 2>/dev/null; then
fail "an unknown launch mode was accepted"
fi
if (
DEFAULT_LAUNCH=agent
load_config() { :; }
sandbox_exists() { :; }
install_sandbox_aws_files() { :; }
install_sandbox_mise() { :; }
run_command --launch bogus
) >/dev/null 2>&1; then
fail "run --launch bogus should exit non-zero"
fi
if (
DEFAULT_LAUNCH=tmux
load_config() { :; }
sandbox_exists() { :; }
install_sandbox_aws_files() { :; }
install_sandbox_mise() { :; }
install_sandbox_workspace() { :; }
run_command -- --resume
) >/dev/null 2>&1; then
fail "agent arguments in tmux mode should be rejected, not dropped"
fi
if ((failures)); then
printf '%d assertion(s) failed\n' "$failures" >&2
exit 1
fi
printf 'All launch mode assertions passed.\n'
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -euo pipefail
LAUNCHER="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/tasks/ai/workspace"
failures=0
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
failures=$((failures + 1))
}
mkdir -p "$work/bin"
cat >"$work/bin/tmux" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"$TMUX_LOG"
if [[ "$1" == has-session ]]; then
exit "${TMUX_HAS_SESSION:-1}"
fi
EOF
cat >"$work/bin/claude" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"$TMUX_LOG"
EOF
chmod 755 "$work/bin/tmux" "$work/bin/claude"
TMUX_LOG="$work/log"
export TMUX_LOG
launch() {
: >"$TMUX_LOG"
PATH="$work/bin:$PATH" bash "$LAUNCHER" "$@" >/dev/null 2>&1
cat "$TMUX_LOG"
}
bash -n "$LAUNCHER" ||
fail "the launcher is not syntactically valid"
if command -v shellcheck >/dev/null 2>&1; then
shellcheck "$LAUNCHER" ||
fail "the launcher is not shellcheck clean"
fi
log="$(launch claude)"
windows="$(grep -cE 'new-session|new-window' <<<"$log")"
[[ "$windows" == 3 ]] ||
fail "expected exactly three windows, got $windows: $log"
for window in agent edit shell; do
grep -qE "(new-session|new-window).* -n $window " <<<"$log" ||
fail "no window named $window: $log"
done
grep -qF 'send-keys -t ai-sbx:agent claude --dangerously-skip-permissions C-m' <<<"$log" ||
fail "the claude mapping must be exactly claude --dangerously-skip-permissions: $log"
grep -qF 'send-keys -t ai-sbx:edit nvim . C-m' <<<"$log" ||
fail "the edit window should open nvim: $log"
grep -qF 'select-window -t ai-sbx:agent' <<<"$log" ||
fail "the agent window should be selected: $log"
grep -qF 'attach-session -t ai-sbx' <<<"$log" ||
fail "the launcher should attach to the session: $log"
log="$(launch codex)"
grep -qF 'send-keys -t ai-sbx:agent codex C-m' <<<"$log" ||
fail "an unobserved agent should fall back to its bare name: $log"
TMUX_HAS_SESSION=0
export TMUX_HAS_SESSION
log="$(launch claude)"
if grep -qE 'new-session|new-window' <<<"$log"; then
fail "an existing session must be attached to, never rebuilt: $log"
fi
grep -qF 'attach-session -t ai-sbx' <<<"$log" ||
fail "an existing session should be attached to: $log"
unset TMUX_HAS_SESSION
mkdir -p "$work/bare"
# shellcheck disable=SC2016
printf '#!%s\nprintf %%s "$*" >>"$TMUX_LOG"\n' "$(command -v bash)" \
>"$work/bare/claude"
chmod 755 "$work/bare/claude"
: >"$TMUX_LOG"
PATH="$work/bare" "$(command -v bash)" "$LAUNCHER" claude >/dev/null 2>&1 ||
fail "the launcher should not fail when tmux is missing"
fallback="$(cat "$TMUX_LOG")"
[[ "$fallback" == '--dangerously-skip-permissions' ]] ||
fail "without tmux the launcher should exec the agent, logged: $fallback"
if ((failures)); then
printf '%d assertion(s) failed\n' "$failures" >&2
exit 1
fi
printf 'All workspace launcher assertions passed.\n'