glove/scripts/apple-container-session.sh
0xkite 497e703e70
feat(runtime): manage sandboxed agent sessions
Keep credentials and network access behind authenticated, audited brokers while binding launch adapters, immutable closures, recovery identities, receipts, and cleanup to the approved plan. Add Apple Container and Linux release lanes without weakening the outer sandbox.
2026-07-30 08:49:42 -04:00

244 lines
7.7 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
umask 077
session_id=""
image=""
image_digest=""
workspace=""
receipt=""
cpus=2
memory="1G"
pids=256
wall_seconds=120
terminal_bytes=16777216
usage() {
cat <<'EOF'
usage: scripts/apple-container-session.sh [options] -- <command> [args...]
options:
--session-id <id> bounded receipt/container identity
--image <reference> reviewed local image reference
--image-digest <digest> exact reviewed OCI index digest (sha256:<64 hex>)
--workspace <path> optional writable workspace mounted at /workspace
--receipt <file> new owner-only JSON receipt
--cpus <count> Apple Container CPU limit (default: 2)
--memory <size> Apple Container memory limit (default: 1G)
--pids <count> guest nproc limit (default: 256)
--wall-seconds <count> host watchdog (default: 120)
--terminal-bytes <n> aggregate stdout/stderr limit (default: 16777216)
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--session-id) session_id="${2:-}"; shift 2 ;;
--image) image="${2:-}"; shift 2 ;;
--image-digest) image_digest="${2:-}"; shift 2 ;;
--workspace) workspace="${2:-}"; shift 2 ;;
--receipt) receipt="${2:-}"; shift 2 ;;
--cpus) cpus="${2:-}"; shift 2 ;;
--memory) memory="${2:-}"; shift 2 ;;
--pids) pids="${2:-}"; shift 2 ;;
--wall-seconds) wall_seconds="${2:-}"; shift 2 ;;
--terminal-bytes) terminal_bytes="${2:-}"; shift 2 ;;
--) shift; break ;;
-h|--help) usage; exit 0 ;;
*) printf 'unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;;
esac
done
[[ "$(uname -s)" == "Darwin" && "$(uname -m)" == "arm64" ]] || {
printf '%s\n' "Apple Container sessions require Apple Silicon macOS" >&2
exit 1
}
command -v container >/dev/null 2>&1 || {
printf '%s\n' "Apple container CLI is unavailable" >&2
exit 1
}
[[ "$session_id" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$ ]] || {
printf '%s\n' "invalid session identity" >&2
exit 2
}
[[ -n "$image" && ${#image} -le 256 && "$image" != *[[:space:]]* ]] || {
printf '%s\n' "invalid image reference" >&2
exit 2
}
[[ "$image_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || {
printf '%s\n' "an exact reviewed OCI image digest is required" >&2
exit 2
}
[[ "$receipt" == /* && ! -e "$receipt" ]] || {
printf '%s\n' "receipt must be a new absolute path" >&2
exit 2
}
[[ "$cpus" =~ ^[1-9][0-9]*([.][0-9]+)?$ ]] || {
printf '%s\n' "invalid CPU limit" >&2
exit 2
}
[[ "$memory" =~ ^[1-9][0-9]*([KMGTPE]i?[Bb]?|[kmgptpe])?$ ]] || {
printf '%s\n' "invalid memory limit" >&2
exit 2
}
for numeric in "$pids" "$wall_seconds" "$terminal_bytes"; do
[[ "$numeric" =~ ^[1-9][0-9]*$ ]] || {
printf '%s\n' "limits must be positive integers" >&2
exit 2
}
done
(( pids <= 4096 && wall_seconds <= 3600 && terminal_bytes <= 1073741824 )) || {
printf '%s\n' "session limit exceeds the built-in safety ceiling" >&2
exit 2
}
(( $# > 0 )) || {
printf '%s\n' "a container command is required" >&2
exit 2
}
if [[ -n "$workspace" ]]; then
[[ "$workspace" == /* && "$workspace" != *","* && -d "$workspace" ]] || {
printf '%s\n' "workspace must be an existing absolute directory without commas" >&2
exit 2
}
workspace="$(cd -- "$workspace" && pwd -P)"
fi
image_inspect="$(container image inspect "$image")" || {
printf '%s\n' "reviewed image is unavailable" >&2
exit 1
}
resolved_image_digest="$(
printf '%s\n' "$image_inspect" |
sed -n 's/^[[:space:]]*"digest"[[:space:]]*:[[:space:]]*"\(sha256:[0-9a-f]\{64\}\)",*$/\1/p' |
head -n 1
)"
unset image_inspect
[[ "$resolved_image_digest" == "$image_digest" ]] || {
printf '%s\n' "reviewed image digest does not match the local image" >&2
exit 1
}
scratch="$(mktemp -d "${TMPDIR:-/tmp}/glove-apple-session.XXXXXX")"
container_name="glove-$session_id"
runner_pid=""
limiter_pid=""
cleanup() {
if [[ -n "$runner_pid" ]] && kill -0 "$runner_pid" 2>/dev/null; then
container kill "$container_name" >/dev/null 2>&1 || true
wait "$runner_pid" 2>/dev/null || true
fi
if [[ -n "$limiter_pid" ]] && kill -0 "$limiter_pid" 2>/dev/null; then
kill "$limiter_pid" >/dev/null 2>&1 || true
wait "$limiter_pid" 2>/dev/null || true
fi
container delete --force "$container_name" >/dev/null 2>&1 || true
find "$scratch" -mindepth 1 -delete
rmdir "$scratch"
}
trap cleanup EXIT INT TERM
started_ms=$(( $(date +%s) * 1000 ))
mkfifo "$scratch/terminal.pipe"
touch "$scratch/terminal"
head -c "$terminal_bytes" <"$scratch/terminal.pipe" >"$scratch/terminal" &
limiter_pid=$!
container_args=(
create
--name "$container_name"
--init
--network none
--no-dns
--read-only
--cap-drop ALL
--cpus "$cpus"
--memory "$memory"
--ulimit "nproc=$pids:$pids"
--tmpfs /tmp
)
if [[ -n "$workspace" ]]; then
container_args+=(--mount "type=bind,source=$workspace,target=/workspace")
fi
container_args+=("$image" "$@")
container "${container_args[@]}" >/dev/null
container_inspect="$(container inspect "$container_name")"
image_identity="$(
printf '%s\n' "$container_inspect" |
sed -n 's/^[[:space:]]*"digest"[[:space:]]*:[[:space:]]*"sha256:\([0-9a-f]\{64\}\)",*$/\1/p' |
head -n 1
)"
unset container_inspect
[[ "$image_identity" =~ ^[0-9a-f]{64}$ ]] || {
printf '%s\n' "created container has no bounded OCI image digest" >&2
exit 1
}
set +e
container start --attach "$container_name" >"$scratch/terminal.pipe" 2>&1 &
runner_pid=$!
set -e
reason="exit"
deadline=$((SECONDS + wall_seconds))
while kill -0 "$runner_pid" 2>/dev/null; do
output_size=$(wc -c <"$scratch/terminal")
if ! kill -0 "$limiter_pid" 2>/dev/null && (( output_size >= terminal_bytes )); then
reason="terminal_output_limit"
container kill "$container_name" >/dev/null 2>&1 || true
break
fi
if (( SECONDS >= deadline )); then
reason="wall_time_limit"
container kill "$container_name" >/dev/null 2>&1 || true
break
fi
sleep 0.1
done
set +e
wait "$runner_pid"
exit_code=$?
wait "$limiter_pid" 2>/dev/null
limiter_pid=""
set -e
runner_pid=""
finished_ms=$(( $(date +%s) * 1000 ))
output_size=$(wc -c <"$scratch/terminal")
if (( output_size >= terminal_bytes )) && [[ "$reason" == "exit" ]]; then
reason="terminal_output_limit"
fi
container delete --force "$container_name" >/dev/null 2>&1 || true
cleanup_verified=true
if container inspect "$container_name" >/dev/null 2>&1; then
cleanup_verified=false
fi
if [[ "$cleanup_verified" != true && "$reason" == "exit" ]]; then
reason="cleanup_failed"
fi
temporary_receipt="$receipt.tmp.$$"
{
printf '{\n'
printf ' "schema_version": 1,\n'
printf ' "backend": "apple_container",\n'
printf ' "session_id": "%s",\n' "$session_id"
printf ' "image_oci_index_digest": "%s",\n' "$image_digest"
printf ' "image_oci_digest": "sha256:%s",\n' "$image_identity"
printf ' "network_mode": "none",\n'
printf ' "root_read_only": true,\n'
printf ' "limits": {"cpus": "%s", "memory": "%s", "pids": %s, "wall_seconds": %s, "terminal_bytes": %s},\n' \
"$cpus" "$memory" "$pids" "$wall_seconds" "$terminal_bytes"
printf ' "started_at_ms": %s,\n' "$started_ms"
printf ' "finished_at_ms": %s,\n' "$finished_ms"
printf ' "exit_code": %s,\n' "$exit_code"
printf ' "terminal_bytes_observed": %s,\n' "$output_size"
printf ' "termination_reason": "%s",\n' "$reason"
printf ' "cleanup_verified": %s\n' "$cleanup_verified"
printf '}\n'
} >"$temporary_receipt"
chmod 0600 "$temporary_receipt"
mv "$temporary_receipt" "$receipt"
[[ "$reason" == "exit" && "$cleanup_verified" == true ]] || exit 1
exit "$exit_code"