Загрузить файлы в «/»

This commit is contained in:
2026-07-15 13:15:52 +00:00
parent c5c7a0344e
commit 1a544e4684
5 changed files with 1261 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
# Agent Sandbox — isolated Linux environment for CLI coding agents
# Designed for macOS hosts (Docker Desktop / OrbStack)
FROM ubuntu:24.04
ARG TARGETARCH
ARG DEBIAN_FRONTEND=noninteractive
ARG USERNAME=agent
ARG USER_UID=1000
ARG USER_GID=1000
LABEL org.opencontainers.image.title="asandbox" \
org.opencontainers.image.description="Secure Docker sandbox for CLI AI coding agents" \
org.opencontainers.image.source="local"
# Base tools
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
wget \
git \
gnupg \
openssh-client \
sudo \
less \
nano \
vim-tiny \
jq \
unzip \
zip \
tar \
gzip \
bzip2 \
xz-utils \
build-essential \
python3 \
python3-pip \
python3-venv \
python3-dev \
ripgrep \
fd-find \
tree \
rsync \
locales \
tzdata \
procps \
iproute2 \
iputils-ping \
dnsutils \
&& rm -rf /var/lib/apt/lists/* \
&& ln -sf /usr/bin/fdfind /usr/local/bin/fd \
&& locale-gen en_US.UTF-8
ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
NPM_CONFIG_UPDATE_NOTIFIER=false \
NPM_CONFIG_FUND=false
# Node.js 22 LTS (many agents are Node-based)
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* \
&& corepack enable || true
# uv (fast Python package manager)
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
&& mv /root/.local/bin/uv /usr/local/bin/uv \
&& mv /root/.local/bin/uvx /usr/local/bin/uvx 2>/dev/null || true
# Create non-root user matching host UID/GID when possible (macOS staff=20 often exists as dialout)
RUN set -eux; \
if getent group "${USER_GID}" >/dev/null; then \
EXISTING_GROUP="$(getent group "${USER_GID}" | cut -d: -f1)"; \
useradd --uid "${USER_UID}" --gid "${USER_GID}" -m -s /bin/bash "${USERNAME}" \
|| useradd --uid "${USER_UID}" -g "${EXISTING_GROUP}" -m -s /bin/bash "${USERNAME}"; \
else \
groupadd --gid "${USER_GID}" "${USERNAME}"; \
useradd --uid "${USER_UID}" --gid "${USER_GID}" -m -s /bin/bash "${USERNAME}"; \
fi; \
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/${USERNAME}"; \
chmod 0440 "/etc/sudoers.d/${USERNAME}"
# installagent + shell integration (agents installed on demand inside shell)
COPY installagent /usr/local/bin/installagent
COPY entrypoint.sh /usr/local/bin/asandbox-entrypoint
COPY bashrc.asandbox /etc/asandbox.bashrc
RUN chmod +x /usr/local/bin/installagent /usr/local/bin/asandbox-entrypoint \
&& echo 'source /etc/asandbox.bashrc' >> /home/${USERNAME}/.bashrc \
&& echo 'source /etc/asandbox.bashrc' >> /home/${USERNAME}/.profile \
&& mkdir -p /home/${USERNAME}/.local/bin /home/${USERNAME}/.npm-global/bin \
&& chown -R ${USERNAME} /home/${USERNAME}
# Workspace mount point (host path is also bind-mounted at real path)
RUN mkdir -p /workspace && chown ${USERNAME} /workspace
WORKDIR /workspace
USER ${USERNAME}
ENV HOME=/home/${USERNAME} \
PATH="/home/${USERNAME}/.local/bin:/home/${USERNAME}/.npm-global/bin:/usr/local/bin:${PATH}"
ENTRYPOINT ["/usr/local/bin/asandbox-entrypoint"]
CMD ["bash", "-l"]
+670
View File
@@ -0,0 +1,670 @@
#!/usr/bin/env bash
# asandbox — run any CLI coding agent inside Docker with host FS integration
# Works on macOS (Docker Desktop / OrbStack) and Linux.
set -euo pipefail
VERSION="1.1.0"
# Program root = folder where this script lives (e.g. /Users/macbook/asandbox)
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IMAGE_NAME="${ASANDBOX_IMAGE:-asandbox:latest}"
SHARE_DIR="${ASANDBOX_SHARE:-${ROOT_DIR}}"
CONFIG_DIR="${ASANDBOX_CONFIG_DIR:-${ROOT_DIR}/config}"
CONFIG_FILE="${CONFIG_DIR}/config.env"
DEFAULT_NETWORK="${ASANDBOX_NETWORK:-bridge}"
DEFAULT_MEMORY="${ASANDBOX_MEMORY:-4g}"
DEFAULT_CPUS="${ASANDBOX_CPUS:-4}"
# ── colors ──────────────────────────────────────────────────────────
if [[ -t 2 ]]; then
C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YLW=$'\033[33m'
C_BLU=$'\033[34m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'; C_BLD=$'\033[1m'
else
C_RED=; C_GRN=; C_YLW=; C_BLU=; C_DIM=; C_RST=; C_BLD=
fi
die() { echo "${C_RED}asandbox:${C_RST} $*" >&2; exit 1; }
info() { echo "${C_BLU}asandbox:${C_RST} $*" >&2; }
ok() { echo "${C_GRN}asandbox:${C_RST} $*" >&2; }
warn() { echo "${C_YLW}asandbox:${C_RST} $*" >&2; }
# ── usage ───────────────────────────────────────────────────────────
usage() {
cat <<'EOF'
asandbox — run CLI AI agents in Docker with host filesystem integration
USAGE
asandbox <command> [options] [--] [agent-args...]
COMMANDS
run <agent|cmd...> Run an agent or arbitrary command in the sandbox
shell Interactive bash inside the sandbox
installagent <name> Install an agent inside the sandbox (claude, cursor, …)
build Build (or rebuild) the sandbox Docker image
agents List known agent presets / installagent catalog
config Show / create config
doctor Check Docker, image, mounts
version Print version
help Show this help
OPTIONS (run / shell)
-w, --workdir DIR Host directory to mount (default: git root or cwd)
-n, --name NAME Container name (default: auto)
--network MODE none | bridge | host (default: bridge)
--memory SIZE Memory limit (default: 4g)
--cpus N CPU limit (default: 4)
--read-only Read-only root filesystem (+tmpfs /tmp)
--no-mount-home Do not mount agent config dirs from host home
--mount-ssh Mount ~/.ssh (read-only) for git over SSH
--mount-gitconfig Mount ~/.gitconfig (read-only)
--mount PATH[:CONT] Extra bind mount (repeatable). CONT defaults to PATH
--env KEY=VAL Pass env var (repeatable)
--env-file FILE Pass env file
--pass-env KEY Pass host env KEY if set (repeatable)
--image NAME Override image (default: asandbox:latest)
--rm / --keep Remove container on exit (default) / keep it
--detach, -d Run in background
-q, --quiet Less noise
--dry-run Print docker command and exit
AGENT PRESETS / installagent
claude, codex, gemini, antigravity (agy), cursor (agent),
opencode, aider, qwen, amp, goose, continue, bash
Inside shell: installagent list | installagent claude
Or any command: asandbox run -- node script.js
EXAMPLES
asandbox build
asandbox shell
asandbox installagent list
asandbox installagent claude
asandbox installagent cursor
asandbox installagent antigravity
asandbox installagent opencode
asandbox run claude
asandbox run claude "explain this project"
asandbox run -w ~/proj codex
asandbox run --network none --read-only -- rg "TODO"
asandbox run --mount-ssh -- git pull
asandbox run --env ANTHROPIC_API_KEY=sk-... claude
SECURITY
• No Docker socket is mounted (agents cannot escape via Docker).
• Runs as non-root user inside the container.
• Capabilities dropped; no-new-privileges enabled.
• Host project directory is bind-mounted (same absolute path on Mac).
• Agent configs (~/.claude, ~/.codex, …) mounted so auth/state persist.
• Use --network none for fully offline runs when possible.
CONFIG
Program folder: same directory as this script (default ~/asandbox)
config/config.env optional defaults
Dockerfile, installagent, persist/ live next to the script
EOF
}
# ── config ──────────────────────────────────────────────────────────
load_config() {
if [[ -f "$CONFIG_FILE" ]]; then
# shellcheck disable=SC1090
set -a; source "$CONFIG_FILE"; set +a
fi
}
write_default_config() {
mkdir -p "$CONFIG_DIR"
if [[ -f "$CONFIG_FILE" ]]; then
info "config already exists: $CONFIG_FILE"
return 0
fi
cat > "$CONFIG_FILE" <<'EOF'
# asandbox defaults (bash-compatible env file)
# ASANDBOX_IMAGE=asandbox:latest
# ASANDBOX_NETWORK=bridge
# ASANDBOX_MEMORY=4g
# ASANDBOX_CPUS=4
# ASANDBOX_MOUNT_SSH=0
# ASANDBOX_MOUNT_GITCONFIG=1
# ASANDBOX_QUIET=0
# Extra env keys always forwarded when set on host:
# ASANDBOX_PASS_ENV="ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY GOOGLE_API_KEY XAI_API_KEY"
EOF
ok "wrote $CONFIG_FILE"
}
# ── helpers ─────────────────────────────────────────────────────────
require_docker() {
command -v docker >/dev/null 2>&1 || die "docker not found. Install Docker Desktop or OrbStack."
docker info >/dev/null 2>&1 || die "Docker daemon not running. Start Docker Desktop / OrbStack."
}
image_exists() {
docker image inspect "$IMAGE_NAME" >/dev/null 2>&1
}
resolve_workdir() {
local dir="${1:-}"
if [[ -n "$dir" ]]; then
(cd "$dir" && pwd -P)
return
fi
# Prefer git root when inside a repo
if git rev-parse --show-toplevel >/dev/null 2>&1; then
git rev-parse --show-toplevel
return
fi
pwd -P
}
# Map agent name → argv inside container
resolve_agent_cmd() {
local agent="$1"
shift || true
case "$agent" in
claude|claude-code)
echo "claude"; printf '%s\n' "$@"
;;
codex|openai-codex)
echo "codex"; printf '%s\n' "$@"
;;
gemini|gemini-cli)
echo "gemini"; printf '%s\n' "$@"
;;
antigravity|agy|antigravity-cli)
echo "agy"; printf '%s\n' "$@"
;;
cursor|cursor-cli|cursor-agent)
echo "agent"; printf '%s\n' "$@"
;;
opencode|open-code)
echo "opencode"; printf '%s\n' "$@"
;;
aider|aider-chat)
echo "aider"; printf '%s\n' "$@"
;;
qwen|qwen-code)
echo "qwen"; printf '%s\n' "$@"
;;
amp)
echo "amp"; printf '%s\n' "$@"
;;
goose)
echo "goose"; printf '%s\n' "$@"
;;
continue|cn)
echo "cn"; printf '%s\n' "$@"
;;
installagent|install-agent)
echo "installagent"; printf '%s\n' "$@"
;;
bash|sh|shell)
if [[ $# -eq 0 ]]; then
echo "bash"; echo "-l"
else
echo "bash"; echo "-lc"; echo "$*"
fi
;;
*)
# Treat first token as command
echo "$agent"; printf '%s\n' "$@"
;;
esac
}
# Known host config dirs to bind-mount (host → container path under $HOME)
agent_home_mounts() {
# format: host_relpath
cat <<'EOF'
.claude
.codex
.config/gh
.gemini
.grok
.cursor
.local/share/claude
.npm
.antigravity
.agy
.config/opencode
.config/aider
EOF
}
# Persistent install roots (survive --rm containers)
ensure_persist_dirs() {
mkdir -p \
"${SHARE_DIR}/persist/.local/bin" \
"${SHARE_DIR}/persist/.npm-global/bin" \
"${SHARE_DIR}/persist/.cache"
}
# Env keys commonly needed by agents
default_pass_env_keys() {
echo "ANTHROPIC_API_KEY OPENAI_API_KEY OPENAI_BASE_URL GEMINI_API_KEY GOOGLE_API_KEY GOOGLE_GENAI_API_KEY XAI_API_KEY GROK_API_KEY CURSOR_API_KEY GITHUB_TOKEN GH_TOKEN TERM COLORTERM LANG LC_ALL EDITOR"
}
# ── build ───────────────────────────────────────────────────────────
cmd_build() {
require_docker
local no_cache=0 force=0
while [[ $# -gt 0 ]]; do
case "$1" in
--no-cache) no_cache=1; shift ;;
--force|-f) force=1; shift ;;
-h|--help) echo "Usage: asandbox build [--no-cache] [--force]"; return 0 ;;
*) die "unknown build option: $1" ;;
esac
done
[[ -f "$SHARE_DIR/Dockerfile" ]] || die "Dockerfile not found at $SHARE_DIR/Dockerfile"
local args=(build -t "$IMAGE_NAME" -f "$SHARE_DIR/Dockerfile" "$SHARE_DIR")
if [[ $no_cache -eq 1 ]]; then
args+=(--no-cache)
fi
# Pass host UID/GID so files created in mounts match macOS user
args+=(--build-arg "USER_UID=$(id -u)" --build-arg "USER_GID=$(id -g)")
info "building image ${C_BLD}${IMAGE_NAME}${C_RST} from $SHARE_DIR"
docker "${args[@]}"
ok "image ready: $IMAGE_NAME"
}
# ── doctor ──────────────────────────────────────────────────────────
cmd_doctor() {
echo "${C_BLD}asandbox doctor${C_RST} (v$VERSION)"
echo
if command -v docker >/dev/null 2>&1; then
ok "docker CLI: $(command -v docker)"
else
die "docker CLI missing"
fi
if docker info >/dev/null 2>&1; then
local ctx
ctx=$(docker context show 2>/dev/null || echo unknown)
ok "docker daemon: running (context: $ctx)"
else
die "docker daemon not running"
fi
if image_exists; then
ok "image: $IMAGE_NAME present"
else
warn "image: $IMAGE_NAME missing — run: asandbox build"
fi
echo
echo "paths:"
echo " share: $SHARE_DIR"
echo " config: $CONFIG_FILE"
echo " home: $HOME"
echo " cwd: $(pwd -P)"
if git rev-parse --show-toplevel >/dev/null 2>&1; then
echo " git: $(git rev-parse --show-toplevel)"
fi
echo
echo "platform: $(uname -s)/$(uname -m)"
echo "uid/gid: $(id -u):$(id -g)"
if [[ -f "$SHARE_DIR/Dockerfile" ]]; then
ok "Dockerfile found"
else
warn "Dockerfile missing at $SHARE_DIR"
fi
}
# ── agents list ─────────────────────────────────────────────────────
cmd_agents() {
cat <<'EOF'
Agent presets (run via asandbox <name> or asandbox run <name>):
claude Anthropic Claude Code
codex OpenAI Codex CLI
gemini Google Gemini CLI
antigravity Google Antigravity CLI (binary: agy)
cursor Cursor Agent CLI (binary: agent)
opencode OpenCode
aider Aider
qwen Qwen Code
amp Sourcegraph Amp
goose Block Goose
continue Continue CLI (binary: cn)
bash Interactive shell
Install inside sandbox (persisted under <program>/persist):
asandbox installagent list
asandbox installagent claude
asandbox installagent cursor
asandbox installagent antigravity
asandbox installagent opencode
asandbox installagent all
# or inside shell:
asandbox shell
$ installagent list
$ installagent gemini
Host config dirs auto-mounted (if present):
~/.claude ~/.codex ~/.gemini ~/.grok ~/.cursor ~/.config/gh
~/.antigravity ~/.agy ~/.config/opencode
EOF
}
# ── installagent (host wrapper → container) ─────────────────────────
cmd_installagent() {
if [[ $# -eq 0 ]]; then
set -- list
fi
# Run installagent inside sandbox with network (needed to download)
cmd_run -q --network bridge -- installagent "$@"
}
# ── run / shell ─────────────────────────────────────────────────────
cmd_run() {
require_docker
local workdir="" container_name="" network="$DEFAULT_NETWORK"
local memory="$DEFAULT_MEMORY" cpus="$DEFAULT_CPUS"
local read_only=0 mount_home=1 mount_ssh=0 mount_gitconfig=1
local rm_flag=1 detach=0 quiet=0 dry_run=0
local -a extra_mounts=() env_args=() pass_envs=() env_files=()
local -a raw_cmd=()
# defaults from env/config
[[ "${ASANDBOX_MOUNT_SSH:-0}" == "1" ]] && mount_ssh=1
[[ "${ASANDBOX_MOUNT_GITCONFIG:-1}" == "0" ]] && mount_gitconfig=0
[[ "${ASANDBOX_QUIET:-0}" == "1" ]] && quiet=1
# Parse ASANDBOX_PASS_ENV
if [[ -n "${ASANDBOX_PASS_ENV:-}" ]]; then
# shellcheck disable=SC2206
pass_envs+=($ASANDBOX_PASS_ENV)
fi
# shellcheck disable=SC2206
pass_envs+=($(default_pass_env_keys))
while [[ $# -gt 0 ]]; do
case "$1" in
-w|--workdir) workdir="${2:-}"; shift 2 ;;
-n|--name) container_name="${2:-}"; shift 2 ;;
--network) network="${2:-}"; shift 2 ;;
--memory) memory="${2:-}"; shift 2 ;;
--cpus) cpus="${2:-}"; shift 2 ;;
--read-only) read_only=1; shift ;;
--no-mount-home) mount_home=0; shift ;;
--mount-ssh) mount_ssh=1; shift ;;
--mount-gitconfig) mount_gitconfig=1; shift ;;
--no-mount-gitconfig) mount_gitconfig=0; shift ;;
--mount)
extra_mounts+=("${2:-}")
shift 2
;;
--env|-e)
env_args+=(--env "${2:-}")
shift 2
;;
--env-file)
env_files+=(--env-file "${2:-}")
shift 2
;;
--pass-env)
pass_envs+=("${2:-}")
shift 2
;;
--image) IMAGE_NAME="${2:-}"; shift 2 ;;
--rm) rm_flag=1; shift ;;
--keep) rm_flag=0; shift ;;
-d|--detach) detach=1; shift ;;
-q|--quiet) quiet=1; shift ;;
--dry-run) dry_run=1; shift ;;
-h|--help) usage; return 0 ;;
--) shift; raw_cmd+=("$@"); break ;;
-*)
die "unknown option: $1 (try --help)"
;;
*)
raw_cmd+=("$@")
break
;;
esac
done
if ! image_exists; then
warn "image $IMAGE_NAME not found — building now..."
cmd_build
fi
local host_wd
host_wd=$(resolve_workdir "$workdir")
[[ -d "$host_wd" ]] || die "workdir does not exist: $host_wd"
# Resolve command
local -a cmd=()
if [[ ${#raw_cmd[@]} -eq 0 ]]; then
cmd=(bash)
else
# read resolve_agent_cmd output into array
local line
while IFS= read -r line; do
cmd+=("$line")
done < <(resolve_agent_cmd "${raw_cmd[@]}")
fi
# Unique name
if [[ -z "$container_name" ]]; then
local base slug
base=$(basename "$host_wd" | tr -c 'a-zA-Z0-9._-' '-' | tr '[:upper:]' '[:lower:]')
slug=$(echo "${cmd[0]}" | tr -c 'a-zA-Z0-9' '-')
container_name="asandbox-${base}-${slug}-$$"
# docker name length limit
container_name="${container_name:0:60}"
fi
local -a docker_args=(
run
--name "$container_name"
--hostname "asandbox"
--network "$network"
--memory "$memory"
--cpus "$cpus"
--security-opt no-new-privileges:true
--cap-drop ALL
--cap-add CHOWN
--cap-add SETUID
--cap-add SETGID
--cap-add DAC_OVERRIDE
--cap-add FOWNER
--cap-add NET_BIND_SERVICE
# Interactive when stdin is a TTY and not detached
)
if [[ $rm_flag -eq 1 ]]; then
docker_args+=(--rm)
fi
if [[ $detach -eq 1 ]]; then
docker_args+=(-d)
else
docker_args+=(-i)
if [[ -t 0 && -t 1 ]]; then
docker_args+=(-t)
fi
fi
if [[ $read_only -eq 1 ]]; then
docker_args+=(--read-only --tmpfs /tmp:rw,exec,nosuid,size=1g --tmpfs /home/agent:rw,exec,nosuid,size=512m)
fi
# ── filesystem integration ──────────────────────────────────────
# 1) Mount workspace at the SAME absolute path (critical for Mac path fidelity)
docker_args+=(-v "${host_wd}:${host_wd}:rw")
# 2) Also expose as /workspace for convenience
docker_args+=(-v "${host_wd}:/workspace:rw")
docker_args+=(-w "$host_wd")
docker_args+=(-e "ASANDBOX_WORKDIR=$host_wd")
docker_args+=(-e "ASANDBOX_HOST_OS=$(uname -s)")
docker_args+=(-e "ASANDBOX_QUIET=$quiet")
# 2b) Persist installagent packages across container runs
ensure_persist_dirs
docker_args+=(-v "${SHARE_DIR}/persist/.local:/home/agent/.local:rw")
docker_args+=(-v "${SHARE_DIR}/persist/.npm-global:/home/agent/.npm-global:rw")
docker_args+=(-v "${SHARE_DIR}/persist/.cache:/home/agent/.cache:rw")
# 3) Agent config / state from host home
if [[ $mount_home -eq 1 ]]; then
local rel host_path cont_path
while IFS= read -r rel; do
[[ -z "$rel" ]] && continue
host_path="${HOME}/${rel}"
cont_path="/home/agent/${rel}"
if [[ -e "$host_path" ]]; then
# ensure parent exists in container via mount of the path itself
docker_args+=(-v "${host_path}:${cont_path}:rw")
fi
done < <(agent_home_mounts)
fi
# 4) SSH keys (optional, read-only)
if [[ $mount_ssh -eq 1 && -d "${HOME}/.ssh" ]]; then
docker_args+=(-v "${HOME}/.ssh:/home/agent/.ssh:ro")
fi
# 5) gitconfig
if [[ $mount_gitconfig -eq 1 && -f "${HOME}/.gitconfig" ]]; then
docker_args+=(-v "${HOME}/.gitconfig:/home/agent/.gitconfig:ro")
fi
# 6) Extra mounts
local m host_m cont_m
for m in "${extra_mounts[@]+"${extra_mounts[@]}"}"; do
if [[ "$m" == *:* ]]; then
host_m="${m%%:*}"
cont_m="${m#*:}"
else
host_m="$m"
cont_m="$m"
fi
# resolve host side if relative
if [[ "$host_m" != /* ]]; then
host_m="$(cd "$(dirname "$host_m")" && pwd -P)/$(basename "$host_m")"
fi
docker_args+=(-v "${host_m}:${cont_m}")
done
# 7) Pass through env (bash 3.2-compatible dedupe)
local key seen_env=" "
for key in "${pass_envs[@]+"${pass_envs[@]}"}"; do
[[ -z "$key" ]] && continue
case "$seen_env" in
*" ${key} "*) continue ;;
esac
seen_env+="${key} "
# indirect expansion works in bash 3.2
if [[ -n "${!key:-}" ]]; then
docker_args+=(-e "${key}=${!key}")
fi
done
# Git author from host
local gname gemail
gname=$(git config --global user.name 2>/dev/null || true)
gemail=$(git config --global user.email 2>/dev/null || true)
[[ -n "$gname" ]] && docker_args+=(-e "GIT_AUTHOR_NAME=$gname" -e "GIT_COMMITTER_NAME=$gname")
[[ -n "$gemail" ]] && docker_args+=(-e "GIT_AUTHOR_EMAIL=$gemail" -e "GIT_COMMITTER_EMAIL=$gemail")
docker_args+=("${env_args[@]+"${env_args[@]}"}")
docker_args+=("${env_files[@]+"${env_files[@]}"}")
# Label for cleanup
docker_args+=(--label "asandbox=1" --label "asandbox.workdir=${host_wd}")
docker_args+=("$IMAGE_NAME")
docker_args+=("${cmd[@]}")
if [[ $quiet -eq 0 ]]; then
info "workdir ${C_DIM}${host_wd}${C_RST}"
info "network ${C_DIM}${network}${C_RST} memory=${memory} cpus=${cpus}"
info "command ${C_DIM}${cmd[*]}${C_RST}"
fi
if [[ $dry_run -eq 1 ]]; then
echo "docker $(printf '%q ' "${docker_args[@]}")"
return 0
fi
# Never mount docker.sock — explicit safety check
local a
for a in "${docker_args[@]}"; do
if [[ "$a" == *docker.sock* ]]; then
die "refusing to mount docker.sock"
fi
done
exec docker "${docker_args[@]}"
}
cmd_shell() {
cmd_run "$@" -- bash
}
cmd_config() {
case "${1:-show}" in
show|cat)
if [[ -f "$CONFIG_FILE" ]]; then
echo "# $CONFIG_FILE"
cat "$CONFIG_FILE"
else
warn "no config yet — run: asandbox config init"
fi
;;
init|create)
write_default_config
;;
path)
echo "$CONFIG_FILE"
;;
*)
die "usage: asandbox config [show|init|path]"
;;
esac
}
# ── main ────────────────────────────────────────────────────────────
main() {
load_config
local cmd="${1:-}"
if [[ -z "$cmd" ]]; then
usage
exit 0
fi
shift || true
case "$cmd" in
run) cmd_run "$@" ;;
shell|sh|bash) cmd_shell "$@" ;;
installagent|install-agent|ia)
cmd_installagent "$@"
;;
build) cmd_build "$@" ;;
agents|list) cmd_agents "$@" ;;
config) cmd_config "$@" ;;
doctor) cmd_doctor "$@" ;;
version|--version|-V) echo "asandbox $VERSION" ;;
help|-h|--help) usage ;;
# Convenience: asandbox claude ... → asandbox run claude ...
claude|claude-code|codex|gemini|gemini-cli|aider|antigravity|agy|cursor|opencode|qwen|amp|goose)
cmd_run "$cmd" "$@"
;;
*)
# Unknown subcommand: treat as agent/command name
cmd_run "$cmd" "$@"
;;
esac
}
main "$@"
+17
View File
@@ -0,0 +1,17 @@
# Sourced for interactive shells inside asandbox
export PATH="${HOME}/.local/bin:${HOME}/.npm-global/bin:${PATH}"
# npm user prefix for installagent
if command -v npm >/dev/null 2>&1; then
npm config set prefix "${HOME}/.npm-global" >/dev/null 2>&1 || true
fi
alias install-agent='installagent'
alias agents='installagent list'
# Helpful tip once per shell when installagent exists
if [[ $- == *i* ]] && command -v installagent >/dev/null 2>&1; then
if [[ "${ASANDBOX_QUIET:-0}" != "1" && "${ASANDBOX_NO_TIP:-0}" != "1" ]]; then
echo "tip: installagent list | installagent claude|cursor|opencode|antigravity|gemini|..." >&2
fi
fi
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# asandbox container entrypoint — normalize env, ensure dirs, run command
set -euo pipefail
# Ensure common agent config + install dirs exist (host mounts may be empty)
mkdir -p \
"${HOME}/.claude" \
"${HOME}/.codex" \
"${HOME}/.config" \
"${HOME}/.local/bin" \
"${HOME}/.npm-global/bin" \
"${HOME}/.ssh" \
"${HOME}/.npm" \
"${HOME}/.gemini" \
"${HOME}/.cursor" \
2>/dev/null || true
export PATH="${HOME}/.local/bin:${HOME}/.npm-global/bin:/usr/local/bin:${PATH}"
# npm prefix for user installs via installagent
if command -v npm >/dev/null 2>&1; then
npm config set prefix "${HOME}/.npm-global" >/dev/null 2>&1 || true
fi
# Git identity fallback so commits work without host gitconfig mount
if [[ -n "${GIT_AUTHOR_NAME:-}" ]]; then
git config --global user.name "${GIT_AUTHOR_NAME}" 2>/dev/null || true
fi
if [[ -n "${GIT_AUTHOR_EMAIL:-}" ]]; then
git config --global user.email "${GIT_AUTHOR_EMAIL}" 2>/dev/null || true
fi
# Respect ASANDBOX_WORKDIR if set
if [[ -n "${ASANDBOX_WORKDIR:-}" && -d "${ASANDBOX_WORKDIR}" ]]; then
cd "${ASANDBOX_WORKDIR}"
fi
# Interactive shell banner
_is_shell=0
if [[ $# -eq 0 || "${1:-}" == "bash" || "${1:-}" == "sh" || "${1:-}" == "bash" ]]; then
_is_shell=1
fi
if [[ "${ASANDBOX_BANNER:-0}" == "1" ]] || { [[ -t 0 ]] && [[ $_is_shell -eq 1 ]]; }; then
if [[ "${ASANDBOX_QUIET:-0}" != "1" ]]; then
echo "┌──────────────────────────────────────────────────┐" >&2
echo "│ asandbox — isolated agent environment │" >&2
echo "│ cwd: $(pwd)" >&2
echo "│ │" >&2
echo "│ installagent list │" >&2
echo "│ installagent <name> # claude cursor opencode │" >&2
echo "│ # antigravity gemini … │" >&2
echo "└──────────────────────────────────────────────────┘" >&2
fi
fi
if [[ $# -eq 0 ]]; then
exec bash -l
fi
# If launching bash login/interactive, keep as-is
exec "$@"
+409
View File
@@ -0,0 +1,409 @@
#!/usr/bin/env bash
# installagent — install CLI AI coding agents inside asandbox
# Usage: installagent <name> | installagent list | installagent status
set -euo pipefail
VERSION="1.1.0"
if [[ -t 2 ]]; then
C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YLW=$'\033[33m'
C_BLU=$'\033[34m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'; C_BLD=$'\033[1m'
else
C_RED=; C_GRN=; C_YLW=; C_BLU=; C_DIM=; C_RST=; C_BLD=
fi
die() { echo "${C_RED}installagent:${C_RST} $*" >&2; exit 1; }
info() { echo "${C_BLU}installagent:${C_RST} $*" >&2; }
ok() { echo "${C_GRN}installagent:${C_RST} $*" >&2; }
warn() { echo "${C_YLW}installagent:${C_RST} $*" >&2; }
ensure_local_path() {
mkdir -p "${HOME}/.local/bin" "${HOME}/.local/lib" "${HOME}/.npm-global"
# Prefer user-local npm so installs survive container recreate (via persist mount)
if command -v npm >/dev/null 2>&1; then
npm config set prefix "${HOME}/.npm-global" >/dev/null 2>&1 || true
fi
export PATH="${HOME}/.local/bin:${HOME}/.npm-global/bin:${PATH}"
}
npm_g() {
ensure_local_path
npm install -g --prefix "${HOME}/.npm-global" "$@"
}
have_cmd() { command -v "$1" >/dev/null 2>&1; }
# ── catalog ─────────────────────────────────────────────────────────
# id|aliases|binary|description
catalog() {
cat <<'EOF'
antigravity|agy,antigravity-cli|agy|Google Antigravity CLI
cursor|cursor-cli,cursor-agent|agent|Cursor Agent CLI
opencode|open-code|opencode|OpenCode open-source coding agent
claude|claude-code,claude-cli|claude|Anthropic Claude Code
gemini|gemini-cli,google-gemini|gemini|Google Gemini CLI
codex|openai-codex|codex|OpenAI Codex CLI
aider|aider-chat|aider|Aider Python coding agent
qwen|qwen-code,qwen-cli|qwen|Qwen Code CLI
amp|sourcegraph-amp|amp|Sourcegraph Amp CLI
goose|block-goose|goose|Block Goose agent
continue|cn|cn|Continue CLI
EOF
}
normalize_name() {
echo "$1" | tr '[:upper:]' '[:lower:]' | tr '_' '-'
}
resolve_id() {
local want
want=$(normalize_name "$1")
local line id aliases
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
id="${line%%|*}"
aliases="${line#*|}"
aliases="${aliases%%|*}"
if [[ "$want" == "$id" ]]; then
echo "$id"
return 0
fi
local a
IFS=',' read -ra _als <<< "$aliases"
for a in "${_als[@]}"; do
a=$(echo "$a" | tr -d ' ')
if [[ "$want" == "$a" ]]; then
echo "$id"
return 0
fi
done
done < <(catalog)
return 1
}
agent_binary() {
local id="$1" line
while IFS= read -r line; do
[[ "${line%%|*}" == "$id" ]] || continue
# id|aliases|binary|desc
local rest="${line#*|}"
rest="${rest#*|}"
echo "${rest%%|*}"
return 0
done < <(catalog)
}
agent_desc() {
local id="$1" line
while IFS= read -r line; do
[[ "${line%%|*}" == "$id" ]] || continue
echo "${line##*|}"
return 0
done < <(catalog)
}
# ── installers ──────────────────────────────────────────────────────
install_antigravity() {
info "installing Antigravity CLI (agy)..."
ensure_local_path
curl -fsSL https://antigravity.google/cli/install.sh | bash
# Some installers put binary only in a custom dir — ensure on PATH
if ! have_cmd agy; then
# common locations
for p in \
"${HOME}/.local/bin/agy" \
"${HOME}/.antigravity/bin/agy" \
"${HOME}/.agy/bin/agy" \
/usr/local/bin/agy; do
if [[ -x "$p" ]]; then
mkdir -p "${HOME}/.local/bin"
ln -sfn "$p" "${HOME}/.local/bin/agy"
break
fi
done
fi
have_cmd agy || die "agy not found after install — check installer output"
ok "Antigravity CLI ready: $(command -v agy)"
agy --version 2>/dev/null || agy version 2>/dev/null || true
}
install_cursor() {
info "installing Cursor Agent CLI..."
ensure_local_path
curl -fsSL https://cursor.com/install | bash
if ! have_cmd agent; then
for p in \
"${HOME}/.local/bin/agent" \
"${HOME}/.cursor/bin/agent" \
"${HOME}/.cursor-agent/bin/agent"; do
if [[ -x "$p" ]]; then
mkdir -p "${HOME}/.local/bin"
ln -sfn "$p" "${HOME}/.local/bin/agent"
break
fi
done
fi
have_cmd agent || die "agent not found after install"
ok "Cursor CLI ready: $(command -v agent)"
agent --version 2>/dev/null || true
}
install_opencode() {
info "installing OpenCode..."
ensure_local_path
if have_cmd npm; then
npm_g opencode-ai
else
curl -fsSL https://opencode.ai/install | bash
fi
if ! have_cmd opencode; then
for p in "${HOME}/.npm-global/bin/opencode" "${HOME}/.local/bin/opencode" /usr/local/bin/opencode; do
[[ -x "$p" ]] && ln -sfn "$p" "${HOME}/.local/bin/opencode" && break
done
fi
have_cmd opencode || die "opencode not found after install"
ok "OpenCode ready: $(command -v opencode)"
opencode --version 2>/dev/null || true
}
install_claude() {
info "installing Claude Code..."
have_cmd npm || die "npm required (Node.js)"
npm_g @anthropic-ai/claude-code
have_cmd claude || die "claude not found after install"
ok "Claude Code ready: $(command -v claude)"
claude --version 2>/dev/null || true
}
install_gemini() {
info "installing Gemini CLI..."
have_cmd npm || die "npm required (Node.js)"
npm_g @google/gemini-cli
have_cmd gemini || die "gemini not found after install"
ok "Gemini CLI ready: $(command -v gemini)"
gemini --version 2>/dev/null || true
}
install_codex() {
info "installing OpenAI Codex CLI..."
have_cmd npm || die "npm required (Node.js)"
npm_g @openai/codex
have_cmd codex || die "codex not found after install"
ok "Codex ready: $(command -v codex)"
codex --version 2>/dev/null || true
}
install_aider() {
info "installing Aider..."
ensure_local_path
if have_cmd uv; then
uv tool install aider-chat
elif have_cmd pip3; then
pip3 install --user --break-system-packages aider-chat \
|| pip3 install --user aider-chat
else
die "need uv or pip3 to install aider"
fi
# uv tools go to ~/.local/bin
have_cmd aider || die "aider not found after install — ensure ~/.local/bin is on PATH"
ok "Aider ready: $(command -v aider)"
aider --version 2>/dev/null || true
}
install_qwen() {
info "installing Qwen Code..."
have_cmd npm || die "npm required (Node.js)"
npm_g @qwen-code/qwen-code
# binary name may be qwen or qwen-code
if ! have_cmd qwen && have_cmd qwen-code; then
ln -sfn "$(command -v qwen-code)" "${HOME}/.local/bin/qwen"
fi
if have_cmd qwen || have_cmd qwen-code; then
ok "Qwen Code ready: $(command -v qwen 2>/dev/null || command -v qwen-code)"
else
die "qwen binary not found after install"
fi
}
install_amp() {
info "installing Amp (Sourcegraph)..."
ensure_local_path
# Official installer when available
if curl -fsSL https://ampcode.com/install.sh 2>/dev/null | bash; then
:
elif have_cmd npm; then
npm_g @sourcegraph/amp 2>/dev/null || npm_g amp 2>/dev/null || true
fi
have_cmd amp || warn "amp may need manual install — see https://ampcode.com"
have_cmd amp && ok "Amp ready: $(command -v amp)"
}
install_goose() {
info "installing Goose..."
ensure_local_path
curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh 2>/dev/null | bash \
|| curl -fsSL https://raw.githubusercontent.com/block/goose/main/download_cli.sh 2>/dev/null | bash \
|| true
if ! have_cmd goose; then
# fallback cargo if present
if have_cmd cargo; then
cargo install goose-cli 2>/dev/null || true
fi
fi
have_cmd goose || die "goose not found after install — see https://github.com/block/goose"
ok "Goose ready: $(command -v goose)"
}
install_continue() {
info "installing Continue CLI..."
ensure_local_path
have_cmd npm || die "npm required"
npm_g @continuedev/cli 2>/dev/null || npm_g continue-cli 2>/dev/null || true
if have_cmd cn || have_cmd continue; then
ok "Continue ready: $(command -v cn 2>/dev/null || command -v continue)"
else
warn "Continue CLI package name may have changed — try: npm install -g @continuedev/cli"
fi
}
do_install() {
local id="$1"
case "$id" in
antigravity) install_antigravity ;;
cursor) install_cursor ;;
opencode) install_opencode ;;
claude) install_claude ;;
gemini) install_gemini ;;
codex) install_codex ;;
aider) install_aider ;;
qwen) install_qwen ;;
amp) install_amp ;;
goose) install_goose ;;
continue) install_continue ;;
*) die "no installer for: $id" ;;
esac
}
cmd_list() {
echo "${C_BLD}Available agents${C_RST} (installagent <name>)"
echo
printf " ${C_BLD}%-14s${C_RST} %-12s %s\n" "NAME" "BINARY" "DESCRIPTION"
printf " %-14s %-12s %s\n" "----" "------" "-----------"
local line id aliases bin desc
while IFS= read -r line; do
id="${line%%|*}"
rest="${line#*|}"
aliases="${rest%%|*}"
rest="${rest#*|}"
bin="${rest%%|*}"
desc="${rest#*|}"
local mark=" "
if have_cmd "$bin"; then
mark="${C_GRN}✓${C_RST}"
else
mark="${C_DIM}·${C_RST}"
fi
printf " %s %-12s %-12s %s\n" "$mark" "$id" "$bin" "$desc"
if [[ -n "$aliases" ]]; then
printf " ${C_DIM}aliases: %s${C_RST}\n" "$aliases"
fi
done < <(catalog)
echo
echo "Examples:"
echo " installagent claude"
echo " installagent antigravity"
echo " installagent cursor"
echo " installagent opencode"
echo " installagent all # install core set"
}
cmd_status() {
ensure_local_path
echo "${C_BLD}Installed agents${C_RST}"
local line id bin
while IFS= read -r line; do
id="${line%%|*}"
rest="${line#*|}"
rest="${rest#*|}"
bin="${rest%%|*}"
if have_cmd "$bin"; then
printf " ${C_GRN}✓${C_RST} %-14s %s\n" "$id" "$(command -v "$bin")"
else
printf " ${C_DIM}·${C_RST} %-14s (not installed)\n" "$id"
fi
done < <(catalog)
echo
echo "PATH includes:"
echo " ${HOME}/.local/bin"
echo " ${HOME}/.npm-global/bin"
}
cmd_all() {
# Core set commonly used
local core=(claude gemini codex opencode cursor antigravity aider)
local name
for name in "${core[@]}"; do
info "── $name ──"
if do_install "$name"; then
ok "$name done"
else
warn "$name failed (continuing)"
fi
done
cmd_status
}
usage() {
cat <<EOF
installagent v${VERSION} — install CLI AI agents inside asandbox
USAGE
installagent <name> Install agent by name or alias
installagent list Show catalog
installagent status Show what is installed
installagent all Install core agents
installagent help This help
AGENTS
antigravity (agy) Google Antigravity CLI
cursor (agent) Cursor Agent CLI
opencode OpenCode
claude Claude Code
gemini Gemini CLI
codex OpenAI Codex
aider Aider
qwen Qwen Code
amp Sourcegraph Amp
goose Block Goose
continue Continue CLI
Installs go to ~/.local and ~/.npm-global (persisted by asandbox).
EOF
}
main() {
ensure_local_path
local cmd="${1:-list}"
shift || true
case "$cmd" in
list|ls|-l) cmd_list ;;
status|st) cmd_status ;;
all) cmd_all ;;
help|-h|--help) usage ;;
version|-V|--version) echo "installagent $VERSION" ;;
*)
local id
if ! id=$(resolve_id "$cmd"); then
die "unknown agent: $cmd (try: installagent list)"
fi
info "resolved '$cmd' → $id ($(agent_desc "$id"))"
do_install "$id"
echo
ok "run: $(agent_binary "$id")"
;;
esac
}
main "$@"