|
| 1 | +#!/bin/sh |
| 2 | +set -eu |
| 3 | +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
| 4 | +export PATH |
| 5 | + |
| 6 | +fail() { printf 'codeboost isolation probe: %s\n' "$1" >&2; exit 78; } |
| 7 | +mount_options() { findmnt --noheadings --output OPTIONS --target "$1" 2>/dev/null || fail "missing mount: $1"; } |
| 8 | +has_option() { printf '%s\n' "$1" | tr ',' '\n' | grep -Fxq "$2"; } |
| 9 | +require_option() { has_option "$(mount_options "$1")" "$2" || fail "$1 must be mounted $2"; } |
| 10 | +filesystem_bytes() { df -B1 --output=size "$1" | tail -n 1 | tr -d ' '; } |
| 11 | +filesystem_inodes() { df --output=itotal "$1" | tail -n 1 | tr -d ' '; } |
| 12 | +require_ceiling() { |
| 13 | + # tmpfs rounds size= up to a whole page, so compare against the page-rounded limit. |
| 14 | + page=$(getconf PAGESIZE) |
| 15 | + [ "$(filesystem_bytes "$1")" -le "$(( ($2 + page - 1) / page * page ))" ] || fail "$1 exceeds its byte limit" |
| 16 | + [ "$(filesystem_inodes "$1")" -le "$3" ] || fail "$1 exceeds its inode limit" |
| 17 | +} |
| 18 | + |
| 19 | +[ "$(id -u)" -ne 0 ] || fail 'agent process must not run as root' |
| 20 | +for field in CapInh CapPrm CapEff CapBnd CapAmb; do |
| 21 | + [ "$(awk -v name="$field:" '$1 == name { print $2 }' /proc/self/status)" = '0000000000000000' ] \ |
| 22 | + || fail 'all capability sets must be empty' |
| 23 | +done |
| 24 | +[ "$(awk '/^NoNewPrivs:/ { print $2 }' /proc/self/status)" = '1' ] || fail 'no-new-privileges must be enabled' |
| 25 | +[ "$(awk '/^Seccomp:/ { print $2 }' /proc/self/status)" = '2' ] || fail 'a seccomp syscall filter must be enforced' |
| 26 | +require_option / ro |
| 27 | + |
| 28 | +[ "${HOME:-}" = '/home/codeboost' ] || fail 'HOME must be the isolated home directory' |
| 29 | +[ "${CODEBOOST_PHASE:-}" != '' ] || fail 'phase is required' |
| 30 | +[ "${CODEBOOST_VENDOR:-}" = 'codex' ] || [ "${CODEBOOST_VENDOR:-}" = 'claude' ] || fail 'vendor is required' |
| 31 | + |
| 32 | +[ "$(findmnt --noheadings --output FSTYPE --target /work)" = 'tmpfs' ] || fail '/work must use a bounded tmpfs task filesystem' |
| 33 | +[ "$(findmnt --noheadings --output FSTYPE --target /work/.git)" = 'tmpfs' ] || fail 'Git metadata must use a separate tmpfs filesystem' |
| 34 | +[ "$(stat -c %d /work)" != "$(stat -c %d /work/.git)" ] || fail 'Git metadata must not alias the work filesystem' |
| 35 | +require_ceiling /work "${CODEBOOST_WORK_BYTES:-0}" "${CODEBOOST_WORK_INODES:-0}" |
| 36 | +require_ceiling /work/.git "${CODEBOOST_METADATA_BYTES:-0}" "${CODEBOOST_METADATA_INODES:-0}" |
| 37 | +require_option /work/.git ro |
| 38 | +require_option /run/codeboost-input ro |
| 39 | +for path in /work /work/.git; do |
| 40 | + require_option "$path" nosuid |
| 41 | + require_option "$path" nodev |
| 42 | +done |
| 43 | + |
| 44 | +case "$CODEBOOST_PHASE" in |
| 45 | + planning|questions|review) require_option /work ro ;; |
| 46 | + execute|fix) require_option /work rw ;; |
| 47 | + *) fail 'unsupported phase' ;; |
| 48 | +esac |
| 49 | + |
| 50 | +for path in /tmp /home/codeboost; do |
| 51 | + [ "$(findmnt --noheadings --output FSTYPE --target "$path")" = 'tmpfs' ] || fail "$path must use tmpfs" |
| 52 | + require_option "$path" rw |
| 53 | + require_option "$path" nosuid |
| 54 | + require_option "$path" nodev |
| 55 | +done |
| 56 | +require_ceiling /tmp 33554432 4096 |
| 57 | +require_ceiling /home/codeboost 1048576 128 |
| 58 | + |
| 59 | +[ -z "$(find /home/codeboost -mindepth 1 -maxdepth 1 -print -quit)" ] || fail 'HOME must begin empty' |
| 60 | +[ -z "$(find /tmp -mindepth 1 -maxdepth 1 -print -quit)" ] || fail '/tmp must begin empty' |
| 61 | +[ ! -e /var/run/docker.sock ] || fail 'Docker socket must not be mounted' |
| 62 | + |
| 63 | +case "$CODEBOOST_VENDOR" in |
| 64 | + codex) |
| 65 | + [ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] || fail 'Claude credential must not accompany Codex' |
| 66 | + [ "${CODEX_HOME:-}" = '/run/codeboost-auth/codex' ] || fail 'CODEX_HOME must be isolated' |
| 67 | + [ -f "$CODEX_HOME/auth.json" ] || fail 'Codex auth file is missing' |
| 68 | + require_option "$CODEX_HOME" rw |
| 69 | + require_option "$CODEX_HOME" nosuid |
| 70 | + require_option "$CODEX_HOME" nodev |
| 71 | + require_option "$CODEX_HOME/auth.json" ro |
| 72 | + require_ceiling "$CODEX_HOME" 4194304 256 |
| 73 | + ;; |
| 74 | + claude) |
| 75 | + [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] || fail 'Claude credential is missing' |
| 76 | + [ -z "${CODEX_HOME:-}" ] || fail 'Codex credential must not accompany Claude' |
| 77 | + ;; |
| 78 | +esac |
| 79 | + |
| 80 | +[ "$(git --version)" != '' ] || fail 'Git is unavailable' |
| 81 | +[ "$(codex --version)" = 'codex-cli 0.153.4' ] || fail 'unexpected Codex version' |
| 82 | +[ "$(claude --version | awk '{print $1}')" = '2.1.281' ] || fail 'unexpected Claude version' |
| 83 | + |
| 84 | +exec "$@" |
0 commit comments