diff --git a/README.md b/README.md index 29ceed0..73f9d51 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,24 @@ To start over with a clean sandbox, delete the container and run the installer a container delete coderunner && ./install.sh ``` +### Resource limits and workspace mounts + +CPU and memory limits can be set with installer options or the `CODERUNNER_CPUS` and `CODERUNNER_MEMORY` environment variables: + +```bash +./install.sh --cpus 4 --memory 8g +``` + +Additional host directories must be mounted under `/workspace`. They are read-only by default; add `:rw` only when the sandbox needs to write to the host: + +```bash +./install.sh \ + --mount "$HOME/projects/api:/workspace/api" \ + --mount "$HOME/projects/output:/workspace/output:rw" +``` + +Extra mounts default to none. Resource and mount settings are fixed when the container is created; delete the container before changing them. + ### Disable outbound network access By default, code running in the sandbox has unrestricted network access. To run it on a host-only network with no internet access: diff --git a/install.sh b/install.sh index 4689cab..f53ec90 100755 --- a/install.sh +++ b/install.sh @@ -19,6 +19,120 @@ install_container_tool() { rm -f "$CONTAINER_INSTALLER" } +CPUS="${CODERUNNER_CPUS:-8}" +MEMORY="${CODERUNNER_MEMORY:-4g}" +MOUNT_SPECS=() +VOLUME_ARGS=() + +usage() { + cat <<'EOF' +Usage: ./install.sh [options] + +Options: + --cpus N CPU count (default: 8) + --memory SIZE Memory limit, e.g. 4g or 8192m (default: 4g) + --mount HOST:TARGET[:ro|rw] + Mount a host directory under /workspace. + Mounts are read-only unless :rw is specified. + -h, --help Show this help +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --cpus) + [ "$#" -ge 2 ] || { echo "Error: --cpus requires a value." >&2; exit 2; } + CPUS="$2" + shift 2 + ;; + --memory) + [ "$#" -ge 2 ] || { echo "Error: --memory requires a value." >&2; exit 2; } + MEMORY="$2" + shift 2 + ;; + --mount) + [ "$#" -ge 2 ] || { echo "Error: --mount requires a value." >&2; exit 2; } + MOUNT_SPECS+=("$2") + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Error: unknown option '$1'." >&2 + usage >&2 + exit 2 + ;; + esac +done + +if ! [[ "$CPUS" =~ ^[1-9][0-9]*$ ]]; then + echo "Error: CPU count must be a positive integer." >&2 + exit 2 +fi +if ! [[ "$MEMORY" =~ ^[1-9][0-9]*([KkMmGgTtPp][Bb]?)?$ ]]; then + echo "Error: memory must be a positive size such as 4g or 8192m." >&2 + exit 2 +fi +MEMORY=$(printf '%s' "$MEMORY" | tr '[:upper:]' '[:lower:]') +MEMORY=${MEMORY%b} + +NORMALIZED_MOUNTS=() +for spec in ${MOUNT_SPECS[@]+"${MOUNT_SPECS[@]}"}; do + source_path=${spec%%:*} + remainder=${spec#*:} + if [ "$remainder" = "$spec" ] || [ -z "$source_path" ] || [ -z "$remainder" ]; then + echo "Error: mount must use HOST:TARGET[:ro|rw]." >&2 + exit 2 + fi + target_path=${remainder%%:*} + if [ "$target_path" = "$remainder" ]; then + mode="ro" + else + mode=${remainder#*:} + if [[ "$mode" == *:* ]]; then + echo "Error: mount must use HOST:TARGET[:ro|rw]." >&2 + exit 2 + fi + fi + if [ "$mode" != "ro" ] && [ "$mode" != "rw" ]; then + echo "Error: mount mode must be 'ro' or 'rw'." >&2 + exit 2 + fi + if [ ! -d "$source_path" ]; then + echo "Error: mount source is not a directory: $source_path" >&2 + exit 2 + fi + source_path=$(cd "$source_path" && pwd -P) + case "$target_path" in + /workspace|/workspace/*) ;; + *) + echo "Error: mount targets must be /workspace or a path below it." >&2 + exit 2 + ;; + esac + if [[ "$target_path" == *"/../"* ]] || [[ "$target_path" == */.. ]] || [[ "$target_path" == *"//"* ]]; then + echo "Error: mount target contains an invalid path segment." >&2 + exit 2 + fi + for existing in ${NORMALIZED_MOUNTS[@]+"${NORMALIZED_MOUNTS[@]}"}; do + existing_target=${existing#*:} + existing_target=${existing_target%:*} + if [ "$existing_target" = "$target_path" ]; then + echo "Error: duplicate mount target: $target_path" >&2 + exit 2 + fi + done + NORMALIZED_MOUNTS+=("$source_path:$target_path:$mode") + if [ "$mode" = "ro" ]; then + VOLUME_ARGS+=(--volume "$source_path:$target_path:ro") + else + # Apple's --volume syntax represents writable mounts by omitting :ro. + VOLUME_ARGS+=(--volume "$source_path:$target_path") + fi +done + # Function to get current macOS version get_macos_version() { sw_vers -productVersion | awk -F. '{print $1 "." $2}' @@ -199,6 +313,26 @@ wait_for_server() { return 1 } +CONFIG_FILE="$HOME/.coderunner/container-config" +desired_config="cpus=$CPUS"$'\n'"memory=$MEMORY" +if [ -n "${NORMALIZED_MOUNTS[*]-}" ]; then + sorted_mounts=$(printf '%s\n' ${NORMALIZED_MOUNTS[@]+"${NORMALIZED_MOUNTS[@]}"} | LC_ALL=C sort) + desired_config="${desired_config}"$'\n'"$(printf '%s\n' "$sorted_mounts" | sed 's/^/mount=/')" +fi +if container inspect coderunner &>/dev/null; then + if [ -f "$CONFIG_FILE" ]; then + if [ "$(cat "$CONFIG_FILE")" != "$desired_config" ]; then + echo "❌ Existing container was created with different resource or mount settings." + echo " Recreate it with: container delete coderunner && ./install.sh [options]" + exit 1 + fi + elif [ "$desired_config" != $'cpus=8\nmemory=4g' ]; then + echo "❌ Existing container predates configurable resources and mounts." + echo " Recreate it with: container delete coderunner && ./install.sh [options]" + exit 1 + fi +fi + # Stop any existing coderunner container echo "Stopping any existing coderunner container..." container stop coderunner 2>/dev/null || true @@ -222,6 +356,11 @@ print(networks[0]["network"] if networks else "default") exit $? fi fi +if container inspect coderunner &>/dev/null; then + echo "❌ Existing coderunner container could not be started." + echo " Check its logs, or recreate it with: container delete coderunner && ./install.sh [options]" + exit 1 +fi echo "Pulling the latest image: instavm/coderunner" if ! container image pull instavm/coderunner; then @@ -230,16 +369,18 @@ if ! container image pull instavm/coderunner; then fi # Run the command to start the sandbox container -echo "Running: container run --volume \"$ASSETS_SRC/skills/user:/app/uploads/skills/user\" --volume \"$ASSETS_SRC/outputs:/app/uploads/outputs\" --name coderunner --detach --cpus 8 --memory 4g instavm/coderunner" +echo "Starting coderunner with $CPUS CPUs and $MEMORY memory..." if container run \ --volume "$ASSETS_SRC/skills/user:/app/uploads/skills/user" \ --volume "$ASSETS_SRC/outputs:/app/uploads/outputs" \ + ${VOLUME_ARGS[@]+"${VOLUME_ARGS[@]}"} \ --name coderunner \ --detach \ - --cpus 8 \ - --memory 4g \ + --cpus "$CPUS" \ + --memory "$MEMORY" \ ${NETWORK_ARGS[@]+"${NETWORK_ARGS[@]}"} \ instavm/coderunner; then + printf '%s' "$desired_config" > "$CONFIG_FILE" wait_for_server exit $? else