1 Commits
Author SHA1 Message Date
mroberts b5dfae0696 Keep the stored GitHub token when setup runs again
The secret store outlives the sandbox, so recreating one to change its image
or add a profile does not lose the token. Prompting for a replacement anyway
made --replace impractical and trained the habit of minting tokens that are
never revoked.

Only a token scoped to this sandbox counts. A global one would authenticate
the agent too, but reaching every repository it can reach is what this task
exists to prevent. The token command still always prompts; it is the way to
replace an expired or revoked token.
2026-08-03 16:09:44 -05:00
3 changed files with 116 additions and 5 deletions
+2 -2
View File
@@ -263,8 +263,8 @@ provider "aws" {
| Command | Effect | | Command | Effect |
| --- | --- | | --- | --- |
| `setup [options]` | Configure the repository, create the sandbox, open the token form, install AWS profiles, Claude configuration and plugins, and mise | | `setup [options]` | Configure the repository, create the sandbox, open the token form if no token is stored yet, install AWS profiles, Claude configuration and plugins, and mise |
| `token` | Replace the GitHub token for this repository — expiry, revocation, permission change | | `token` | Replace the GitHub token for this repository — expiry, revocation, permission change. Always prompts |
| `run [--launch MODE] [-- args...]` | Refresh AWS credentials and the repository's mise tools, then attach to the agent or to a tmux workspace | | `run [--launch MODE] [-- args...]` | Refresh AWS credentials and the repository's mise tools, then attach to the agent or to a tmux workspace |
| `refresh` | Refresh AWS credentials, without attaching | | `refresh` | Refresh AWS credentials, without attaching |
| `config` | Re-apply your Claude configuration, plugins and dotfiles after the host changes, without recreating the sandbox | | `config` | Re-apply your Claude configuration, plugins and dotfiles after the host changes, without recreating the sandbox |
+24 -3
View File
@@ -83,8 +83,10 @@ Usage:
mise run ai:sbx -- status mise run ai:sbx -- status
mise run ai:sbx -- remove mise run ai:sbx -- remove
Setup opens a pre-filled GitHub token form in your browser. Use "token" on Setup opens a pre-filled GitHub token form in your browser, unless a token is
its own to replace an expired or revoked token later. already stored for the sandbox. The secret store outlives the sandbox, so
recreating one with --replace keeps its token. Use "token" on its own to
replace an expired or revoked token.
Set AI_SBX_TEMPLATE in your mise config to reuse one custom image across Set AI_SBX_TEMPLATE in your mise config to reuse one custom image across
every repository without repeating --template. every repository without repeating --template.
@@ -242,6 +244,17 @@ read_token() {
printf '%s' "$token" printf '%s' "$token"
} }
# Only a token scoped to this sandbox counts. A global one would authenticate
# the agent too, but reaching every repository the token can reach is exactly
# what this task exists to prevent, so it is not treated as satisfying setup.
sandbox_has_github_token() {
sbx secret ls 2>/dev/null |
awk -v scope="$SANDBOX_NAME" '
$1 == scope && $2 == "service" && $3 == "github" { found = 1 }
END { exit !found }
'
}
install_github_token() { install_github_token() {
local url local url
url="$(token_url)" url="$(token_url)"
@@ -1354,7 +1367,15 @@ setup_command() {
create_sandbox create_sandbox
fi fi
install_github_token # The secret store outlives the sandbox, so recreating one to change its
# image keeps the token. Prompting anyway would train the habit of minting
# replacement tokens and never revoking the old ones.
if sandbox_has_github_token; then
printf 'Keeping the GitHub token already stored for %s. Replace it with: mise run ai:sbx -- token\n' \
"$SANDBOX_NAME"
else
install_github_token
fi
install_sandbox_aws_files install_sandbox_aws_files
+90
View File
@@ -0,0 +1,90 @@
#!/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
fail() {
printf 'FAIL: %s\n' "$1" >&2
failures=$((failures + 1))
}
SANDBOX_NAME=ai-repo-abc123
listing=""
sbx() {
printf '%s\n' "$listing"
}
with_listing() {
listing="$1"
sandbox_has_github_token
}
full_listing() {
cat <<'EOF'
SCOPE TYPE NAME SECRET
ai-repo-abc123 service github (stored)
(global) service anthropic (oauth configured)
CUSTOM SECRETS
SCOPE TARGETS ENV PLACEHOLDER SECRET
ai-repo-abc123 localstack.cloud LOCALSTACK_AUTH_TOKEN sbx-cs-0c2f39c1 ls-vOL***
EOF
}
with_listing "$(full_listing)" ||
fail "a sandbox-scoped github token was not detected"
with_listing "$(
cat <<'EOF'
SCOPE TYPE NAME SECRET
(global) service anthropic (oauth configured)
EOF
)" && fail "no github token stored, yet setup would have been skipped"
with_listing "$(
cat <<'EOF'
SCOPE TYPE NAME SECRET
(global) service github (stored)
EOF
)" && fail "a global github token must not satisfy a per-repository sandbox"
with_listing "$(
cat <<'EOF'
SCOPE TYPE NAME SECRET
ai-other-sandbox service github (stored)
EOF
)" && fail "another sandbox's github token must not count as this one's"
with_listing "$(
cat <<'EOF'
CUSTOM SECRETS
SCOPE TARGETS ENV PLACEHOLDER SECRET
ai-repo-abc123 github.com github sbx-cs-abc gh***
EOF
)" && fail "a custom secret must not be mistaken for the stored service token"
with_listing "" &&
fail "empty output should mean no token, not a stored one"
grep -q 'if sandbox_has_github_token; then' "$TASK" ||
fail "setup no longer guards install_github_token"
awk '/^token_command\(\)/, /^}/' "$TASK" | grep -q 'install_github_token' ||
fail "the token command must always prompt; it is the way to replace one"
awk '/^token_command\(\)/, /^}/' "$TASK" | grep -q 'sandbox_has_github_token' &&
fail "the token command must not skip when a token exists"
if ((failures)); then
printf '%d assertion(s) failed\n' "$failures" >&2
exit 1
fi
printf 'All GitHub token assertions passed.\n'