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.
This commit is contained in:
2026-08-03 16:09:44 -05:00
parent 53db7df812
commit b5dfae0696
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 |
+23 -2
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
# 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 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'