Skip to main content

ENTRYPOINT_SH

Constant ENTRYPOINT_SH 

Source
pub const ENTRYPOINT_SH: &[u8] = b"#!/bin/bash\nset -euo pipefail\n\nlog() {\n    echo \"[opencode-cloud] $*\"\n}\n\nread_opencode_cloud_version() {\n    local version_file=\"/etc/opencode-cloud-version\"\n    local version\n\n    if [ -r \"${version_file}\" ]; then\n        version=\"$(head -n 1 \"${version_file}\" 2>/dev/null | tr -d \"\\r\\n\")\"\n    else\n        version=\"\"\n    fi\n\n    if [ -z \"${version}\" ]; then\n        printf \"dev\"\n    else\n        printf \"%s\" \"${version}\"\n    fi\n}\n\nformat_url_host() {\n    local host=\"$1\"\n\n    if [[ \"${host}\" == *:* ]] && [[ \"${host}\" != \\[*] ]]; then\n        printf \"[%s]\" \"${host}\"\n    else\n        printf \"%s\" \"${host}\"\n    fi\n}\n\ndisplay_local_host() {\n    local host=\"$1\"\n\n    if [ \"${host}\" = \"0.0.0.0\" ] || [ \"${host}\" = \"::\" ]; then\n        printf \"localhost\"\n    else\n        if [ \"${host}\" = \"127.0.0.1\" ] || [ \"${host}\" = \"::1\" ]; then\n            printf \"localhost\"\n            return\n        fi\n        printf \"%s\" \"${host}\"\n    fi\n}\n\nbuild_service_url() {\n    local host=\"$1\"\n    local port=\"$2\"\n    printf \"http://%s:%s\" \"$(format_url_host \"${host}\")\" \"${port}\"\n}\n\nprint_welcome_banner() {\n    local version local_host local_url bind_url\n    version=\"$(read_opencode_cloud_version)\"\n    local_host=\"$(display_local_host \"${OPENCODE_HOST}\")\"\n    local_url=\"$(build_service_url \"${local_host}\" \"${OPENCODE_PORT}\")\"\n    bind_url=\"$(build_service_url \"${OPENCODE_HOST}\" \"${OPENCODE_PORT}\")\"\n\n    log \"----------------------------------------------------------------------\"\n    log \"Welcome to opencode-cloud-sandbox\"\n    log \"You are running opencode-cloud v${version}\"\n    log \"WARNING: opencode-cloud is still a work in progress and is rapidly evolving.\"\n    log \"Expect frequent updates and breaking changes. Use with caution.\"\n    log \"For questions, problems, and feature requests, file an issue:\"\n    log \"  https://github.com/pRizz/opencode-cloud/issues\"\n    log \"opencode-cloud runs opencode in a Docker sandbox; use occ/opencode-cloud CLI to manage users, mounts, and updates.\"\n    log \"\"\n    log \"Getting started:\"\n    log \"  1) Access the web UI:\"\n    log \"     Local URL: ${local_url}\"\n    log \"     Bind URL:  ${bind_url}\"\n    log \"  2) First-time setup:\"\n    log \"     If no users are configured, this container prints an Initial One-Time Password (IOTP)\"\n    log \"     in the logs below. Enter it on the login page, then enroll a passkey\"\n    log \"     for the default \'opencoder\' account.\"\n    log \"     The IOTP is deleted immediately after successful passkey enrollment.\"\n    log \"  3) Optional admin CLI path:\"\n    log \"     occ user add <username>\"\n    log \"     occ user add <username> --generate\"\n    log \"     occ user passwd <username>\"\n    log \"  4) Cloud note: external URL may differ based on DNS, reverse proxy/load balancer,\"\n    log \"     ingress, TLS termination, and port mappings.\"\n    log \"  5) Sign in with a passkey (recommended) or username/password fallback.\"\n    log \"     2FA setup and management are available from the upper-right session menu.\"\n    log \"Docs: https://github.com/pRizz/opencode-cloud#readme\"\n    log \"Deploy guides: https://github.com/pRizz/opencode-cloud/tree/main/docs/deploy\"\n    log \"----------------------------------------------------------------------\"\n}\n\nOPENCODE_PORT=\"${OPENCODE_PORT:-${PORT:-3000}}\"\nOPENCODE_HOST=\"${OPENCODE_HOST:-0.0.0.0}\"\nexport OPENCODE_PORT OPENCODE_HOST\n\nBOOTSTRAP_HELPER=\"/usr/local/bin/opencode-cloud-bootstrap\"\nBOOTSTRAP_STATE_DIR=\"/var/lib/opencode-users\"\nPROTECTED_USER=\"opencoder\"\nBUILTIN_USERS_FILE=\"/etc/opencode-cloud/builtin-home-users.txt\"\nFALLBACK_BUILTIN_HOME_USERS=(\"opencoder\" \"ubuntu\")\nBUILTIN_HOME_USERS=()\n# NOTE: Do not change this prefix; admins may depend on it for log grep extraction.\ngreppable_iotp_prefix=\"INITIAL ONE-TIME PASSWORD (IOTP): \"\n\nprint_welcome_banner\n\ndetect_droplet() {\n    local hint=\"${OPENCODE_CLOUD_ENV:-}\"\n    if [ -n \"${hint}\" ]; then\n        hint=\"$(printf \"%s\" \"${hint}\" | tr \"[:upper:]\" \"[:lower:]\")\"\n        if [[ \"${hint}\" == *digitalocean* || \"${hint}\" == *droplet* ]]; then\n            return 0\n        fi\n    fi\n    curl -fsS --connect-timeout 1 --max-time 1 http://169.254.169.254/metadata/v1/id >/dev/null 2>&1\n}\n\ndetect_railway() {\n    [ -n \"${RAILWAY_ENVIRONMENT:-}\" ] && return 0\n    [ -n \"${RAILWAY_SERVICE_NAME:-}\" ] && return 0\n    return 1\n}\n\ncollect_non_persistent_paths() {\n    local -a paths=(\n        \"/home/opencoder/workspace\"\n        \"/home/opencoder/.local/share/opencode\"\n        \"/home/opencoder/.local/state/opencode\"\n        \"/home/opencoder/.cache/opencode\"\n        \"/home/opencoder/.config/opencode\"\n        \"/var/lib/opencode-users\"\n    )\n    local -a non_persistent=()\n    local fs_type\n    for path in \"${paths[@]}\"; do\n        fs_type=\"$(stat -f -c %T \"${path}\" 2>/dev/null || true)\"\n        case \"${fs_type}\" in\n            \"\"|overlay|overlayfs|tmpfs|ramfs|squashfs)\n                non_persistent+=(\"${path}\")\n                ;;\n        esac\n    done\n    if [ ${#non_persistent[@]} -eq 0 ]; then\n        return 1\n    fi\n    printf \"%s\\n\" \"${non_persistent[@]}\"\n    return 0\n}\n\nnon_persistent_paths=\"$(collect_non_persistent_paths || true)\"\nif [ -n \"${non_persistent_paths}\" ]; then\n    log \"=================================================================\"\n    log \"WARNING: Persistence is not configured for one or more paths.\"\n    log \"Data loss is likely if the container is recreated or updated.\"\n    log \"Non-persistent paths:\"\n    while IFS= read -r path; do\n        log \"  - ${path}\"\n    done <<< \"${non_persistent_paths}\"\n    if detect_droplet; then\n        log \"Detected DigitalOcean Docker Droplet environment.\"\n        log \"By default, Docker Droplets do not configure volumes or persistence.\"\n        log \"You will almost certainly lose data if you are not careful.\"\n    elif detect_railway; then\n        log \"Detected Railway environment.\"\n        log \"Railway does not automatically configure persistent storage for Docker containers.\"\n        log \"Attach a Railway Volume to persist data across deploys.\"\n        log \"Mount it to /home/opencoder/.local/share/opencode for session and project data.\"\n        log \"Railway deploy guide: https://github.com/pRizz/opencode-cloud/blob/main/docs/deploy/railway.md\"\n    fi\n    log \"Configure persistence: https://github.com/pRizz/opencode-cloud/tree/main/docs/deploy\"\n    log \"For docker run, add volume flags such as:\"\n    log \"  -v opencode-data:/home/opencoder/.local/share/opencode\"\n    log \"  -v opencode-workspace:/home/opencoder/workspace\"\n    log \"  -v opencode-users:/var/lib/opencode-users\"\n    log \"  -v opencode-config:/home/opencoder/.config/opencode\"\n    log \"  -v opencode-state:/home/opencoder/.local/state/opencode\"\n    log \"  -v opencode-cache:/home/opencoder/.cache/opencode\"\n    log \"=================================================================\"\nfi\n\nif [ \"${USE_SYSTEMD:-}\" = \"1\" ]; then\n    exec /sbin/init\nelse\n    # Ensure broker socket directory exists\n    install -d -m 0755 /run/opencode\n\n    # Ensure user records directory exists (ephemeral unless mounted)\n    install -d -m 0700 /var/lib/opencode-users\n\n    load_builtin_home_users() {\n        BUILTIN_HOME_USERS=(\"${FALLBACK_BUILTIN_HOME_USERS[@]}\")\n\n        if [ ! -r \"${BUILTIN_USERS_FILE}\" ]; then\n            return 0\n        fi\n\n        local username\n        while IFS= read -r username; do\n            username=\"$(printf \"%s\" \"${username}\" | tr -d \'\\r\\n\')\"\n            if [ -n \"${username}\" ]; then\n                BUILTIN_HOME_USERS+=(\"${username}\")\n            fi\n        done < \"${BUILTIN_USERS_FILE}\"\n    }\n\n    is_builtin_home_user() {\n        local username=\"$1\"\n        local builtin\n        for builtin in \"${BUILTIN_HOME_USERS[@]}\"; do\n            if [ \"${username}\" = \"${builtin}\" ]; then\n                return 0\n            fi\n        done\n        return 1\n    }\n\n    user_record_path() {\n        local username=\"$1\"\n        printf \"/var/lib/opencode-users/%s.json\" \"${username}\"\n    }\n\n    user_record_exists() {\n        local username=\"$1\"\n        [ -f \"$(user_record_path \"${username}\")\" ]\n    }\n\n    ensure_auth_config() {\n        local config_dir=\"/home/opencoder/.config/opencode\"\n        local config_json=\"${config_dir}/opencode.json\"\n        local config_jsonc=\"${config_dir}/opencode.jsonc\"\n\n        install -d -m 0755 \"${config_dir}\"\n\n        if [ -f \"${config_json}\" ] || [ -f \"${config_jsonc}\" ]; then\n            return\n        fi\n\n        if ! cat > \"${config_jsonc}\" <<\'EOF\'\n{\n  \"auth\": {\n    \"enabled\": true\n  }\n}\nEOF\n        then\n            log \"WARNING: Failed to create ${config_jsonc}; auth may be disabled.\"\n            return\n        fi\n\n        chown opencoder:opencoder \"${config_jsonc}\" 2>/dev/null || true\n        chmod 644 \"${config_jsonc}\" 2>/dev/null || true\n        log \"Created default auth config at ${config_jsonc}.\"\n    }\n\n    check_auth_enabled() {\n        local config_dir=\"/home/opencoder/.config/opencode\"\n        local config_file=\"\"\n\n        for candidate in \"${config_dir}/opencode.json\" \"${config_dir}/opencode.jsonc\"; do\n            if [ -f \"${candidate}\" ]; then\n                config_file=\"${candidate}\"\n                break\n            fi\n        done\n\n        if [ -z \"${config_file}\" ]; then\n            return 1\n        fi\n\n        local auth_enabled\n        # Strip // line-comments before parsing (JSONC compat)\n        auth_enabled=\"$(grep -v \'^\\s*//\' \"${config_file}\" | jq -r \'.auth.enabled // false\' 2>/dev/null || echo \"false\")\"\n        [ \"${auth_enabled}\" = \"true\" ]\n    }\n\n    warn_security_posture() {\n        if ! check_auth_enabled; then\n            log \"=================================================================\"\n            log \"SECURITY WARNING: Authentication is not enabled.\"\n            log \"Anyone who can reach this container can use opencode without signing in.\"\n            log \"Enable auth by creating or editing the opencode config:\"\n            log \"  /home/opencoder/.config/opencode/opencode.jsonc\"\n            log \'  { \"auth\": { \"enabled\": true } }\'\n            log \"Docs: https://github.com/pRizz/opencode-cloud/tree/main/docs/deploy\"\n            log \"=================================================================\"\n        fi\n\n        # Advisory HTTPS notice for cloud deployments binding to all interfaces\n        if [ \"${OPENCODE_HOST}\" = \"0.0.0.0\" ] || [ \"${OPENCODE_HOST}\" = \"::\" ]; then\n            if detect_droplet; then\n                log \"=================================================================\"\n                log \"SECURITY NOTICE: opencode is binding to all network interfaces.\"\n                log \"If this container is exposed to the internet without HTTPS,\"\n                log \"credentials and session data will be transmitted in the clear.\"\n                log \"\"\n                log \"Recommended: terminate TLS in front of opencode.\"\n                log \"  - Reverse proxy (Caddy, Nginx, Traefik)\"\n                log \"  - Cloud load balancer with TLS\"\n                log \"Caddy setup: https://github.com/pRizz/opencode-cloud/blob/main/docs/deploy/digitalocean-droplet.md#optional-https-caddy\"\n                log \"=================================================================\"\n            fi\n        fi\n    }\n\n    restore_users() {\n        shopt -s nullglob\n        local records=(/var/lib/opencode-users/*.json)\n        if [ ${#records[@]} -eq 0 ]; then\n            return 1\n        fi\n        for record in \"${records[@]}\"; do\n            local username password_hash locked\n            username=\"$(jq -r \".username // empty\" \"${record}\")\"\n            password_hash=\"$(jq -r \".password_hash // empty\" \"${record}\")\"\n            locked=\"$(jq -r \".locked // false\" \"${record}\")\"\n            if [ -z \"${username}\" ]; then\n                log \"Skipping invalid user record: ${record}\"\n                continue\n            fi\n            if is_builtin_home_user \"${username}\"; then\n                log \"Skipping built-in user record: ${record}\"\n                continue\n            fi\n            if ! id -u \"${username}\" >/dev/null 2>&1; then\n                log \"Creating user: ${username}\"\n                useradd -m -s /bin/bash \"${username}\"\n            fi\n            if [ -n \"${password_hash}\" ]; then\n                usermod -p \"${password_hash}\" \"${username}\"\n            fi\n            if [ \"${locked}\" = \"true\" ]; then\n                passwd -l \"${username}\" >/dev/null\n            else\n                passwd -u \"${username}\" >/dev/null || true\n            fi\n            log \"Restored user: ${username}\"\n        done\n        return 0\n    }\n\n    persist_user_record() {\n        local username=\"$1\"\n        if is_builtin_home_user \"${username}\"; then\n            log \"Skipping persistence for built-in user: ${username}\"\n            return 0\n        fi\n        local shadow_hash\n        shadow_hash=\"$(getent shadow \"${username}\" | cut -d: -f2)\"\n        if [ -z \"${shadow_hash}\" ]; then\n            log \"Failed to read shadow hash for ${username}\"\n            return 1\n        fi\n        local status locked\n        status=\"$(passwd -S \"${username}\" | tr -s \" \" | cut -d\" \" -f2)\"\n        locked=\"false\"\n        if [ \"${status}\" = \"L\" ]; then\n            locked=\"true\"\n        fi\n        local record_path\n        record_path=\"$(user_record_path \"${username}\")\"\n        umask 077\n        jq -n --arg username \"${username}\" --arg hash \"${shadow_hash}\" --argjson locked \"${locked}\" \'{username:$username,password_hash:$hash,locked:$locked}\' > \"${record_path}\"\n        chmod 600 \"${record_path}\"\n        log \"Persisted user record: ${username}\"\n    }\n\n    bootstrap_user() {\n        local username=\"${OPENCODE_BOOTSTRAP_USER:-}\"\n        local password=\"${OPENCODE_BOOTSTRAP_PASSWORD:-}\"\n        local password_hash=\"${OPENCODE_BOOTSTRAP_PASSWORD_HASH:-}\"\n        if [ -z \"${username}\" ]; then\n            return 1\n        fi\n        if [ -z \"${password_hash}\" ] && [ -z \"${password}\" ]; then\n            log \"OPENCODE_BOOTSTRAP_USER is set but no password or hash provided\"\n            exit 1\n        fi\n        if ! id -u \"${username}\" >/dev/null 2>&1; then\n            log \"Creating bootstrap user: ${username}\"\n            useradd -m -s /bin/bash \"${username}\"\n        fi\n        if [ -n \"${password_hash}\" ]; then\n            usermod -p \"${password_hash}\" \"${username}\"\n        else\n            echo \"${username}:${password}\" | chpasswd\n        fi\n        persist_user_record \"${username}\"\n        log \"Bootstrap user ready: ${username}\"\n        return 0\n    }\n\n    has_non_protected_persisted_users() {\n        # Persisted records are the source of truth for managed login users.\n        # Built-in image users (e.g., ubuntu/opencoder) must not disable IOTP.\n        shopt -s nullglob\n        local records=(/var/lib/opencode-users/*.json)\n        local record username\n        for record in \"${records[@]}\"; do\n            username=\"$(jq -r \".username // empty\" \"${record}\" 2>/dev/null || true)\"\n            if [ -z \"${username}\" ]; then\n                continue\n            fi\n            if is_builtin_home_user \"${username}\"; then\n                continue\n            fi\n            if [ \"${username}\" != \"${PROTECTED_USER}\" ]; then\n                return 0\n            fi\n        done\n\n        return 1\n    }\n\n    migrate_unmanaged_home_users_to_records() {\n        # Migrate any non-built-in Linux users before bootstrap checks so real\n        # manually-created accounts disable IOTP in a consistent, managed way.\n        local line username home\n        while IFS= read -r line; do\n            username=\"$(cut -d: -f1 <<< \"${line}\")\"\n            home=\"$(cut -d: -f6 <<< \"${line}\")\"\n            if [[ \"${home}\" != /home/* ]]; then\n                continue\n            fi\n            if is_builtin_home_user \"${username}\"; then\n                continue\n            fi\n            if user_record_exists \"${username}\"; then\n                continue\n            fi\n            if persist_user_record \"${username}\"; then\n                log \"Migrated unmanaged user to managed records: ${username}\"\n            else\n                log \"WARNING: Failed to migrate unmanaged user to records: ${username}\"\n            fi\n        done < <(getent passwd)\n    }\n\n    clear_bootstrap_state() {\n        rm -f \\\n            \"${BOOTSTRAP_STATE_DIR}/.initial-otp.json\" \\\n            \"${BOOTSTRAP_STATE_DIR}/.initial-otp.secret\" \\\n            \"${BOOTSTRAP_STATE_DIR}/.initial-otp.lock\"\n    }\n\n    announce_bootstrap_mode() {\n        local bootstrap_json=\"$1\"\n        local active otp created_at reason\n        active=\"$(jq -r \".active // false\" <<< \"${bootstrap_json}\" 2>/dev/null || true)\"\n        otp=\"$(jq -r \".otp // empty\" <<< \"${bootstrap_json}\" 2>/dev/null || true)\"\n        created_at=\"$(jq -r \".created_at // empty\" <<< \"${bootstrap_json}\" 2>/dev/null || true)\"\n        reason=\"$(jq -r \".reason // empty\" <<< \"${bootstrap_json}\" 2>/dev/null || true)\"\n\n        if [ \"${active}\" = \"true\" ] && [ -n \"${otp}\" ]; then\n            log \"----------------------------------------------------------------------\"\n            log \"${greppable_iotp_prefix}${otp}\"\n            if [ -n \"${created_at}\" ]; then\n                log \"Issued at (UTC): ${created_at}\"\n            fi\n            log \"Use this IOTP on the web login page to complete first-time setup.\"\n            log \"Find it in Docker logs and keep it private.\"\n            log \"This IOTP is deleted after successful passkey enrollment.\"\n            log \"----------------------------------------------------------------------\"\n            return\n        fi\n\n        if [ \"${reason}\" = \"user_exists\" ]; then\n            log \"Bootstrap mode disabled: one or more configured users already exist.\"\n            return\n        fi\n\n        if [ \"${reason}\" = \"completed\" ]; then\n            log \"Bootstrap mode disabled: initial passkey setup for \'${PROTECTED_USER}\' is already complete.\"\n            return\n        fi\n\n        log \"Bootstrap mode unavailable: ${reason:-unknown reason}\"\n    }\n\n    restore_or_bootstrap_users() {\n        if restore_users; then\n            log \"User records restored\"\n            return\n        fi\n\n        if bootstrap_user; then\n            return\n        fi\n\n        log \"No persisted users and no bootstrap user configured\"\n    }\n\n    maybe_initialize_bootstrap_mode() {\n        local bootstrap_init_json\n\n        if [ ! -x \"${BOOTSTRAP_HELPER}\" ]; then\n            log \"Bootstrap helper is missing; first-time setup is unavailable.\"\n            return\n        fi\n\n        bootstrap_init_json=\"$(\"${BOOTSTRAP_HELPER}\" init 2>/dev/null || true)\"\n        if [ -z \"${bootstrap_init_json}\" ]; then\n            log \"Bootstrap helper returned no output; first-time setup may be unavailable.\"\n            return\n        fi\n\n        announce_bootstrap_mode \"${bootstrap_init_json}\"\n    }\n\n    sync_bootstrap_state() {\n        if has_non_protected_persisted_users; then\n            clear_bootstrap_state\n            return\n        fi\n\n        if [ -n \"${OPENCODE_BOOTSTRAP_USER:-}\" ]; then\n            return\n        fi\n\n        maybe_initialize_bootstrap_mode\n    }\n\n    load_builtin_home_users\n    ensure_auth_config\n    restore_or_bootstrap_users\n    migrate_unmanaged_home_users_to_records\n    sync_bootstrap_state\n    warn_security_posture\n\n    log \"Starting opencode on ${OPENCODE_HOST}:${OPENCODE_PORT}\"\n    /usr/local/bin/opencode-broker &\n    # Use runuser to switch to the container runtime user without password prompt\n    exec runuser -u opencoder -- sh -lc \"cd /home/opencoder/workspace && /opt/opencode/bin/opencode web --port ${OPENCODE_PORT} --hostname ${OPENCODE_HOST}\"\nfi\n";