Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
147 changes: 144 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
Copilot marked this conversation as resolved.
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}'
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading