Provision LOCALSTACK_AUTH_TOKEN for every repository
LocalStack is a common enough dependency that declaring it per repository is busywork, and the token is already in the host environment when it is needed at all. It is now provisioned host-wide, through the same custom-secret path as the per-repository declarations, so the value stays out of the sandbox and the proxy substitutes it on requests to localstack.cloud. Provisioning is conditional on the sandbox having Docker. LocalStack runs as a container, so on a sandbox created from a non-docker template the credential would be dead weight, and the entry is skipped with a message rather than stored. An unset variable is skipped silently, which costs nothing on a machine that never uses LocalStack. The provisioning body moves into provision_secret so the host-wide and per-repository paths cannot drift apart.
This commit is contained in:
@@ -437,6 +437,28 @@ still provision.
|
|||||||
Placeholders are derived from the repository and variable name, so re-running `setup`
|
Placeholders are derived from the repository and variable name, so re-running `setup`
|
||||||
does not invalidate a value already exported inside a running sandbox.
|
does not invalidate a value already exported inside a running sandbox.
|
||||||
|
|
||||||
|
### Host-wide credentials
|
||||||
|
|
||||||
|
Some credentials are worth provisioning everywhere rather than declaring per
|
||||||
|
repository. These are taken from your host environment automatically:
|
||||||
|
|
||||||
|
| Variable | Hosts | Provisioned when |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `LOCALSTACK_AUTH_TOKEN` | `localstack.cloud`, `*.localstack.cloud` | the variable is set **and** the sandbox has Docker |
|
||||||
|
|
||||||
|
The Docker condition matters: LocalStack runs as a container, so on a sandbox created
|
||||||
|
from a non-`-docker` template the token would be dead weight. It is skipped there with
|
||||||
|
a message rather than stored.
|
||||||
|
|
||||||
|
Nothing happens if the variable is unset, so this costs nothing on a machine that
|
||||||
|
does not use LocalStack.
|
||||||
|
|
||||||
|
> **Unverified.** LocalStack runs as a *nested* container inside the sandbox's own
|
||||||
|
> Docker daemon. Whether its outbound activation request traverses the `sbx` proxy —
|
||||||
|
> and therefore gets the placeholder substituted — has not been tested. If activation
|
||||||
|
> fails reporting an invalid token, the placeholder is reaching LocalStack literally,
|
||||||
|
> and the token needs exporting as a real value instead.
|
||||||
|
|
||||||
## AWS profile naming
|
## AWS profile naming
|
||||||
|
|
||||||
Only a trailing `-readonly` is removed. Everything else passes through:
|
Only a trailing `-readonly` is removed. Everything else passes through:
|
||||||
|
|||||||
+78
-36
@@ -8,6 +8,13 @@ DEFAULT_MODE="${AI_SBX_MODE:-clone}"
|
|||||||
DEFAULT_TOKEN_DAYS="${AI_SBX_TOKEN_DAYS:-30}"
|
DEFAULT_TOKEN_DAYS="${AI_SBX_TOKEN_DAYS:-30}"
|
||||||
DEFAULT_TEMPLATE="${AI_SBX_TEMPLATE:-}"
|
DEFAULT_TEMPLATE="${AI_SBX_TEMPLATE:-}"
|
||||||
DEFAULT_TOOLS="${AI_SBX_TOOLS:-bun}"
|
DEFAULT_TOOLS="${AI_SBX_TOOLS:-bun}"
|
||||||
|
|
||||||
|
# VAR|host[,host...]|requirement. Provisioned for every repository when the
|
||||||
|
# variable is present in the host environment. "docker" skips the entry on a
|
||||||
|
# sandbox that cannot run containers, where the credential would be useless.
|
||||||
|
HOST_WIDE_SECRETS=(
|
||||||
|
"LOCALSTACK_AUTH_TOKEN|localstack.cloud,*.localstack.cloud|docker"
|
||||||
|
)
|
||||||
CLAUDE_HOME="${CLAUDE_HOME:-$HOME/.claude}"
|
CLAUDE_HOME="${CLAUDE_HOME:-$HOME/.claude}"
|
||||||
|
|
||||||
# Only these leave the host. ~/.claude also holds OAuth credentials, shell
|
# Only these leave the host. ~/.claude also holds OAuth credentials, shell
|
||||||
@@ -76,6 +83,8 @@ Registry credentials are declared per repository in the user's config
|
|||||||
directory as a "secrets" file, one line of VAR|host|command each. The command
|
directory as a "secrets" file, one line of VAR|host|command each. The command
|
||||||
runs on the host and its output becomes an sbx custom secret, so the sandbox
|
runs on the host and its output becomes an sbx custom secret, so the sandbox
|
||||||
sees a placeholder and the proxy substitutes the real value.
|
sees a placeholder and the proxy substitutes the real value.
|
||||||
|
LOCALSTACK_AUTH_TOKEN is provisioned for every repository when it is set on
|
||||||
|
the host and the sandbox has Docker to make use of it.
|
||||||
|
|
||||||
AI_SBX_TOOLS lists mise tools installed globally in the sandbox, defaulting
|
AI_SBX_TOOLS lists mise tools installed globally in the sandbox, defaulting
|
||||||
to bun because several Claude plugins run their hooks under it. Set it to an
|
to bun because several Claude plugins run their hooks under it. Set it to an
|
||||||
@@ -780,14 +789,80 @@ secret_placeholder() {
|
|||||||
printf 'sbx-cs-%s' "$digest"
|
printf 'sbx-cs-%s' "$digest"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sandbox_has_docker() {
|
||||||
|
sbx exec "$SANDBOX_NAME" \
|
||||||
|
bash -lc 'command -v docker >/dev/null' \
|
||||||
|
</dev/null >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
provision_secret() {
|
||||||
|
local var="$1" hosts="$2" value="$3"
|
||||||
|
local placeholder
|
||||||
|
placeholder="$(secret_placeholder "$var")"
|
||||||
|
|
||||||
|
local -a host_args=()
|
||||||
|
local host
|
||||||
|
for host in ${hosts//,/ }; do
|
||||||
|
host_args+=(--host "$host")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Piped rather than --value: the secret would otherwise be visible in the
|
||||||
|
# process list to anything running as this user.
|
||||||
|
printf '%s' "$value" |
|
||||||
|
sbx secret set-custom "$SANDBOX_NAME" \
|
||||||
|
"${host_args[@]}" \
|
||||||
|
--env "$var" \
|
||||||
|
--placeholder "$placeholder" >/dev/null 2>&1 ||
|
||||||
|
{
|
||||||
|
printf 'Could not store %s in the sandbox.\n' "$var" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# A sandbox that already exists keeps whatever environment it was created
|
||||||
|
# with, so the placeholder is exported explicitly.
|
||||||
|
sbx exec "$SANDBOX_NAME" bash -c "
|
||||||
|
persistent=/etc/sandbox-persistent.sh
|
||||||
|
marker=$(printf '%q' "# ai-sbx secret $var")
|
||||||
|
|
||||||
|
grep -Fq \"\$marker\" \"\$persistent\" 2>/dev/null && exit 0
|
||||||
|
|
||||||
|
printf '%s\nexport %s=%s\n' \
|
||||||
|
\"\$marker\" $(printf '%q' "$var") $(printf '%q' "$placeholder") \
|
||||||
|
>>\"\$persistent\"
|
||||||
|
" </dev/null >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
printf 'Provisioned %s for %s\n' "$var" "${hosts//,/, }"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Credentials worth provisioning for every repository rather than declaring per
|
||||||
|
# repository, taken straight from the host environment. LocalStack only matters
|
||||||
|
# when the agent can run containers, so it is skipped where Docker is absent.
|
||||||
|
install_host_wide_secrets() {
|
||||||
|
local entry var hosts requirement
|
||||||
|
|
||||||
|
for entry in "${HOST_WIDE_SECRETS[@]}"; do
|
||||||
|
IFS='|' read -r var hosts requirement <<<"$entry"
|
||||||
|
|
||||||
|
[[ -n "${!var:-}" ]] || continue
|
||||||
|
|
||||||
|
if [[ "$requirement" == docker ]] && ! sandbox_has_docker; then
|
||||||
|
printf 'Skipping %s: the sandbox has no Docker to run it.\n' "$var" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
provision_secret "$var" "$hosts" "${!var}" || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
install_sandbox_secrets() {
|
install_sandbox_secrets() {
|
||||||
|
install_host_wide_secrets
|
||||||
|
|
||||||
local declarations
|
local declarations
|
||||||
declarations="$(read_secret_declarations)" || return 0
|
declarations="$(read_secret_declarations)" || return 0
|
||||||
|
|
||||||
[[ -n "$declarations" ]] || return 0
|
[[ -n "$declarations" ]] || return 0
|
||||||
|
|
||||||
local var hosts command value placeholder
|
local var hosts command value
|
||||||
local -a host_args
|
|
||||||
|
|
||||||
while IFS='|' read -r var hosts command; do
|
while IFS='|' read -r var hosts command; do
|
||||||
[[ -n "$var" ]] || continue
|
[[ -n "$var" ]] || continue
|
||||||
@@ -804,40 +879,7 @@ install_sandbox_secrets() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
placeholder="$(secret_placeholder "$var")"
|
provision_secret "$var" "$hosts" "$value" || true
|
||||||
|
|
||||||
host_args=()
|
|
||||||
local host
|
|
||||||
for host in ${hosts//,/ }; do
|
|
||||||
host_args+=(--host "$host")
|
|
||||||
done
|
|
||||||
|
|
||||||
# Piped rather than --value: the secret would otherwise be visible in
|
|
||||||
# the process list to anything running as this user.
|
|
||||||
if printf '%s' "$value" |
|
|
||||||
sbx secret set-custom "$SANDBOX_NAME" \
|
|
||||||
"${host_args[@]}" \
|
|
||||||
--env "$var" \
|
|
||||||
--placeholder "$placeholder" >/dev/null 2>&1; then
|
|
||||||
|
|
||||||
printf 'Provisioned %s for %s\n' "$var" "${hosts//,/, }"
|
|
||||||
else
|
|
||||||
printf 'Could not store %s in the sandbox.\n' "$var" >&2
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# A sandbox that already exists keeps whatever environment it was
|
|
||||||
# created with, so the placeholder is exported explicitly.
|
|
||||||
sbx exec "$SANDBOX_NAME" bash -c "
|
|
||||||
persistent=/etc/sandbox-persistent.sh
|
|
||||||
marker=$(printf '%q' "# ai-sbx secret $var")
|
|
||||||
|
|
||||||
grep -Fq \"\$marker\" \"\$persistent\" 2>/dev/null && exit 0
|
|
||||||
|
|
||||||
printf '%s\nexport %s=%s\n' \
|
|
||||||
\"\$marker\" $(printf '%q' "$var") $(printf '%q' "$placeholder") \
|
|
||||||
>>\"\$persistent\"
|
|
||||||
" </dev/null >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
unset value
|
unset value
|
||||||
done <<<"$declarations"
|
done <<<"$declarations"
|
||||||
|
|||||||
@@ -69,6 +69,22 @@ REPOSITORY="other/repo"
|
|||||||
[[ "$(secret_placeholder FONTAWESOME_API_KEY)" != "$first" ]] ||
|
[[ "$(secret_placeholder FONTAWESOME_API_KEY)" != "$first" ]] ||
|
||||||
fail "placeholder does not vary by repository"
|
fail "placeholder does not vary by repository"
|
||||||
|
|
||||||
|
for entry in "${HOST_WIDE_SECRETS[@]}"; do
|
||||||
|
IFS='|' read -r var hosts requirement <<<"$entry"
|
||||||
|
|
||||||
|
[[ -n "$var" ]] || fail "host-wide entry has no variable: $entry"
|
||||||
|
[[ -n "$hosts" ]] || fail "host-wide entry $var has no hosts"
|
||||||
|
[[ "$requirement" == docker || "$requirement" == "-" ]] ||
|
||||||
|
fail "host-wide entry $var has an unknown requirement: '$requirement'"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s\n' "${HOST_WIDE_SECRETS[@]}" | grep -q '^LOCALSTACK_AUTH_TOKEN|' ||
|
||||||
|
fail "LOCALSTACK_AUTH_TOKEN should be provisioned host-wide"
|
||||||
|
|
||||||
|
printf '%s\n' "${HOST_WIDE_SECRETS[@]}" | grep '^LOCALSTACK_AUTH_TOKEN|' |
|
||||||
|
grep -q 'localstack\.cloud' ||
|
||||||
|
fail "LOCALSTACK_AUTH_TOKEN must target localstack.cloud"
|
||||||
|
|
||||||
if ((failures)); then
|
if ((failures)); then
|
||||||
printf '%d assertion(s) failed\n' "$failures" >&2
|
printf '%d assertion(s) failed\n' "$failures" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Reference in New Issue
Block a user