diff --git a/.github/workflows/offline-package-ai-workspace-installer.yaml b/.github/workflows/offline-package-ai-workspace-installer.yaml new file mode 100644 index 0000000..f733de0 --- /dev/null +++ b/.github/workflows/offline-package-ai-workspace-installer.yaml @@ -0,0 +1,196 @@ +name: Build Offline AI Workspace All-in-One Package + +on: + push: + paths: + - 'scripts/create-ai-workspace-offline-package.sh' + - 'scripts/ai-workspace-offline-install.sh' + - '.github/workflows/offline-package-ai-workspace-installer.yaml' + workflow_dispatch: + inputs: + tag: + description: "Release tag. Leave empty to use offline-ai-workspace-" + required: false + type: string + playbooks_ref: + description: "ai-workspace-infra/playbooks git ref" + required: false + default: "main" + type: string + console_ref: + description: "ai-workspace-lab/xworkspace-console git ref" + required: false + default: "main" + type: string + core_skills_ref: + description: "ai-workspace-lab/xworkspace-core-skills git ref" + required: false + default: "main" + type: string + +permissions: + contents: write + +concurrency: + group: build-offline-ai-workspace-all-in-one + cancel-in-progress: false + +jobs: + build-offline-package: + name: Build ${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - distro: debian + version: "13" + arch: amd64 + - distro: debian + version: "13" + arch: arm64 + - distro: debian + version: "12" + arch: amd64 + - distro: debian + version: "12" + arch: arm64 + - distro: debian + version: "11" + arch: amd64 + - distro: debian + version: "11" + arch: arm64 + - distro: ubuntu + version: "26.04" + arch: amd64 + - distro: ubuntu + version: "26.04" + arch: arm64 + - distro: ubuntu + version: "24.04" + arch: amd64 + - distro: ubuntu + version: "24.04" + arch: arm64 + - distro: ubuntu + version: "22.04" + arch: amd64 + - distro: ubuntu + version: "22.04" + arch: arm64 + steps: + - uses: actions/checkout@v4 + + - name: Enable QEMU for cross-arch package collection + uses: docker/setup-qemu-action@v3 + + - name: Install build dependencies + run: | + set -euo pipefail + sudo apt-get update -y + sudo apt-get install -y curl git jq python3-pip + + - name: Build offline package + env: + DISTRO_ID: ${{ matrix.distro }} + DISTRO_VERSION: ${{ matrix.version }} + ARCH: ${{ matrix.arch }} + PLAYBOOKS_REF: ${{ github.event.inputs.playbooks_ref || 'main' }} + CONSOLE_REF: ${{ github.event.inputs.console_ref || 'main' }} + CORE_SKILLS_REF: ${{ github.event.inputs.core_skills_ref || 'main' }} + PACKAGE_VERSION: ${{ github.run_number }} + run: | + set -euo pipefail + chmod +x scripts/create-ai-workspace-offline-package.sh + scripts/create-ai-workspace-offline-package.sh + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ai-workspace-all-in-one-offline-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }} + path: ai-workspace-all-in-one-offline-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }}.tar.gz + if-no-files-found: error + + test-offline-package: + needs: build-offline-package + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - distro: debian + version: "13" + arch: amd64 + - distro: ubuntu + version: "24.04" + arch: amd64 + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: ai-workspace-all-in-one-offline-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }} + path: offline-test + + - name: Verify offline package contents + run: | + set -euo pipefail + cd offline-test + tar -tzf ai-workspace-all-in-one-offline-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }}.tar.gz > contents.txt + grep -q 'scripts/ai-workspace-offline-install.sh' contents.txt + grep -q 'metadata/manifest.json' contents.txt + grep -q 'repos/playbooks' contents.txt + grep -q 'repos/xworkspace-console' contents.txt + grep -q 'packages/apt/Packages.gz' contents.txt + + publish-release: + needs: test-offline-package + runs-on: ubuntu-latest + env: + TAG_NAME: ${{ github.event.inputs.tag != '' && github.event.inputs.tag || format('offline-ai-workspace-{0}', github.run_number) }} + RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }} + RSYNC_SSH_USER: ${{ secrets.RSYNC_SSH_USER }} + VPS_HOST: ${{ secrets.VPS_HOST }} + REMOTE_ROOT: /data/update-server/offline-package/ai-workspace + steps: + - uses: actions/checkout@v4 + + - name: Create GitHub Release + uses: actions/create-release@v1 + with: + tag_name: ${{ env.TAG_NAME }} + release_name: Build ${{ env.TAG_NAME }} + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Download all package artifacts + uses: actions/download-artifact@v4 + with: + path: release-artifacts + pattern: ai-workspace-all-in-one-offline-* + merge-multiple: true + + - name: Upload packages to GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ env.TAG_NAME }} + files: release-artifacts/*.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Rsync packages to remote mirror + if: ${{ env.RSYNC_SSH_KEY != '' && env.RSYNC_SSH_USER != '' && env.VPS_HOST != '' }} + run: | + set -euo pipefail + sudo apt-get update -y + sudo apt-get install -y rsync openssh-client + mkdir -p ~/.ssh + echo "$RSYNC_SSH_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts + remote_dir="${REMOTE_ROOT}/${TAG_NAME}" + ssh -i ~/.ssh/id_rsa "${RSYNC_SSH_USER}@${VPS_HOST}" "mkdir -p '${remote_dir}'" + rsync -av -e "ssh -i ~/.ssh/id_rsa" release-artifacts/*.tar.gz \ + "${RSYNC_SSH_USER}@${VPS_HOST}:${remote_dir}/" diff --git a/docs/OFFLINE_AI_WORKSPACE_INSTALLER.md b/docs/OFFLINE_AI_WORKSPACE_INSTALLER.md new file mode 100644 index 0000000..1a7b062 --- /dev/null +++ b/docs/OFFLINE_AI_WORKSPACE_INSTALLER.md @@ -0,0 +1,59 @@ +# Offline AI Workspace Installer + +`offline-package-ai-workspace-installer.yaml` builds tarball resource packs for +`setup-ai-workspace-all-in-one.sh`. + +## Supported Targets + +- Debian: 13, 12, 11 +- Ubuntu LTS: 26.04, 24.04, 22.04 +- Architectures: amd64, arm64 + +Ubuntu 20.04 is not in the default matrix because standard support has moved to +Ubuntu Pro/ESM. + +## Package Contents + +- `repos/playbooks` +- `repos/xworkspace-console` +- `repos/xworkspace-core-skills` +- `repos/xworkmate-bridge` +- `repos/qmd` +- `repos/litellm` +- `packages/apt` +- `packages/npm` +- `packages/npm-cache` +- `packages/pip` +- `packages/bin` +- `packages/images` +- `scripts/ai-workspace-offline-install.sh` +- `metadata/manifest.json` +- `metadata/*.commit` + +## Runtime Usage + +Extract the target package on the host and run: + +```bash +sudo ./scripts/ai-workspace-offline-install.sh +``` + +The script configures a local APT repository, installs bundled binaries, loads +packaged container images when Docker is available, and runs the packaged +all-in-one bootstrap with local source directories. + +## Deployment Timing Notes + +A remote `setup-ai-workspace-all-in-one.sh` run on `acp-bridge.onwalk.net` +showed these visible timing hotspots: + +- OpenClaw npm package/plugin install and dependency repair: about 68 seconds +- Codex ACP Go build: about 37 seconds +- ACP vhosts for Codex/OpenCode/Gemini/Hermes: about 95 seconds +- Console runtime apt/package setup and ttyd: about 47 seconds +- Agent skill sync and quality checks: about 48 seconds + +The first several minutes of the sampled log included Node, apt, and AI runtime +setup before the captured timing window. Those are treated as resource-heavy +setup phases and are included in the offline package through APT caches, npm +tarballs, pip wheels, local git sources, binaries, and container image tarballs. diff --git a/scripts/ai-workspace-offline-install.sh b/scripts/ai-workspace-offline-install.sh new file mode 100755 index 0000000..31bf4de --- /dev/null +++ b/scripts/ai-workspace-offline-install.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +APT_DIR="${ROOT}/packages/apt" +BIN_DIR="${ROOT}/packages/bin" +IMAGE_DIR="${ROOT}/packages/images" +NPM_CACHE_DIR="${ROOT}/packages/npm-cache" +PIP_WHEEL_DIR="${ROOT}/packages/pip" + +info() { + printf '\033[1;34m[INFO]\033[0m %s\n' "$*" >&2 +} + +warn() { + printf '\033[1;33m[WARN]\033[0m %s\n' "$*" >&2 +} + +require_root() { + if [ "$(id -u)" -ne 0 ]; then + echo "Run this installer as root or with sudo." >&2 + exit 1 + fi +} + +configure_local_apt_repo() { + if [ ! -d "${APT_DIR}" ] || ! compgen -G "${APT_DIR}/*.deb" >/dev/null; then + warn "No offline .deb cache found at ${APT_DIR}; skipping local APT repo setup." + return + fi + + info "Configuring local APT repository from ${APT_DIR}" + cat > /etc/apt/sources.list.d/ai-workspace-offline.list </dev/null; then + info "Installing bundled Vault binary" + apt-get install -y unzip || true + tmp="$(mktemp -d)" + unzip -o "${BIN_DIR}"/vault_*_linux_*.zip -d "${tmp}" + install -m 0755 "${tmp}/vault" /usr/local/bin/vault + rm -rf "${tmp}" + fi + + case "$(uname -m)" in + x86_64|amd64) ttyd_arch=x86_64 ;; + aarch64|arm64) ttyd_arch=aarch64 ;; + *) ttyd_arch="" ;; + esac + if [ -n "${ttyd_arch}" ] && [ -f "${BIN_DIR}/ttyd.${ttyd_arch}" ]; then + info "Installing bundled ttyd binary" + install -m 0755 "${BIN_DIR}/ttyd.${ttyd_arch}" /usr/local/bin/ttyd + fi +} + +load_container_images() { + if [ ! -d "${IMAGE_DIR}" ] || ! compgen -G "${IMAGE_DIR}/*.tar" >/dev/null; then + return + fi + if command -v docker >/dev/null 2>&1; then + for image_tar in "${IMAGE_DIR}"/*.tar; do + info "Loading container image ${image_tar}" + docker load -i "${image_tar}" || true + done + else + warn "Docker is not installed yet; bundled image tarballs remain in ${IMAGE_DIR}." + fi +} + +configure_language_package_caches() { + if [ -d "${NPM_CACHE_DIR}" ]; then + info "Configuring npm to prefer bundled cache" + npm config set cache "${NPM_CACHE_DIR}" --global || true + npm config set prefer-offline true --global || true + fi + + if [ -d "${PIP_WHEEL_DIR}" ]; then + info "Configuring pip to prefer bundled wheelhouse" + mkdir -p /etc/pip.conf.d + cat > /etc/pip.conf <&2; exit 1 ;; +esac + +case "${DISTRO_ID}:${DISTRO_VERSION}" in + debian:11|debian:12|debian:13|ubuntu:22.04|ubuntu:24.04|ubuntu:26.04) ;; + *) + echo "Unsupported target: ${DISTRO_ID}:${DISTRO_VERSION}" >&2 + echo "Supported: debian 11/12/13 and ubuntu 22.04/24.04/26.04" >&2 + exit 1 + ;; +esac + +WORKDIR="${WORKDIR:-${REPO_ROOT}/ai-workspace-all-in-one-offline-${DISTRO_ID}-${DISTRO_VERSION}-${ARCH}}" +OUT="${OUT:-${REPO_ROOT}/ai-workspace-all-in-one-offline-${DISTRO_ID}-${DISTRO_VERSION}-${ARCH}.tar.gz}" + +image_for_target() { + case "${DISTRO_ID}" in + debian) echo "debian:${DISTRO_VERSION}" ;; + ubuntu) echo "ubuntu:${DISTRO_VERSION}" ;; + esac +} + +safe_name() { + tr '/:@+' '----' <<<"$1" +} + +clone_repo() { + local url=$1 + local ref=$2 + local dest=$3 + + git clone --depth 1 --branch "${ref}" "${url}" "${dest}" 2>/dev/null || { + git clone --depth 1 "${url}" "${dest}" + git -C "${dest}" fetch --depth 1 origin "${ref}" + git -C "${dest}" checkout FETCH_HEAD + } + git -C "${dest}" rev-parse HEAD +} + +download_apt_packages() { + local image=$1 + local platform="linux/${ARCH}" + local apt_dir="${WORKDIR}/packages/apt" + local apt_lists="${WORKDIR}/metadata/apt" + + mkdir -p "${apt_dir}" "${apt_lists}" + + docker run --rm --platform "${platform}" \ + -e DISTRO_ID="${DISTRO_ID}" \ + -e NODEJS_MAJOR_VERSIONS="${NODEJS_MAJOR_VERSIONS}" \ + -e NODEJS_22_VERSION="${NODEJS_22_VERSION}" \ + -e NODEJS_24_VERSION="${NODEJS_24_VERSION}" \ + -v "${apt_dir}:/offline-apt" \ + -v "${apt_lists}:/offline-meta" \ + "${image}" \ + bash -lc "$(cat <<'CONTAINER' +set -euo pipefail +export DEBIAN_FRONTEND=noninteractive +apt-get update -y +apt-get install -y ca-certificates curl gnupg dpkg-dev apt-transport-https + +install -d -m 0755 /etc/apt/keyrings +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /etc/apt/keyrings/nodesource.gpg +for major in ${NODEJS_MAJOR_VERSIONS}; do + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${major}.x nodistro main" \ + > "/etc/apt/sources.list.d/nodesource-node${major}.list" +done +curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \ + | gpg --dearmor -o /etc/apt/keyrings/google-linux-signing-key.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/google-linux-signing-key.gpg] https://dl.google.com/linux/chrome/deb/ stable main" \ + > /etc/apt/sources.list.d/google-chrome.list +curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg -o /etc/apt/keyrings/yarn.asc +echo "deb [signed-by=/etc/apt/keyrings/yarn.asc] https://dl.yarnpkg.com/debian/ stable main" \ + > /etc/apt/sources.list.d/yarn.list +curl -fsSL https://download.docker.com/linux/${DISTRO_ID}/gpg -o /etc/apt/keyrings/docker.asc || true +if [ "${DISTRO_ID}" = "ubuntu" ] || [ "${DISTRO_ID}" = "debian" ]; then + . /etc/os-release + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${DISTRO_ID} ${VERSION_CODENAME} stable" \ + > /etc/apt/sources.list.d/docker.list +fi + +apt-get update -y || true + +packages=( + ansible git curl ca-certificates gnupg jq rsync unzip wget + caddy xfce4 python3 python3-pip python3-venv python3-dev python3-setuptools + build-essential pkg-config python-is-python3 pandoc fonts-noto-cjk + fonts-noto-cjk-extra fonts-wqy-zenhei fonts-wqy-microhei + google-chrome-stable nodejs yarn +) + +if apt-cache show docker-ce >/dev/null 2>&1; then + packages+=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin) +elif apt-cache show docker.io >/dev/null 2>&1; then + packages+=(docker.io docker-compose-plugin) +fi + +if apt-cache show golang-go >/dev/null 2>&1; then + packages+=(golang-go) +fi +if apt-cache show texlive-xetex >/dev/null 2>&1; then + packages+=(texlive-xetex texlive-latex-extra texlive-fonts-recommended texlive-lang-chinese latexmk) +fi + +apt-get install --download-only -y --no-install-recommends "${packages[@]}" || true +apt-get download "nodejs=${NODEJS_22_VERSION}-1nodesource1" || true +apt-get download "nodejs=${NODEJS_24_VERSION}-1nodesource1" || true +cp -n ./*.deb /offline-apt/ 2>/dev/null || true +find /var/cache/apt/archives -name '*.deb' -exec cp -n {} /offline-apt/ \; +cd /offline-apt +dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz +find . -maxdepth 1 -name '*.deb' -printf '%f\n' | sort > /offline-meta/deb-files.txt +CONTAINER +)" \ + -e DISTRO_ID="${DISTRO_ID}" \ + -e NODEJS_MAJOR_VERSIONS="${NODEJS_MAJOR_VERSIONS}" +} + +download_npm_packages() { + local npm_dir="${WORKDIR}/packages/npm" + local npm_cache_dir="${WORKDIR}/packages/npm-cache" + mkdir -p "${npm_dir}" "${npm_cache_dir}" + npm pack --pack-destination "${npm_dir}" "opencode-ai" + npm pack --pack-destination "${npm_dir}" "@google/gemini-cli" + npm pack --pack-destination "${npm_dir}" "@openai/codex" + npm pack --pack-destination "${npm_dir}" "@anthropic-ai/claude-code" + npm pack --pack-destination "${npm_dir}" "openclaw@${OPENCLAW_VERSION}" + npm pack --pack-destination "${npm_dir}" "@openclaw/codex@${OPENCLAW_VERSION}" + npm pack --pack-destination "${npm_dir}" "playwright@${PLAYWRIGHT_VERSION}" + npm cache add --cache "${npm_cache_dir}" "opencode-ai" + npm cache add --cache "${npm_cache_dir}" "@google/gemini-cli" + npm cache add --cache "${npm_cache_dir}" "@openai/codex" + npm cache add --cache "${npm_cache_dir}" "@anthropic-ai/claude-code" + npm cache add --cache "${npm_cache_dir}" "openclaw@${OPENCLAW_VERSION}" + npm cache add --cache "${npm_cache_dir}" "@openclaw/codex@${OPENCLAW_VERSION}" + npm cache add --cache "${npm_cache_dir}" "playwright@${PLAYWRIGHT_VERSION}" +} + +download_pip_wheels() { + local wheel_dir="${WORKDIR}/packages/pip" + mkdir -p "${wheel_dir}" + python3 -m pip download --dest "${wheel_dir}" \ + "litellm[proxy] @ git+${LITELLM_REPO}@${LITELLM_REF}" \ + prisma psycopg2-binary || true +} + +download_binaries() { + local bin_dir="${WORKDIR}/packages/bin" + local vault_arch="${ARCH}" + local ttyd_arch + mkdir -p "${bin_dir}" + + case "${ARCH}" in + amd64) ttyd_arch=x86_64 ;; + arm64) ttyd_arch=aarch64 ;; + esac + + curl -fsSL \ + "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_${vault_arch}.zip" \ + -o "${bin_dir}/vault_${VAULT_VERSION}_linux_${vault_arch}.zip" + + if [ "${TTYD_VERSION}" = "latest" ]; then + curl -fsSL "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${ttyd_arch}" \ + -o "${bin_dir}/ttyd.${ttyd_arch}" + else + curl -fsSL "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${ttyd_arch}" \ + -o "${bin_dir}/ttyd.${ttyd_arch}" + fi + chmod +x "${bin_dir}/ttyd.${ttyd_arch}" +} + +export_container_images() { + local images_dir="${WORKDIR}/packages/images" + mkdir -p "${images_dir}" + docker pull --platform "linux/${ARCH}" "${POSTGRES_IMAGE}" + docker save "${POSTGRES_IMAGE}" -o "${images_dir}/$(safe_name "${POSTGRES_IMAGE}").tar" +} + +write_manifest() { + local manifest="${WORKDIR}/metadata/manifest.json" + mkdir -p "$(dirname "${manifest}")" + cat > "${manifest}" < "${WORKDIR}/metadata/playbooks.commit" + clone_repo "${CONSOLE_REPO}" "${CONSOLE_REF}" "${WORKDIR}/repos/xworkspace-console" > "${WORKDIR}/metadata/xworkspace-console.commit" + clone_repo "${CORE_SKILLS_REPO}" "${CORE_SKILLS_REF}" "${WORKDIR}/repos/xworkspace-core-skills" > "${WORKDIR}/metadata/xworkspace-core-skills.commit" + clone_repo "${XWORKMATE_BRIDGE_REPO}" "${XWORKMATE_BRIDGE_REF}" "${WORKDIR}/repos/xworkmate-bridge" > "${WORKDIR}/metadata/xworkmate-bridge.commit" + clone_repo "${QMD_REPO}" "${QMD_REF}" "${WORKDIR}/repos/qmd" > "${WORKDIR}/metadata/qmd.commit" + clone_repo "${LITELLM_REPO}" "${LITELLM_REF}" "${WORKDIR}/repos/litellm" > "${WORKDIR}/metadata/litellm.commit" + + download_apt_packages "${image}" + download_npm_packages + download_pip_wheels + download_binaries + export_container_images + + cp "${SCRIPT_DIR}/ai-workspace-offline-install.sh" "${WORKDIR}/scripts/" + chmod +x "${WORKDIR}/scripts/ai-workspace-offline-install.sh" + write_manifest + + cat > "${WORKDIR}/README.md" <<'README' +# AI Workspace All-in-One Offline Package + +Extract this archive on the target host, then run: + +```bash +sudo ./scripts/ai-workspace-offline-install.sh +``` + +The installer configures a local APT repository from `packages/apt`, loads bundled +container images when Docker is available, and runs the packaged +`xworkspace-console/scripts/setup-ai-workspace-all-in-one.sh` with local source +directories for playbooks, console, core skills, and bridge code. + +Set the same environment variables supported by the online installer before +running the script, for example `TOKEN`, `XWORKMATE_BRIDGE_DOMAIN`, or +`AI_WORKSPACE_SECURITY_LEVEL`. +README + + tar -czf "${OUT}" -C "$(dirname "${WORKDIR}")" "$(basename "${WORKDIR}")" + ls -lh "${OUT}" +} + +main "$@" diff --git a/scripts/setup-ai-workspace-all-in-one.sh b/scripts/setup-ai-workspace-all-in-one.sh index b1a8b72..a4c6485 100755 --- a/scripts/setup-ai-workspace-all-in-one.sh +++ b/scripts/setup-ai-workspace-all-in-one.sh @@ -23,6 +23,7 @@ set -euo pipefail # Unified auth token passed to xworkmate-bridge, LiteLLM, OpenClaw, and Vault. # PLAYBOOK_DIR (optional local playbooks checkout; useful for macOS validation) # XWORKSPACE_CONSOLE_DIR (optional local xworkspace-console checkout for macOS) +# QMD_SOURCE_REPO / LITELLM_SOURCE_REPO (optional local git sources for offline installs) # AI_WORKSPACE_DARWIN_MODE=local (default on macOS) | ansible # ============================================================================== @@ -995,6 +996,10 @@ append_var "VAULT_DEPLOY_MODE" "vault_deploy_mode" append_var "XWORKSPACE_CONSOLE_ENABLE_XRDP" "xworkspace_console_enable_xrdp" append_var "AI_WORKSPACE_RUNTIME_MODES" "ai_workspace_runtime_modes" append_var "POSTGRESQL_DEPLOY_MODE" "postgresql_deploy_mode" +append_var "QMD_SOURCE_REPO" "qmd_source_repo" +append_var "QMD_VERSION" "qmd_version" +append_var "LITELLM_SOURCE_REPO" "litellm_source_repo" +append_var "LITELLM_VERSION" "litellm_version" # 4. Resolve one auth token for the bridge and downstream service UIs/APIs. UNIFIED_AUTH_TOKEN="$(resolve_unified_auth_token)"