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:
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user