Загрузить файлы в «/»
This commit is contained in:
+103
@@ -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"]
|
||||||
@@ -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 "$@"
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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 "$@"
|
||||||
Reference in New Issue
Block a user