#!/bin/sh set -eu log() { printf '%s\n' "[hf-code-server-init] $*" } DATA_ROOT="${DATA_ROOT:-/data}" CODER_HOME="${CODER_HOME:-/home/coder}" BOOTSTRAP_ROOT="${BOOTSTRAP_ROOT:-/opt/bootstrap}" CODE_SERVER_BIN="${CODE_SERVER_BIN:-/usr/bin/code-server}" CLAUDE_SYSTEM_BIN="${CLAUDE_SYSTEM_BIN:-/usr/local/bin/claude-system}" CLAUDE_VSCODE_WRAPPER="${CLAUDE_VSCODE_WRAPPER:-/usr/local/bin/claude-vscode-wrapper}" SPACES_SYNC_BIN="${SPACES_SYNC_BIN:-/usr/local/bin/sync-workspace-spaces}" WORKSPACE_DATA="${DATA_ROOT}/workspace" WORKSPACE_LINK="${WORKSPACE:-${CODER_HOME}/workspace}" CODE_SERVER_ROOT="${DATA_ROOT}/code-server" USER_DATA_DIR="${CODE_SERVER_USER_DATA:-${CODE_SERVER_ROOT}/user-data}" EXTENSIONS_DIR="${CODE_SERVER_EXTENSIONS:-${CODE_SERVER_ROOT}/extensions}" CLAUDE_DIR="${DATA_ROOT}/claude" NGROK_DIR="${DATA_ROOT}/ngrok" UV_CACHE_DIR_RUNTIME="${DATA_ROOT}/cache/uv" HF_CACHE_DIR="${DATA_ROOT}/cache/huggingface" uid="$(id -u)" gid="$(id -g)" ensure_dir() { directory="$1" if ! mkdir -p "${directory}" 2>/dev/null; then sudo mkdir -p "${directory}" fi if [ ! -w "${directory}" ]; then sudo chown "${uid}:${gid}" "${directory}" fi if [ ! -w "${directory}" ]; then log "ERROR: ${directory} is not writable by uid ${uid}" exit 1 fi } for directory in \ "${WORKSPACE_DATA}" \ "${CODE_SERVER_ROOT}" \ "${USER_DATA_DIR}" \ "${EXTENSIONS_DIR}" \ "${CLAUDE_DIR}" \ "${NGROK_DIR}" \ "${UV_CACHE_DIR_RUNTIME}" \ "${HF_CACHE_DIR}" do ensure_dir "${directory}" done # DigitalOcean App Platform has no persistent filesystem volumes. When Spaces # synchronization is configured, restore a snapshot only into a fresh runtime # workspace. Existing mounted or local workspace data always takes precedence. if [ -n "${SPACES_BUCKET-}" ]; then "${SPACES_SYNC_BIN}" validate if [ -z "$(find "${WORKSPACE_DATA}" -mindepth 1 -maxdepth 1 -print -quit)" ]; then "${SPACES_SYNC_BIN}" restore fi fi # Make the requested editor-visible path point to the candidate's persistent # workspace. The directory is created before code-server is launched. if [ -L "${WORKSPACE_LINK}" ]; then current_target="$(readlink "${WORKSPACE_LINK}" || true)" if [ "${current_target}" != "${WORKSPACE_DATA}" ]; then rm -f "${WORKSPACE_LINK}" fi elif [ -e "${WORKSPACE_LINK}" ]; then if [ -d "${WORKSPACE_LINK}" ]; then cp -a -n "${WORKSPACE_LINK}/." "${WORKSPACE_DATA}/" 2>/dev/null || true fi rm -rf "${WORKSPACE_LINK}" fi if [ ! -L "${WORKSPACE_LINK}" ]; then ln -s "${WORKSPACE_DATA}" "${WORKSPACE_LINK}" fi # Seed the candidate instructions and uv project metadata on first startup. # Existing candidate edits are never overwritten on later restarts or rebuilds. for project_file in README.md pyproject.toml uv.lock; do source_file="${BOOTSTRAP_ROOT}/workspace-src/${project_file}" target_file="${WORKSPACE_DATA}/${project_file}" if [ ! -e "${target_file}" ]; then cp "${source_file}" "${target_file}" fi done # Restore the prebuilt environment if it is absent, is not Python 3.12, or was # built from a different dependency manifest. expected_venv_id="$(cat "${BOOTSTRAP_ROOT}/venv.id")" actual_venv_id="$(cat "${WORKSPACE_DATA}/.venv/.hf-bootstrap-id" 2>/dev/null || true)" if ! "${WORKSPACE_DATA}/.venv/bin/python" -c \ 'import sys; raise SystemExit(sys.version_info[:2] != (3, 12))' \ >/dev/null 2>&1 \ || [ "${actual_venv_id}" != "${expected_venv_id}" ] then log "Restoring Python 3.12 virtual environment" rm -rf "${WORKSPACE_DATA}/.venv" # Some managed volume implementations allow normal file creation but reject # restoring archived mtimes. The virtual environment does not depend on those # timestamps, so keep extraction portable across managed container platforms. tar --touch -C "${WORKSPACE_DATA}" -xzf "${BOOTSTRAP_ROOT}/venv.tar.gz" fi # Each candidate Space owns one repository. Initialize it once, leave existing # history untouched on later restarts, and keep environment artifacts ignored # without adding a visible top-level .gitignore file. new_repository=0 if [ ! -d "${WORKSPACE_DATA}/.git" ]; then log "Initializing Git repository" if ! git -C "${WORKSPACE_DATA}" init -b main >/dev/null 2>&1; then git -C "${WORKSPACE_DATA}" init >/dev/null git -C "${WORKSPACE_DATA}" symbolic-ref HEAD refs/heads/main fi new_repository=1 fi if ! git -C "${WORKSPACE_DATA}" config --get user.name >/dev/null 2>&1; then git -C "${WORKSPACE_DATA}" config user.name "${GIT_USER_NAME:-Candidate}" fi if ! git -C "${WORKSPACE_DATA}" config --get user.email >/dev/null 2>&1; then git -C "${WORKSPACE_DATA}" config user.email "${GIT_USER_EMAIL:-candidate@localhost}" fi exclude_file="${WORKSPACE_DATA}/.git/info/exclude" touch "${exclude_file}" if [ -f "${BOOTSTRAP_ROOT}/workspace.gitignore" ]; then while IFS= read -r pattern || [ -n "${pattern}" ]; do case "${pattern}" in ''|'#'*) continue ;; esac if ! grep -Fqx -- "${pattern}" "${exclude_file}"; then printf '%s\n' "${pattern}" >> "${exclude_file}" fi done < "${BOOTSTRAP_ROOT}/workspace.gitignore" fi # Commit the supplied instructions and uv project definition as the clean # baseline for a fresh repository. Existing history is never rewritten. if [ "${new_repository}" -eq 1 ] \ || ! git -C "${WORKSPACE_DATA}" rev-parse --verify HEAD >/dev/null 2>&1 then git -C "${WORKSPACE_DATA}" add README.md pyproject.toml uv.lock if ! git -C "${WORKSPACE_DATA}" diff --cached --quiet; then git -C "${WORKSPACE_DATA}" -c commit.gpgsign=false commit \ -m "Initialize Python environment" >/dev/null fi fi # Restore preinstalled extensions and the installer metadata they need. if [ -d "${BOOTSTRAP_ROOT}/code-server-user-data" ] \ && [ -z "$(find "${USER_DATA_DIR}" -mindepth 1 -maxdepth 1 -print -quit)" ] then cp -a "${BOOTSTRAP_ROOT}/code-server-user-data/." "${USER_DATA_DIR}/" fi cp -a -n "${BOOTSTRAP_ROOT}/extensions/." "${EXTENSIONS_DIR}/" list_extensions() { "${CODE_SERVER_BIN}" \ --user-data-dir "${USER_DATA_DIR}" \ --extensions-dir "${EXTENSIONS_DIR}" \ --list-extensions 2>/dev/null || true } has_extension() { extension_id="$1" printf '%s\n' "${installed_extensions}" \ | grep -Fxi "${extension_id}" >/dev/null 2>&1 } installed_extensions="$(list_extensions)" if ! has_extension 'Anthropic.claude-code' || ! has_extension 'ms-python.python'; then log "Repairing required extensions" "${CODE_SERVER_BIN}" \ --user-data-dir "${USER_DATA_DIR}" \ --extensions-dir "${EXTENSIONS_DIR}" \ --force \ --install-extension Anthropic.claude-code \ --install-extension ms-python.python installed_extensions="$(list_extensions)" fi if ! has_extension 'Anthropic.claude-code'; then log "ERROR: Anthropic.claude-code is unavailable after initialization" exit 1 fi if ! has_extension 'ms-python.python'; then log "ERROR: ms-python.python is unavailable after initialization" exit 1 fi patch_claude_extension_binaries() { test -x "${CLAUDE_SYSTEM_BIN}" for extension_dir in \ "${EXTENSIONS_DIR}"/anthropic.claude-code-* \ "${EXTENSIONS_DIR}"/Anthropic.claude-code-* do [ -d "${extension_dir}" ] || continue for binary_name in claude claude.real; do bundled_binary="${extension_dir}/resources/native-binary/${binary_name}" if [ ! -e "${bundled_binary}" ] && [ ! -L "${bundled_binary}" ]; then continue fi if "${bundled_binary}" --version >/dev/null 2>&1; then continue fi log "Replacing incompatible Claude extension binary" rm -f "${bundled_binary}" ln -s "${CLAUDE_SYSTEM_BIN}" "${bundled_binary}" "${bundled_binary}" --version >/dev/null done done } patch_claude_extension_binaries # Keep editor configuration outside the candidate project. The Python # environment and Git metadata stay out of Explorer and search; the visible # baseline consists of README.md, pyproject.toml, and uv.lock. Workspace Trust # is unnecessary in this dedicated interview container, where the candidate # already has terminal access, and disabling it lets the Claude extension load # immediately instead of starting in Restricted Mode. settings_file="${USER_DATA_DIR}/User/settings.json" settings_dir="$(dirname "${settings_file}")" mkdir -p "${settings_dir}" if [ ! -f "${settings_file}" ]; then printf '{}\n' > "${settings_file}" fi if jq -e 'type == "object"' "${settings_file}" >/dev/null 2>&1; then settings_tmp="${settings_file}.tmp.$$" jq \ --arg wrapper "${CLAUDE_VSCODE_WRAPPER}" \ --arg python "${WORKSPACE_LINK}/.venv/bin/python" \ --arg workspace "${WORKSPACE_LINK}" \ '. + { "claudeCode.claudeProcessWrapper": $wrapper, "python.defaultInterpreterPath": $python, "python.terminal.activateEnvironment": true, "security.workspace.trust.enabled": false, "terminal.integrated.cwd": $workspace, "workbench.startupEditor": "readme", "workbench.browser.enableRemoteProxy": true, "workbench.browser.dataStorage": "workspace", "workbench.browser.openLocalhostLinks": true, "workbench.browser.showInTitleBar": true, "files.exclude": ((."files.exclude" // {}) + { "**/.venv": true, "**/.git": true }), "search.exclude": ((."search.exclude" // {}) + { "**/.venv": true, "**/.git": true }) }' \ "${settings_file}" > "${settings_tmp}" mv "${settings_tmp}" "${settings_file}" else log "WARNING: ${settings_file} is not plain JSON; leaving it unchanged" fi # Verify the Claude wrapper both with and without the extension-injected path. test -x "${CLAUDE_VSCODE_WRAPPER}" "${CLAUDE_VSCODE_WRAPPER}" --version >/dev/null "${CLAUDE_VSCODE_WRAPPER}" \ "${EXTENSIONS_DIR}/test/resources/native-binary/claude" \ --version >/dev/null # The system-level Claude Code policy pins all primary, fallback, quick-task, # and subagent roles to OpenRouter's Claude Opus 4.8 model. The API token stays # in the runtime environment and is never written to this file. CLAUDE_MANAGED_SETTINGS="/etc/claude-code/managed-settings.json" test -f "${CLAUDE_MANAGED_SETTINGS}" jq -e ' .model == "anthropic/claude-opus-4.8" and .availableModels == ["anthropic/claude-opus-4.8"] and .env.ANTHROPIC_BASE_URL == "https://openrouter.ai/api" and .env.ANTHROPIC_API_KEY == "" and .env.ANTHROPIC_MODEL == "anthropic/claude-opus-4.8" and .env.ANTHROPIC_DEFAULT_OPUS_MODEL == "anthropic/claude-opus-4.8" and .env.ANTHROPIC_DEFAULT_SONNET_MODEL == "anthropic/claude-opus-4.8" and .env.ANTHROPIC_DEFAULT_HAIKU_MODEL == "anthropic/claude-opus-4.8" and .env.CLAUDE_CODE_SUBAGENT_MODEL == "anthropic/claude-opus-4.8" ' "${CLAUDE_MANAGED_SETTINGS}" >/dev/null # Persist ngrok's default Linux configuration directory. mkdir -p "${CODER_HOME}/.config" if [ -L "${CODER_HOME}/.config/ngrok" ]; then current_target="$(readlink "${CODER_HOME}/.config/ngrok" || true)" if [ "${current_target}" != "${NGROK_DIR}" ]; then rm -f "${CODER_HOME}/.config/ngrok" fi elif [ -e "${CODER_HOME}/.config/ngrok" ]; then if [ -d "${CODER_HOME}/.config/ngrok" ]; then cp -a -n "${CODER_HOME}/.config/ngrok/." "${NGROK_DIR}/" 2>/dev/null || true fi rm -rf "${CODER_HOME}/.config/ngrok" fi if [ ! -L "${CODER_HOME}/.config/ngrok" ]; then ln -s "${NGROK_DIR}" "${CODER_HOME}/.config/ngrok" fi test -d "${WORKSPACE_LINK}" test -f "${WORKSPACE_LINK}/README.md" test -f "${WORKSPACE_LINK}/pyproject.toml" test -f "${WORKSPACE_LINK}/uv.lock" test -x "${WORKSPACE_LINK}/.venv/bin/python" git -C "${WORKSPACE_DATA}" rev-parse --is-inside-work-tree >/dev/null log "Workspace ready: ${WORKSPACE_LINK}" log "Candidate instructions ready: ${WORKSPACE_LINK}/README.md" log "uv project ready: ${WORKSPACE_LINK}/pyproject.toml" log "Python ready: $("${WORKSPACE_LINK}/.venv/bin/python" --version 2>&1)" log "Git ready: $(git -C "${WORKSPACE_DATA}" branch --show-current 2>/dev/null || true)" log "Claude CLI ready: $("${CLAUDE_SYSTEM_BIN}" --version 2>&1)" log "Claude routing ready: OpenRouter / anthropic/claude-opus-4.8" log "Extensions ready: $(printf '%s' "${installed_extensions}" | tr '\n' ' ')"