Open the network policy for hosts a sandbox actually needs

Sandboxes default to a deny-everything-else policy, so the registry credentials
provisioned as custom secrets were unusable: npm.fontawesome.com,
proget.careevolution.com and localstack.cloud were all denied, and the request
never left the sandbox for the proxy to substitute a token into. The failure
looked like a connection error rather than a policy decision.

Provisioning a secret now allows its hosts in the same step, since a credential
for a denied host cannot be used by definition. AI_SBX_NETWORK declares any
further hosts, comma or space separated, for private registries that back no
secret.

The marketplace loop's inline policy call moves into the shared helper so the
two cannot drift.

The Balanced policy already permits github.com, codeload and the
githubusercontent hosts, registry.npmjs.org, pypi.org, files.pythonhosted.org,
crates.io and the Go proxies, so npm, pip, cargo, go and a plugin-managed
Neovim need nothing declared. Only private hosts do.
This commit is contained in:
2026-08-03 10:42:56 -05:00
parent d2b7a5ef63
commit 2890b1dd98
4 changed files with 448 additions and 3 deletions
+41 -3
View File
@@ -8,6 +8,7 @@ DEFAULT_MODE="${AI_SBX_MODE:-clone}"
DEFAULT_TOKEN_DAYS="${AI_SBX_TOKEN_DAYS:-30}"
DEFAULT_TEMPLATE="${AI_SBX_TEMPLATE:-}"
DEFAULT_TOOLS="${AI_SBX_TOOLS:-bun}"
DEFAULT_NETWORK="${AI_SBX_NETWORK:-}"
# VAR|host[,host...]|requirement. Provisioned for every repository when the
# variable is present in the host environment. "docker" skips the entry on a
@@ -86,6 +87,10 @@ 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_NETWORK lists hosts to allow through the sandbox network policy, comma
or space separated. Hosts backing a provisioned secret are allowed
automatically, since a credential for a denied host can never be used.
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
empty string to install none.
@@ -719,9 +724,7 @@ install_sandbox_claude_plugins() {
host="${host%%/*}"
if [[ "$host" != "github.com" ]]; then
sbx policy allow network \
--sandbox "$SANDBOX_NAME" "$host" \
</dev/null >/dev/null 2>&1 || true
allow_sandbox_host "$host"
fi
# Without </dev/null sbx exec drains the loop's input and only the
@@ -789,6 +792,33 @@ secret_placeholder() {
printf 'sbx-cs-%s' "$digest"
}
allow_sandbox_host() {
local host="$1"
[[ -n "$host" ]] || return 0
sbx policy allow network --sandbox "$SANDBOX_NAME" "$host" \
</dev/null >/dev/null 2>&1 || true
}
# The default policy denies everything outside common development hosts, so a
# private registry is unreachable no matter what credential it holds.
install_sandbox_network() {
[[ -n "$DEFAULT_NETWORK" ]] || return 0
local -a allow_hosts
read -r -a allow_hosts <<<"${DEFAULT_NETWORK//,/ }"
((${#allow_hosts[@]})) || return 0
local host
for host in "${allow_hosts[@]}"; do
allow_sandbox_host "$host"
done
printf 'Allowed network access to: %s\n' "${allow_hosts[*]}"
}
sandbox_has_docker() {
sbx exec "$SANDBOX_NAME" \
bash -lc 'command -v docker >/dev/null' \
@@ -804,6 +834,10 @@ provision_secret() {
local host
for host in ${hosts//,/ }; do
host_args+=(--host "$host")
# A credential for a host the policy denies is dead weight: the request
# never leaves the sandbox, so the proxy never substitutes anything.
allow_sandbox_host "$host"
done
# Piped rather than --value: the secret would otherwise be visible in the
@@ -1115,6 +1149,8 @@ setup_command() {
install_sandbox_claude_config
install_sandbox_network
install_sandbox_secrets
install_sandbox_mise
@@ -1163,6 +1199,8 @@ config_command() {
install_sandbox_claude_config
install_sandbox_network
install_sandbox_secrets
}