#!/bin/sh # ========================================================================== # claude-code セッションブート(使い捨てコンテナ内で1回だけ実行) # # 1. git 認証(SSH 鍵 / HTTPS トークン)を $HOME にセットアップ # 2. 許可済みリポジトリを $WORKSPACE_DIR へ clone # 3. `claude` を PATH に用意(無ければ非 root で $HOME にインストール) # 4. ttyd でブラウザにシェル(+ 任意で claude 自動起動)を提供 # 5. アイドルウォッチドッグ:無通信が続いたら自身を終了 → Pod 破棄 # # 前提: 非 root(uid=10001) / readOnlyRootFilesystem。 # 書き込み可能なのは $HOME, $WORKSPACE_DIR, /tmp のみ。 # git / ttyd / node は事前ビルドイメージに同梱されている想定 # (files/Dockerfile.session)。未同梱時は下記フォールバックが走る。 # ========================================================================== set -eu log() { printf '\033[36m[boot]\033[0m %s\n' "$*"; } err() { printf '\033[31m[boot:ERROR]\033[0m %s\n' "$*" >&2; } : "${HOME:=/home/claude}" : "${WORKSPACE_DIR:=/workspace}" : "${TTYD_PORT:=7681}" : "${IDLE_TIMEOUT_SECONDS:=1800}" : "${AUTOSTART_CLAUDE:=true}" : "${REPO_REF:=main}" export HOME export NPM_CONFIG_CACHE="$HOME/.npm" export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$PATH" mkdir -p "$HOME/.ssh" "$WORKSPACE_DIR" "$HOME/.npm-global" chmod 700 "$HOME/.ssh" # -------------------------------------------------------------------------- # 1. git 認証セットアップ # -------------------------------------------------------------------------- git config --global user.name "${GIT_AUTHOR_NAME:-Claude}" git config --global user.email "${GIT_AUTHOR_EMAIL:-claude@cafepieters.com}" git config --global safe.directory '*' git config --global advice.detachedHead false git config --global credential.helper '' ASKPASS_DISABLE() { export GIT_TERMINAL_PROMPT=0; } ASKPASS_DISABLE case "${GIT_IDENTITY_TYPE:-}" in ssh) if [ ! -r /git-identity/id ]; then err "SSH 鍵がマウントされていません"; exit 1; fi install -m 600 /git-identity/id "$HOME/.ssh/id_session" # known_hosts を必須にし、ホスト鍵の TOFU を無効化する if [ -r /git-identity/known_hosts ]; then install -m 644 /git-identity/known_hosts "$HOME/.ssh/known_hosts" else err "known_hosts がありません(StrictHostKeyChecking を維持するため必須)"; exit 1 fi cat > "$HOME/.ssh/config" < "$HOME/.git-credentials" </dev/null || git checkout -b "$REPO_REF" 2>/dev/null || true log "作業ディレクトリ: $REPO_DIR ($(git rev-parse --short HEAD 2>/dev/null || echo '?'))" # -------------------------------------------------------------------------- # 3. claude CLI を用意 # -------------------------------------------------------------------------- if ! command -v claude >/dev/null 2>&1; then log "claude が未同梱のため $HOME にインストールします(初回は時間がかかります)" npm config set prefix "$HOME/.npm-global" >/dev/null 2>&1 || true _ver="${CLAUDE_CODE_VERSION:-}" _pkg="@anthropic-ai/claude-code${_ver:+@$_ver}" if ! npm install -g --no-fund --no-audit "$_pkg"; then err "claude-code のインストールに失敗しました。事前ビルドイメージの利用を推奨します。" exit 1 fi fi log "claude: $(claude --version 2>/dev/null || echo 'unknown')" # -------------------------------------------------------------------------- # 3b. Claude 認証情報(credentials モード) # -------------------------------------------------------------------------- # Secret からマウントした .credentials.json を書き込み可能な $HOME/.claude へコピーする。 # claude はアクセストークン失効時に refreshToken でこのファイルを自己更新する # (セッション中のみ有効。クラスタ全体の更新は refresher CronJob が担当)。 if [ -n "${SEED_CREDENTIALS_FILE:-}" ] && [ -r "$SEED_CREDENTIALS_FILE" ]; then mkdir -p "$HOME/.claude" chmod 700 "$HOME/.claude" install -m 600 "$SEED_CREDENTIALS_FILE" "$HOME/.claude/.credentials.json" if command -v jq >/dev/null 2>&1; then _exp="$(jq -r '.claudeAiOauth.expiresAt // empty' "$HOME/.claude/.credentials.json" 2>/dev/null)" [ -n "$_exp" ] && log "Claude 認証情報を配置(accessToken 期限: $(date -u -d "@$(( _exp / 1000 ))" 2>/dev/null || echo "$_exp"))" else log "Claude 認証情報を $HOME/.claude/.credentials.json に配置しました" fi unset ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN fi # 追加 apk(root 権限がある事前ビルドイメージのみ有効。非 root では警告のみ) if [ -n "${EXTRA_APK_PACKAGES:-}" ]; then if [ "$(id -u)" = "0" ]; then apk add --no-cache $EXTRA_APK_PACKAGES || err "apk add に一部失敗" else err "EXTRA_APK_PACKAGES は非 root では無視されます: $EXTRA_APK_PACKAGES" fi fi # -------------------------------------------------------------------------- # 4. 補助コマンド(claude-verify / claude-push) # -------------------------------------------------------------------------- mkdir -p "$HOME/.local/bin" cat > "$HOME/.local/bin/claude-verify" <<'EOF' #!/bin/sh # セッション設定の検証コマンドを実行する if [ -z "${VERIFY_COMMAND:-}" ]; then echo "verifyCommand は未設定です"; exit 0; fi echo "+ $VERIFY_COMMAND" exec sh -c "$VERIFY_COMMAND" EOF chmod +x "$HOME/.local/bin/claude-verify" cat > "$HOME/.local/bin/claude-push" <<'EOF' #!/bin/sh # 検証 → 安全なブランチへ push(デフォルト/保護ブランチ・--force は禁止) set -eu if [ "${AUTOPUSH_ENABLED:-false}" != "true" ]; then echo "autoPush は無効です(values: session.autoPush.enabled)。手動 push は 'git push' を使ってください。" >&2 exit 1 fi cur="$(git rev-parse --abbrev-ref HEAD)" prefix="${AUTOPUSH_BRANCH_PREFIX:-claude/}" protected="${AUTOPUSH_PROTECTED:-main,master,develop,release}" case ",$protected," in *",$cur,"*) echo "現在のブランチ '$cur' は保護対象です。作業ブランチへ切り替えてください。" >&2; exit 1;; esac case "$cur" in "$prefix"*) target="$cur" ;; *) target="${prefix}${cur}" ;; esac if [ -n "${VERIFY_COMMAND:-}" ]; then echo "== verify: $VERIFY_COMMAND" sh -c "$VERIFY_COMMAND" || { echo "検証に失敗したため push を中止します。" >&2; exit 1; } fi if [ -n "$(git status --porcelain)" ]; then git add -A git commit -m "${1:-chore: work from claude-code session $SESSION_ID}" fi [ "$target" = "$cur" ] || git branch -f "$target" "$cur" echo "== push origin $target(--force なし)" exec git push origin "HEAD:$target" EOF chmod +x "$HOME/.local/bin/claude-push" # -------------------------------------------------------------------------- # 5. アイドルウォッチドッグ # -------------------------------------------------------------------------- idle_watchdog() { # ttyd への ESTABLISHED 接続が IDLE_TIMEOUT_SECONDS の間ゼロなら Pod を終了させる last_seen="$(date +%s)" while true; do sleep 30 conns=0 if command -v ss >/dev/null 2>&1; then conns="$(ss -Htn "sport = :$TTYD_PORT" state established 2>/dev/null | wc -l | tr -d ' ')" elif [ -r /proc/net/tcp ]; then hexport="$(printf '%04X' "$TTYD_PORT")" conns="$(awk -v p=":$hexport" '$2 ~ p"$" && $4=="01"{n++} END{print n+0}' /proc/net/tcp)" fi now="$(date +%s)" if [ "${conns:-0}" -gt 0 ]; then last_seen="$now" elif [ $(( now - last_seen )) -ge "$IDLE_TIMEOUT_SECONDS" ]; then err "アイドル ${IDLE_TIMEOUT_SECONDS}s を超過。セッションを終了します。" kill -TERM "$TTYD_PID" 2>/dev/null || true exit 0 fi done } # -------------------------------------------------------------------------- # 6. ttyd 起動 # -------------------------------------------------------------------------- if ! command -v ttyd >/dev/null 2>&1; then err "ttyd が見つかりません。事前ビルドイメージ(files/Dockerfile.session)を利用してください。" exit 1 fi SHELL_CMD="/bin/bash" command -v bash >/dev/null 2>&1 || SHELL_CMD="/bin/sh" cat > "$HOME/.session-shell" </dev/null 2>&1; then exec claude fi exec $SHELL_CMD -l EOF chmod +x "$HOME/.session-shell" log "ttyd を 0.0.0.0:$TTYD_PORT で起動します" ttyd --port "$TTYD_PORT" --interface 0.0.0.0 --writable \ --client-option fontSize=14 --client-option 'theme={"background":"#1e1e1e"}' \ --terminal-type xterm-256color \ "$HOME/.session-shell" & TTYD_PID=$! idle_watchdog & wait "$TTYD_PID"