#!/usr/bin/env bash set -euo pipefail # @describe Start (or reuse) a sandbox Docker container with a bind-mounted workspace. # @option --workspace! Host path to mount into the container at /work (must be under $AICHAT_SANDBOX_BASE). # @option --image Docker image to use. Default: aichat-docker:latest # @option --name Optional explicit container name. Default: auto-generated. # @option --network Enable network access (true/false). Default: true # @option --ttl_minutes Optional TTL label for cleanup tooling. Default: 0 (no TTL) main() { local base="${AICHAT_SANDBOX_BASE:-$HOME/aichat-workspaces}" local image="${argc_image:-aichat-docker:latest}" local net="${argc_network:-true}" local ttl="${argc_ttl_minutes:-0}" mkdir -p "$base" local ws ws="$(realpath -m "${argc_workspace}")" local base_real base_real="$(realpath -m "$base")" case "$ws" in "$base_real"/*) ;; *) echo "ERROR: workspace must be under $base_real (got $ws)" >> "$LLM_OUTPUT" exit 2 ;; esac mkdir -p "$ws" local name="${argc_name:-aichat-sbx-$(date +%s)-$RANDOM}" if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then printf '{"container":"%s","workspace":"%s","status":"already-exists"}\n' "$name" "$ws" >> "$LLM_OUTPUT" return 0 fi local net_arg=() if [[ "$net" == "false" ]]; then net_arg+=(--network none) fi local uid gid uid="$(id -u)" gid="$(id -g)" docker run -d --rm \ --name "$name" \ -u "${uid}:${gid}" \ -w /work \ -v "${ws}:/work:rw" \ --cap-drop=ALL \ --security-opt=no-new-privileges \ --pids-limit 256 \ --memory 2g \ --cpus 2 \ "${net_arg[@]}" \ --label "aichat.sandbox=true" \ --label "aichat.ttl_minutes=${ttl}" \ "$image" \ sh -lc 'sleep infinity' >/dev/null printf '{"container":"%s","workspace":"%s","status":"started"}\n' "$name" "$ws" >> "$LLM_OUTPUT" } eval "$(argc --argc-eval "$0" "$@")"