Created
June 9, 2026 00:03
-
-
Save pgbezerra/bd3f3fcafc1adf112b796114e3e9e2e1 to your computer and use it in GitHub Desktop.
groundcrew sbx sandbox provisioning with proxy-isolated secrets (claude/codex/opencode)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # sbx proxy secrets — copy to config.env (gitignored) and fill in your values. | |
| # | |
| # cp config.env.example config.env | |
| # $EDITOR config.env | |
| # ./recreate-sandboxes.sh secrets # register at the proxy (ONCE; re-run to rotate) | |
| # ./recreate-sandboxes.sh # create the sandboxes | |
| # | |
| # Values are registered at the sbx PROXY. Real tokens never enter a sandbox — the | |
| # agent sees only sentinels, which the proxy substitutes on outbound traffic to the | |
| # matching host. | |
| # | |
| # Each value is a plain token. To keep secrets in a manager instead, use a 1Password | |
| # reference and resolve it at call time, e.g. | |
| # AI_SANDBOX_LINEAR_TOKEN="op://<vault>/<item>/credential" | |
| # op run --env-file=config.env -- ./recreate-sandboxes.sh secrets | |
| # | |
| # All optional — unset = that credential is skipped (GitHub falls back to sbx's | |
| # built-in default; Claude falls back to interactive /login). | |
| # GitHub PAT — overrides sbx's built-in github default. | |
| # AI_SANDBOX_GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx" | |
| # Claude subscription session — from `claude setup-token` (subscription OAuth | |
| # token, NOT an API key). Never set ANTHROPIC_API_KEY (switches to API billing). | |
| AI_SANDBOX_CLAUDE_CODE_OAUTH_TOKEN="sk-ant-oat-xxxxxxxxxxxxxxxx" | |
| # Linear MCP. | |
| AI_SANDBOX_LINEAR_TOKEN="lin_api_xxxxxxxxxxxxxxxx" | |
| # Slack MCP bot token (xoxb-…, read-only scopes). | |
| # AI_SANDBOX_SLACK_MCP_XOXB_TOKEN="xoxb-xxxxxxxxxxxx-xxxxxxxxxxxx" | |
| # Snowflake — PAT is secret; account is plain and selects the proxy host. | |
| # AI_SANDBOX_SNOWFLAKE_ACCOUNT=org-account | |
| # AI_SANDBOX_SNOWFLAKE_PASSWORD="xxxxxxxxxxxxxxxx" | |
| # Datadog / pup — DD_SITE defaults to datadoghq.com (EU: datadoghq.eu). | |
| # AI_SANDBOX_DD_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | |
| # AI_SANDBOX_DD_APP_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | |
| # AI_SANDBOX_DD_SITE=datadoghq.com | |
| # npm read-only registry token (private package installs). | |
| AI_SANDBOX_NPM_TOKEN="npm_xxxxxxxxxxxxxxxxxxxx" | |
| # Buf Schema Registry token. | |
| # AI_SANDBOX_BUF_TOKEN="xxxxxxxxxxxxxxxx" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # recreate-sandboxes.sh — provision groundcrew sbx sandboxes with proxy-isolated | |
| # secrets. Reproduces the sbx lifecycle groundcrew removed in commit 0669f6d, | |
| # plus the proxy/sentinel secrets model from ../jamietm-ai-sandbox. | |
| # | |
| # USAGE | |
| # # 1. One-time secret setup (register real tokens at the sbx proxy): | |
| # cp config.env.example config.env && $EDITOR config.env # fill op:// refs | |
| # op run --env-file=config.env -- ./recreate-sandboxes.sh secrets | |
| # | |
| # # 2. Create the groundcrew-<agent> sandboxes (pass your workspace dir — | |
| # # where per-ticket worktrees are siblings; defaults to cwd): | |
| # ./recreate-sandboxes.sh --dir ~/Documents/source/cbh-groundcrew | |
| # ./recreate-sandboxes.sh --dir ~/path --force # drop + recreate | |
| # | |
| # # Rotate a secret later: re-run `secrets` (live, no recreate needed). | |
| # | |
| # SECRETS MODEL | |
| # Real tokens are registered at the proxy with `sbx secret set-custom` and never | |
| # enter a sandbox. Each credential's in-sandbox env var holds a deterministic | |
| # non-secret SENTINEL (suffixed with a host-local id in .sbx-host-id); the proxy | |
| # swaps the sentinel for the real value in outbound traffic to the matching host. | |
| # GitHub/OpenAI use sbx's built-in services instead (no sentinel). | |
| # | |
| # COMMON OVERRIDES (env vars) | |
| # PROJECT_DIR host dir mounted into the sandbox (or pass --dir; default: cwd) | |
| # AGENTS agents to provision (default: "claude codex") | |
| # AUTH_MODE "proxy" (default) | "interactive" (in-sandbox login) | |
| # MIRROR_CONFIG copy curated host ~/.claude + ~/.codex into the box (default: true) | |
| # TEMPLATE sbx --template override GIT_DEFAULTS (default: true) | |
| # | |
| set -euo pipefail | |
| # ── Config ────────────────────────────────────────────────────────────────── | |
| PROJECT_DIR="${PROJECT_DIR:-$PWD}" # overridable via --dir <path>; see Main | |
| AGENTS="${AGENTS:-claude codex opencode}" | |
| TEMPLATE="${TEMPLATE:-}" | |
| KITS="${KITS:-}" | |
| GIT_DEFAULTS="${GIT_DEFAULTS:-true}" | |
| MIRROR_CONFIG="${MIRROR_CONFIG:-true}" | |
| SANDBOX_HOME="${SANDBOX_HOME:-/home/agent}" | |
| HOST_HOME="${HOST_HOME:-$HOME}" | |
| AUTH_MODE="${AUTH_MODE:-proxy}" | |
| CONFIG_ENV="${CONFIG_ENV:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/config.env}" | |
| OVERLAY_KIT="${OVERLAY_KIT:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.sbx-kit-local}" | |
| HOST_ID_FILE="${HOST_ID_FILE:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.sbx-host-id}" | |
| # plugins/ excluded: hundreds of MB of marketplace checkouts that overflow sbx | |
| # cp's tar, with host-absolute symlinks that wouldn't resolve in the box. | |
| CLAUDE_ALLOWLIST="CLAUDE.md MEMORY.md skills commands agents memory hooks" | |
| # auth.json excluded — auth is proxy-managed. | |
| CODEX_ALLOWLIST="config.toml AGENTS.md instructions.md prompts" | |
| # opencode config lives under ~/.config/opencode; auth.json (and ~/.local/share) | |
| # stays out — proxy-managed. | |
| OPENCODE_ALLOWLIST="opencode.jsonc opencode.json AGENTS.md skills" | |
| # ── Sentinels ───────────────────────────────────────────────────────────────── | |
| # The same value is registered at the proxy (--placeholder) and injected into the | |
| # sandbox (--env), so they must never drift. The host-local id keeps the sentinel | |
| # unguessable and stable across rotations/recreates. | |
| load_host_id() { | |
| if [ -f "$HOST_ID_FILE" ]; then | |
| HOST_ID="$(tr -d ' \t\r\n' < "$HOST_ID_FILE")" | |
| fi | |
| if [ -z "${HOST_ID:-}" ]; then | |
| if [ -r /dev/urandom ] && command -v od >/dev/null 2>&1; then | |
| HOST_ID="$(od -An -N8 -tx1 /dev/urandom | tr -d ' \n')" | |
| elif command -v uuidgen >/dev/null 2>&1; then | |
| HOST_ID="$(uuidgen | tr -d '-' | tr '[:upper:]' '[:lower:]' | cut -c1-16)" | |
| else | |
| echo "error: no source of randomness for .sbx-host-id" >&2; exit 1 | |
| fi | |
| printf '%s\n' "$HOST_ID" > "$HOST_ID_FILE" | |
| fi | |
| SENTINEL_CLAUDE="claude-code-oauth-token-sentinel-${HOST_ID}" | |
| SENTINEL_LINEAR="linear-api-token-sentinel-${HOST_ID}" | |
| SENTINEL_SLACK="slack-bot-token-sentinel-${HOST_ID}" | |
| SENTINEL_SNOWFLAKE="snowflake-pat-sentinel-${HOST_ID}" | |
| SENTINEL_DD_API="datadog-api-key-sentinel-${HOST_ID}" | |
| SENTINEL_DD_APP="datadog-app-key-sentinel-${HOST_ID}" | |
| SENTINEL_NPM="npm-read-token-sentinel-${HOST_ID}" | |
| SENTINEL_BUF="buf-token-sentinel-${HOST_ID}" | |
| } | |
| # ── Mode: secrets — register real tokens at the proxy ───────────────────────── | |
| assert_resolved() { | |
| local name="$1" val="${2-}" | |
| if [ "${val#op://}" != "$val" ]; then | |
| echo "error: $name is an unresolved op:// ref — wrap with: op run --env-file=config.env -- $0 secrets" >&2 | |
| exit 1 | |
| fi | |
| } | |
| register_proxy_secrets() { | |
| load_host_id | |
| if [ -n "${AI_SANDBOX_GITHUB_TOKEN:-}" ]; then | |
| assert_resolved AI_SANDBOX_GITHUB_TOKEN "$AI_SANDBOX_GITHUB_TOKEN" | |
| printf '%s' "$AI_SANDBOX_GITHUB_TOKEN" | sbx secret set -g github | |
| echo "GitHub: proxy secret set (built-in github service)" | |
| else | |
| echo "GitHub: AI_SANDBOX_GITHUB_TOKEN unset — keeping sbx's built-in github default" | |
| fi | |
| # CLAUDE_CODE_OAUTH_TOKEN: subscription OAuth token from `claude setup-token`, | |
| # NOT an API key. Never set ANTHROPIC_API_KEY — it outranks this and bills usage. | |
| if [ -n "${AI_SANDBOX_CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then | |
| assert_resolved AI_SANDBOX_CLAUDE_CODE_OAUTH_TOKEN "$AI_SANDBOX_CLAUDE_CODE_OAUTH_TOKEN" | |
| sbx secret set-custom -g --host api.anthropic.com \ | |
| --env CLAUDE_CODE_OAUTH_TOKEN --placeholder "$SENTINEL_CLAUDE" \ | |
| --value "$AI_SANDBOX_CLAUDE_CODE_OAUTH_TOKEN" | |
| echo "Claude: proxy secret set (CLAUDE_CODE_OAUTH_TOKEN -> api.anthropic.com)" | |
| else | |
| echo "Claude: AI_SANDBOX_CLAUDE_CODE_OAUTH_TOKEN unset — agent falls back to interactive /login" | |
| fi | |
| if [ -n "${AI_SANDBOX_LINEAR_TOKEN:-}" ]; then | |
| assert_resolved AI_SANDBOX_LINEAR_TOKEN "$AI_SANDBOX_LINEAR_TOKEN" | |
| sbx secret set-custom -g --host mcp.linear.app \ | |
| --env LINEAR_TOKEN --placeholder "$SENTINEL_LINEAR" \ | |
| --value "$AI_SANDBOX_LINEAR_TOKEN" | |
| echo "Linear: proxy secret set (LINEAR_TOKEN -> mcp.linear.app)" | |
| fi | |
| if [ -n "${AI_SANDBOX_SLACK_MCP_XOXB_TOKEN:-}" ]; then | |
| assert_resolved AI_SANDBOX_SLACK_MCP_XOXB_TOKEN "$AI_SANDBOX_SLACK_MCP_XOXB_TOKEN" | |
| sbx secret set-custom -g --host slack.com \ | |
| --env SLACK_MCP_XOXB_TOKEN --placeholder "$SENTINEL_SLACK" \ | |
| --value "$AI_SANDBOX_SLACK_MCP_XOXB_TOKEN" | |
| echo "Slack: proxy secret set (SLACK_MCP_XOXB_TOKEN -> slack.com)" | |
| fi | |
| # Snowflake account is non-secret but selects the proxy host. | |
| if [ -n "${AI_SANDBOX_SNOWFLAKE_PASSWORD:-}" ]; then | |
| assert_resolved AI_SANDBOX_SNOWFLAKE_PASSWORD "$AI_SANDBOX_SNOWFLAKE_PASSWORD" | |
| : "${AI_SANDBOX_SNOWFLAKE_ACCOUNT:?set AI_SANDBOX_SNOWFLAKE_ACCOUNT alongside the PAT}" | |
| sbx secret set-custom -g --host "$AI_SANDBOX_SNOWFLAKE_ACCOUNT.snowflakecomputing.com" \ | |
| --env SNOWFLAKE_PASSWORD --placeholder "$SENTINEL_SNOWFLAKE" \ | |
| --value "$AI_SANDBOX_SNOWFLAKE_PASSWORD" | |
| echo "Snowflake: proxy secret set (SNOWFLAKE_PASSWORD -> $AI_SANDBOX_SNOWFLAKE_ACCOUNT.snowflakecomputing.com)" | |
| fi | |
| if [ -n "${AI_SANDBOX_DD_API_KEY:-}" ]; then | |
| assert_resolved AI_SANDBOX_DD_API_KEY "$AI_SANDBOX_DD_API_KEY" | |
| : "${AI_SANDBOX_DD_APP_KEY:?set AI_SANDBOX_DD_APP_KEY alongside AI_SANDBOX_DD_API_KEY}" | |
| assert_resolved AI_SANDBOX_DD_APP_KEY "$AI_SANDBOX_DD_APP_KEY" | |
| local dd_site="${AI_SANDBOX_DD_SITE:-datadoghq.com}" | |
| sbx secret set-custom -g --host "api.$dd_site" \ | |
| --env DD_API_KEY --placeholder "$SENTINEL_DD_API" --value "$AI_SANDBOX_DD_API_KEY" | |
| sbx secret set-custom -g --host "api.$dd_site" \ | |
| --env DD_APP_KEY --placeholder "$SENTINEL_DD_APP" --value "$AI_SANDBOX_DD_APP_KEY" | |
| echo "Datadog: proxy secrets set (DD_API_KEY/DD_APP_KEY -> api.$dd_site)" | |
| fi | |
| if [ -n "${AI_SANDBOX_NPM_TOKEN:-}" ]; then | |
| assert_resolved AI_SANDBOX_NPM_TOKEN "$AI_SANDBOX_NPM_TOKEN" | |
| sbx secret set-custom -g --host registry.npmjs.org \ | |
| --env NPM_TOKEN --placeholder "$SENTINEL_NPM" --value "$AI_SANDBOX_NPM_TOKEN" | |
| echo "npm: proxy secret set (NPM_TOKEN -> registry.npmjs.org)" | |
| fi | |
| if [ -n "${AI_SANDBOX_BUF_TOKEN:-}" ]; then | |
| assert_resolved AI_SANDBOX_BUF_TOKEN "$AI_SANDBOX_BUF_TOKEN" | |
| sbx secret set-custom -g --host buf.build \ | |
| --env BUF_TOKEN --placeholder "$SENTINEL_BUF" --value "$AI_SANDBOX_BUF_TOKEN" | |
| echo "Buf: proxy secret set (BUF_TOKEN -> buf.build)" | |
| fi | |
| echo "Done. Proxy secrets are global and live — rotate by re-running this with new values." | |
| } | |
| # ── Overlay kit: bake sentinel env vars into the sandbox at create ──────────── | |
| # Reads config.env literally (no op resolution) to decide which env vars to inject; | |
| # only configured services get one. | |
| config_value() { | |
| local key="$1" line val | |
| [ -f "$CONFIG_ENV" ] || return 0 | |
| line="$(grep -E "^[[:space:]]*(export[[:space:]]+)?${key}=" "$CONFIG_ENV" | tail -n1 || true)" | |
| [ -n "$line" ] || return 0 | |
| val="${line#*=}" | |
| case "$val" in | |
| \"*\") val="${val#\"}"; val="${val%\"}" ;; | |
| \'*\') val="${val#\'}"; val="${val%\'}" ;; | |
| esac | |
| printf '%s' "$val" | |
| } | |
| config_has() { [ -n "$(config_value "$1")" ]; } | |
| OVERLAY_VARS_JSON="" | |
| add_var() { | |
| local sep="" | |
| [ -n "$OVERLAY_VARS_JSON" ] && sep="," | |
| OVERLAY_VARS_JSON="${OVERLAY_VARS_JSON}${sep}\"$1\":\"$2\"" | |
| } | |
| render_overlay() { | |
| load_host_id | |
| OVERLAY_VARS_JSON="" | |
| # Gating CLAUDE matters: it outranks /login, so a bogus always-on value would | |
| # break `claude` for anyone who hasn't configured a token. | |
| config_has AI_SANDBOX_CLAUDE_CODE_OAUTH_TOKEN && add_var CLAUDE_CODE_OAUTH_TOKEN "$SENTINEL_CLAUDE" | |
| config_has AI_SANDBOX_LINEAR_TOKEN && add_var LINEAR_TOKEN "$SENTINEL_LINEAR" | |
| config_has AI_SANDBOX_SLACK_MCP_XOXB_TOKEN && add_var SLACK_MCP_XOXB_TOKEN "$SENTINEL_SLACK" | |
| config_has AI_SANDBOX_SNOWFLAKE_PASSWORD && add_var SNOWFLAKE_PASSWORD "$SENTINEL_SNOWFLAKE" | |
| config_has AI_SANDBOX_DD_API_KEY && { add_var DD_API_KEY "$SENTINEL_DD_API"; add_var DD_APP_KEY "$SENTINEL_DD_APP"; } | |
| config_has AI_SANDBOX_NPM_TOKEN && add_var NPM_TOKEN "$SENTINEL_NPM" | |
| config_has AI_SANDBOX_BUF_TOKEN && add_var BUF_TOKEN "$SENTINEL_BUF" | |
| # Non-secret values the tools need alongside the proxied secrets. | |
| local acct user site | |
| acct="$(config_value AI_SANDBOX_SNOWFLAKE_ACCOUNT)"; [ -n "$acct" ] && add_var SNOWFLAKE_ACCOUNT "$acct" | |
| user="$(config_value AI_SANDBOX_SNOWFLAKE_USER)"; [ -n "$user" ] && add_var SNOWFLAKE_USER "$user" | |
| site="$(config_value AI_SANDBOX_DD_SITE)"; [ -n "$site" ] && add_var DD_SITE "$site" | |
| if [ -z "$OVERLAY_VARS_JSON" ]; then | |
| echo "overlay: no configured secrets in $CONFIG_ENV — sandbox gets no sentinel env vars" | |
| OVERLAY_KIT="" | |
| return | |
| fi | |
| mkdir -p "$OVERLAY_KIT" | |
| cat > "$OVERLAY_KIT/spec.yaml" <<EOF | |
| { | |
| "schemaVersion": "1", | |
| "kind": "mixin", | |
| "name": "groundcrew-sandbox-local", | |
| "displayName": "groundcrew sandbox local secrets overlay", | |
| "description": "Generated (gitignored): proxy-managed credential sentinels. Regenerated each create; do not edit.", | |
| "environment": { "variables": { ${OVERLAY_VARS_JSON} } } | |
| } | |
| EOF | |
| echo "overlay: wrote $OVERLAY_KIT/spec.yaml (env vars: $(printf '%s' "$OVERLAY_VARS_JSON" | grep -oE '"[A-Z_]+":' | tr -d '":' | paste -sd' ' -))" | |
| } | |
| # ── Create ──────────────────────────────────────────────────────────────────── | |
| sandbox_name_for() { | |
| printf 'groundcrew-%s' "$1" \ | |
| | tr '[:upper:]' '[:lower:]' \ | |
| | sed -E 's/[^a-z0-9.+-]+/-/g; s/-+/-/g; s/^-|-$//g' | |
| } | |
| sandbox_exists() { | |
| sbx ls 2>/dev/null | awk '{print $1}' | grep -qxF "$1" | |
| } | |
| # Disable signing (no key in the box) and route github over HTTPS so the proxy's | |
| # gh credential applies regardless of how the host cloned. | |
| GIT_DEFAULT_COMMANDS='git config --global commit.gpgsign false && git config --global tag.gpgsign false && (git config --global --unset-all url."https://github.com/".insteadOf 2>/dev/null || true) && git config --global --add url."https://github.com/".insteadOf "git@github.com:" && git config --global --add url."https://github.com/".insteadOf "ssh://git@github.com/"' | |
| apply_git_defaults() { | |
| local name="$1" | |
| echo "${name}: applying git defaults..." | |
| sbx exec "$name" sh -c "$GIT_DEFAULT_COMMANDS" | |
| } | |
| # rm -rf before each copy because `sbx cp` nests source INTO an existing dest | |
| # (skills/ -> skills/skills/). Per-entry failures warn instead of aborting. | |
| mirror_config() { | |
| local name="$1" host_dir="$2" sandbox_dir="$3" allowlist="$4" | |
| [ -d "$host_dir" ] || { echo "${name}: ${host_dir} absent, skipping mirror"; return; } | |
| echo "${name}: mirroring ${host_dir} -> ${sandbox_dir}" | |
| sbx exec "$name" mkdir -p "$sandbox_dir" | |
| for entry in $allowlist; do | |
| local host_path="${host_dir}/${entry}" | |
| [ -e "$host_path" ] || continue | |
| local sandbox_path="${sandbox_dir}/${entry}" | |
| sbx exec "$name" rm -rf "$sandbox_path" || true | |
| if ! sbx cp "$host_path" "${name}:${sandbox_path}"; then | |
| echo "${name}: warning — failed to copy '${entry}', skipping" >&2 | |
| fi | |
| done | |
| } | |
| mirror_agent_config() { | |
| local agent="$1" name="$2" | |
| case "$agent" in | |
| claude) mirror_config "$name" "${HOST_HOME}/.claude" "${SANDBOX_HOME}/.claude" "$CLAUDE_ALLOWLIST" ;; | |
| codex) mirror_config "$name" "${HOST_HOME}/.codex" "${SANDBOX_HOME}/.codex" "$CODEX_ALLOWLIST" ;; | |
| opencode) mirror_config "$name" "${HOST_HOME}/.config/opencode" "${SANDBOX_HOME}/.config/opencode" "$OPENCODE_ALLOWLIST" ;; | |
| *) echo "${name}: no config mirror for agent '${agent}', skipping" ;; | |
| esac | |
| } | |
| # FORCE drops an existing sandbox first so the --kit overlay re-applies (the env | |
| # vars are injected only at create). | |
| ensure_sandbox() { | |
| local agent="$1" name="$2" | |
| if [ "${FORCE:-false}" = "true" ] && sandbox_exists "$name"; then | |
| echo "${name}: --force, dropping existing sandbox..." | |
| sbx rm --force "$name" || echo "${name}: warning — sbx rm failed" >&2 | |
| fi | |
| if sandbox_exists "$name"; then | |
| echo "${name}: already exists" | |
| else | |
| echo "${name}: creating (agent=${agent}, template=${TEMPLATE:-default})" | |
| local args=(create --name "$name") | |
| [ -n "$TEMPLATE" ] && args+=(--template "$TEMPLATE") | |
| for kit in $KITS; do args+=(--kit "$kit"); done | |
| args+=("$agent" "$PROJECT_DIR") | |
| if ! sbx "${args[@]}"; then | |
| sandbox_exists "$name" || { echo "${name}: create failed" >&2; exit 1; } | |
| fi | |
| echo "${name}: created" | |
| fi | |
| [ "$GIT_DEFAULTS" = "true" ] && apply_git_defaults "$name" | |
| [ "$MIRROR_CONFIG" = "true" ] && mirror_agent_config "$agent" "$name" | |
| } | |
| auth_agent_interactive() { | |
| local agent="$1" name="$2" | |
| case "$agent" in | |
| claude) echo "${name}: 'claude auth login'..."; sbx exec -it "$name" claude auth login ;; | |
| codex) echo "${name}: 'codex login --device-auth'..."; sbx exec -it "$name" codex login --device-auth ;; | |
| cursor) echo "${name}: 'cursor-agent login'..."; sbx exec -it -e NO_OPEN_BROWSER=1 "$name" cursor-agent login ;; | |
| *) echo "${name}: no interactive recipe for '${agent}', skipping" >&2 ;; | |
| esac | |
| } | |
| # ── Main ────────────────────────────────────────────────────────────────────── | |
| command -v sbx >/dev/null || { echo "error: 'sbx' not on PATH" >&2; exit 1; } | |
| MODE="create" | |
| FORCE="false" | |
| USAGE="usage: $0 [create|secrets] [--dir <path>] [--force]" | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| --force) FORCE="true" ;; | |
| --dir) shift; [ $# -gt 0 ] || { echo "$USAGE" >&2; exit 2; }; PROJECT_DIR="$1" ;; | |
| --dir=*) PROJECT_DIR="${1#--dir=}" ;; | |
| secrets|create) MODE="$1" ;; | |
| *) echo "$USAGE" >&2; exit 2 ;; | |
| esac | |
| shift | |
| done | |
| if [ "$MODE" = "secrets" ]; then | |
| register_proxy_secrets | |
| exit 0 | |
| fi | |
| [ -d "$PROJECT_DIR" ] || { echo "error: PROJECT_DIR '$PROJECT_DIR' not a dir" >&2; exit 1; } | |
| PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)" | |
| echo "Project dir (mount): $PROJECT_DIR" | |
| echo "Agents: $AGENTS Auth mode: $AUTH_MODE" | |
| [ "$AUTH_MODE" = "proxy" ] && echo "(proxy mode: run '$0 secrets' first so tokens live at the proxy, not in the sandbox)" | |
| if [ "$AUTH_MODE" = "proxy" ]; then | |
| render_overlay | |
| [ -n "$OVERLAY_KIT" ] && KITS="$OVERLAY_KIT${KITS:+ $KITS}" | |
| fi | |
| echo | |
| for agent in $AGENTS; do | |
| name="$(sandbox_name_for "$agent")" | |
| echo "════ ${agent} -> ${name} ════" | |
| ensure_sandbox "$agent" "$name" | |
| if [ "$AUTH_MODE" = "interactive" ]; then | |
| auth_agent_interactive "$agent" "$name" | |
| else | |
| echo "${name}: proxy auth — sentinels injected by registered proxy secrets, no in-sandbox login" | |
| fi | |
| echo | |
| done | |
| echo "Done. Verify with: sbx ls Proxy secrets: sbx secret ls" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment