#!/usr/bin/env bash set -euo pipefail # Start the Den server stack inside a Daytona sandbox. # Services: MySQL, Den API, and Den Web. if [ -n "${OPENWORK_WORKSPACE_DIR:-}" ]; then REPO_DIR="$OPENWORK_WORKSPACE_DIR" elif [ -f /workspace/package.json ]; then REPO_DIR="/workspace" else REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" fi cd "$REPO_DIR" DEN_API_PORT="${DEN_API_PORT:-8788}" DEN_WEB_PORT="${DEN_WEB_PORT:-3005}" PNPM_STORE="${PNPM_STORE:-$REPO_DIR/.openwork-daytona/pnpm-store}" DEN_API_PUBLIC_URL="${DEN_API_PUBLIC_URL:-http://localhost:$DEN_API_PORT}" DEN_WEB_PUBLIC_URL="${DEN_WEB_PUBLIC_URL:-http://localhost:$DEN_WEB_PORT}" DEN_WEB_PUBLIC_HOST="${DEN_WEB_PUBLIC_URL#http://}" DEN_WEB_PUBLIC_HOST="${DEN_WEB_PUBLIC_HOST#https://}" DEN_WEB_PUBLIC_HOST="${DEN_WEB_PUBLIC_HOST%%/*}" export OPENWORK_DEV_MODE="${OPENWORK_DEV_MODE:-1}" export DEN_ORG_MODE="${DEN_ORG_MODE:-multi_org}" # Eval sign-ups must not depend on the HIBP API. export DEN_PASSWORD_BREACH_SCREENING_ENABLED="${DEN_PASSWORD_BREACH_SCREENING_ENABLED:-false}" export DEN_GENERATED_ARTIFACT_VIEWS_ENABLED="${DEN_GENERATED_ARTIFACT_VIEWS_ENABLED:-false}" export DATABASE_URL="${DATABASE_URL:-mysql://root:password@127.0.0.1:3306/openwork_den}" export DEN_DB_ENCRYPTION_KEY="${DEN_DB_ENCRYPTION_KEY:-daytona-den-db-encryption-key-please-change-1234567890}" export BETTER_AUTH_SECRET="${BETTER_AUTH_SECRET:-daytona-den-auth-secret-please-change-1234567890}" export BETTER_AUTH_URL="${BETTER_AUTH_URL:-$DEN_WEB_PUBLIC_URL}" export DEN_BETTER_AUTH_URL="$BETTER_AUTH_URL" export DEN_API_PUBLIC_URL export DEN_MCP_RESOURCE_URL="${DEN_MCP_RESOURCE_URL:-$DEN_API_PUBLIC_URL/mcp}" # DEN_API_BASE is the externally reachable Den API origin (see # packages/docs/start-here/private-network-deployment.mdx): Den Web publishes # it through /api/runtime-config as the desktop's denApiUrl. Desktops run in # other sandboxes and can only reach the public preview URL, never this # sandbox's loopback. export DEN_API_BASE="${DEN_API_BASE:-${DEN_API_PUBLIC_URL:-http://127.0.0.1:$DEN_API_PORT}}" export DEN_AUTH_ORIGIN="${DEN_AUTH_ORIGIN:-$DEN_WEB_PUBLIC_URL}" export DEN_AUTH_FALLBACK_BASE="${DEN_AUTH_FALLBACK_BASE:-http://127.0.0.1:$DEN_API_PORT}" export NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL="${NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL:-$DEN_WEB_PUBLIC_URL}" export DEN_PROVISIONER_MODE="${DEN_PROVISIONER_MODE:-stub}" export DEN_WORKER_URL_TEMPLATE="${DEN_WORKER_URL_TEMPLATE:-https://workers.local/{workerId}}" export DEN_WEB_ALLOWED_DEV_ORIGINS="${DEN_WEB_ALLOWED_DEV_ORIGINS:-$DEN_WEB_PUBLIC_HOST}" DEFAULT_ORIGINS="$DEN_WEB_PUBLIC_URL,$DEN_API_PUBLIC_URL,http://localhost:$DEN_WEB_PORT,http://127.0.0.1:$DEN_WEB_PORT,http://localhost:$DEN_API_PORT,http://127.0.0.1:$DEN_API_PORT" export CORS_ORIGINS="${CORS_ORIGINS:-$DEFAULT_ORIGINS}" # Daytona mints a fresh preview hostname on every preview-url call, so any # origin baked at boot goes stale immediately. Trust the preview proxy domain # by wildcard (better-auth supports wildcard trusted origins) so rotated # preview URLs keep working. Local hosts produce no wildcard. PREVIEW_PROXY_HOST="${DEN_WEB_PUBLIC_URL#http://}" PREVIEW_PROXY_HOST="${PREVIEW_PROXY_HOST#https://}" PREVIEW_PROXY_HOST="${PREVIEW_PROXY_HOST%%/*}" PREVIEW_PROXY_WILDCARD="" case "$PREVIEW_PROXY_HOST" in localhost*|127.*|0.0.0.0*|\[*) ;; *.*.*) PREVIEW_PROXY_WILDCARD="https://*.${PREVIEW_PROXY_HOST#*.}" ;; esac # Preview hosts are not app.*: tell den-api the preview domain serves den-web # so desktop handoff links point at the den-web /api/den proxy. export DEN_WEB_APP_HOSTS="${DEN_WEB_APP_HOSTS:-${PREVIEW_PROXY_WILDCARD:+.${PREVIEW_PROXY_HOST#*.}}}" export DEN_BETTER_AUTH_TRUSTED_ORIGINS="${DEN_BETTER_AUTH_TRUSTED_ORIGINS:-$CORS_ORIGINS${PREVIEW_PROXY_WILDCARD:+,$PREVIEW_PROXY_WILDCARD}}" run_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else echo "ERROR: root privileges are required to start MySQL." >&2 exit 1 fi } MYSQL_ROOT_CMD=(run_root mysql -uroot) wait_for_http() { local url="$1" local label="$2" local max_wait="${3:-180}" local elapsed=0 while [ "$elapsed" -lt "$max_wait" ]; do if curl -sf "$url" >/dev/null 2>&1; then echo "==> $label ready after ${elapsed}s" return 0 fi sleep 5 elapsed=$((elapsed + 5)) done echo "ERROR: $label did not become ready at $url" >&2 return 1 } echo "==> Starting MySQL..." run_root service mysql start >/tmp/openwork-mysql-service.log 2>&1 || run_root service mariadb start >/tmp/openwork-mysql-service.log 2>&1 for _ in $(seq 1 60); do if mysql -uroot -ppassword -e "SELECT 1" >/dev/null 2>&1; then MYSQL_ROOT_CMD=(mysql -uroot -ppassword) break fi if run_root mysql -uroot -e "SELECT 1" >/dev/null 2>&1; then MYSQL_ROOT_CMD=(run_root mysql -uroot) break fi sleep 2 done "${MYSQL_ROOT_CMD[@]}" <<'SQL' CREATE DATABASE IF NOT EXISTS openwork_den; ALTER USER 'root'@'localhost' IDENTIFIED BY 'password'; CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES; SQL echo "==> Installing dependencies if needed..." mkdir -p "$PNPM_STORE" .openwork-daytona baseline=.openwork-daytona/pnpm-lock.sha256 current="$(sha256sum pnpm-lock.yaml | cut -d " " -f 1)" # The server stack never runs Electron or browser automation; skip their # binary downloads on reinstalls too. INSTALL_ENV=(CI=1 ELECTRON_SKIP_BINARY_DOWNLOAD=1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PUPPETEER_SKIP_DOWNLOAD=1 CYPRESS_INSTALL_BINARY=0) if [ ! -d node_modules ] || [ ! -f "$baseline" ] || [ "$(cat "$baseline")" != "$current" ]; then env "${INSTALL_ENV[@]}" pnpm install --store-dir "$PNPM_STORE" --frozen-lockfile \ || env "${INSTALL_ENV[@]}" pnpm install --store-dir "$PNPM_STORE" printf "%s" "$current" > "$baseline" else echo "==> Skipping pnpm install (node_modules present and lockfile unchanged)." fi # Content keys for the sandbox-invariant builds baked into the snapshot. # A build is skipped only when the git trees that feed it and the lockfile # both match what the snapshot (or a previous boot) built. Snapshots without # markers, refs that changed those trees, or refs where a tree is missing # rebuild exactly as before. An empty key never matches and is never stored. build_key() { local trees trees="$(git rev-parse "$@" 2>/dev/null)" || return 0 # Byte-identical to the snapshot bake: one tree hash per line, then the # lockfile sha, all newline-terminated. printf '%s\n%s\n' "$trees" "$current" | sha256sum | cut -d ' ' -f 1 } echo "==> Pushing Den DB schema..." pnpm --filter @openwork-ee/den-db db:push > /tmp/den-db-push.log 2>&1 den_api_assets_marker=.openwork-daytona/den-api-assets.tree den_api_assets_key="$(build_key HEAD:packages/mcp-apps)" if [ -n "$den_api_assets_key" ] && [ -d packages/mcp-apps/dist ] && [ -f "$den_api_assets_marker" ] \ && [ "$(cat "$den_api_assets_marker")" = "$den_api_assets_key" ]; then echo "==> Skipping Den API runtime asset build (baked assets match this ref)." else echo "==> Building Den API runtime assets..." pnpm --filter @openwork-ee/den-api run build:mcp-apps if [ -n "$den_api_assets_key" ]; then printf "%s" "$den_api_assets_key" > "$den_api_assets_marker" else rm -f "$den_api_assets_marker" fi fi echo "==> Starting Den API on :$DEN_API_PORT..." # The den-api process cmdline is "tsx watch src/main.ts" (cwd-relative), so a # pattern anchored on the repo path never matches and restarts silently keep # the old server alive on the port. main.ts is unique to den-api here. pkill -f "tsx watch src/main.ts" >/dev/null 2>&1 || true nohup env \ PORT="$DEN_API_PORT" \ DATABASE_URL="$DATABASE_URL" \ DEN_DB_ENCRYPTION_KEY="$DEN_DB_ENCRYPTION_KEY" \ BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \ BETTER_AUTH_URL="$BETTER_AUTH_URL" \ DEN_API_PUBLIC_URL="$DEN_API_PUBLIC_URL" \ DEN_WEB_APP_HOSTS="$DEN_WEB_APP_HOSTS" \ DEN_MCP_RESOURCE_URL="$DEN_MCP_RESOURCE_URL" \ DEN_BETTER_AUTH_TRUSTED_ORIGINS="$DEN_BETTER_AUTH_TRUSTED_ORIGINS" \ CORS_ORIGINS="$CORS_ORIGINS" \ DEN_BOOTSTRAP_ADMIN_EMAILS="${DEN_BOOTSTRAP_ADMIN_EMAILS:-}" \ PROVISIONER_MODE="$DEN_PROVISIONER_MODE" \ WORKER_URL_TEMPLATE="$DEN_WORKER_URL_TEMPLATE" \ DEN_ORG_MODE="$DEN_ORG_MODE" \ DEN_PASSWORD_BREACH_SCREENING_ENABLED="$DEN_PASSWORD_BREACH_SCREENING_ENABLED" \ DEN_GENERATED_ARTIFACT_VIEWS_ENABLED="$DEN_GENERATED_ARTIFACT_VIEWS_ENABLED" \ OPENWORK_DEV_MODE="$OPENWORK_DEV_MODE" \ NODE_OPTIONS="--conditions=development" \ pnpm --filter @openwork-ee/den-api exec tsx watch src/main.ts > /tmp/den-api.log 2>&1 & wait_for_http "http://127.0.0.1:$DEN_API_PORT/health" "Den API" 180 if [ "${RUN_SEED:-0}" = "1" ]; then demo_email="${DEN_DEMO_OWNER_EMAIL:-alex@acme.test}" demo_password="${DEN_DEMO_OWNER_PASSWORD:-OpenWorkDemo123!}" signin_ok() { curl -sf -o /dev/null -X POST "http://127.0.0.1:$DEN_API_PORT/api/auth/sign-in/email" \ -H 'content-type: application/json' \ -d "{\"email\":\"$demo_email\",\"password\":\"$demo_password\"}" } if signin_ok; then echo "==> Demo org already present ($demo_email)" else echo "==> Seeding demo org..." (cd ee/apps/den-api && env \ DATABASE_URL="$DATABASE_URL" \ DEN_DB_ENCRYPTION_KEY="$DEN_DB_ENCRYPTION_KEY" \ BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \ BETTER_AUTH_URL="$BETTER_AUTH_URL" \ DEN_API_PUBLIC_URL="$DEN_API_PUBLIC_URL" \ DEN_ORG_MODE="$DEN_ORG_MODE" \ OPENWORK_DEV_MODE="$OPENWORK_DEV_MODE" \ DEN_DEMO_SEED_FETCH_GITHUB="${DEN_DEMO_SEED_FETCH_GITHUB:-0}" \ node --conditions=development --import tsx scripts/seed-demo-org.ts) > /tmp/den-seed.log 2>&1 if signin_ok; then echo "==> Demo org seeded ($demo_email)" else echo "ERROR: seed completed but $demo_email cannot sign in. See /tmp/den-seed.log" >&2 tail -40 /tmp/den-seed.log >&2 || true exit 1 fi fi echo "DEMO_OWNER_READY=$demo_email" fi den_web_marker=.openwork-daytona/den-web-build.tree den_web_key="$(build_key HEAD:packages/ui HEAD:ee/packages/utils HEAD:ee/apps/den-web)" if [ -n "$den_web_key" ] && [ -d ee/apps/den-web/.next ] && [ -f "$den_web_marker" ] \ && [ "$(cat "$den_web_marker")" = "$den_web_key" ]; then echo "==> Skipping Den Web build (baked build matches this ref)." else # Production build instead of next dev: dev-mode HMR websockets 502 through # the preview proxy and block hydration. The build bakes no per-sandbox # values (all DEN_* env is consumed at runtime by next start), so a build # from the snapshot's base commit is reusable across sandboxes. echo "==> Building Den Web..." if ! env \ DEN_WEB_PORT="$DEN_WEB_PORT" \ DEN_API_BASE="$DEN_API_BASE" \ DEN_AUTH_ORIGIN="$DEN_AUTH_ORIGIN" \ DEN_AUTH_FALLBACK_BASE="$DEN_AUTH_FALLBACK_BASE" \ NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL="$NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL" \ NEXT_PUBLIC_POSTHOG_KEY= \ NEXT_PUBLIC_POSTHOG_API_KEY= \ DEN_ORG_MODE="$DEN_ORG_MODE" \ OPENWORK_DEV_MODE="$OPENWORK_DEV_MODE" \ DEN_WEB_ALLOWED_DEV_ORIGINS="$DEN_WEB_ALLOWED_DEV_ORIGINS" \ bash -c 'pnpm --filter @openwork/ui build && pnpm --filter @openwork-ee/utils build && pnpm --filter @openwork-ee/den-web build' > /tmp/den-web-build.log 2>&1; then echo "ERROR: Den Web build failed. Last 80 lines:" >&2 tail -n 80 /tmp/den-web-build.log >&2 exit 1 fi if [ -n "$den_web_key" ]; then printf "%s" "$den_web_key" > "$den_web_marker" else rm -f "$den_web_marker" fi fi echo "==> Starting Den Web on :$DEN_WEB_PORT..." pkill -f "next dev --hostname 0.0.0.0 --port $DEN_WEB_PORT" >/dev/null 2>&1 || true pkill -f "next start --hostname 0.0.0.0 --port $DEN_WEB_PORT" >/dev/null 2>&1 || true nohup env \ DEN_WEB_PORT="$DEN_WEB_PORT" \ DEN_API_BASE="$DEN_API_BASE" \ DEN_AUTH_ORIGIN="$DEN_AUTH_ORIGIN" \ DEN_AUTH_FALLBACK_BASE="$DEN_AUTH_FALLBACK_BASE" \ NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL="$NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL" \ NEXT_PUBLIC_POSTHOG_KEY= \ NEXT_PUBLIC_POSTHOG_API_KEY= \ DEN_ORG_MODE="$DEN_ORG_MODE" \ OPENWORK_DEV_MODE="$OPENWORK_DEV_MODE" \ DEN_WEB_ALLOWED_DEV_ORIGINS="$DEN_WEB_ALLOWED_DEV_ORIGINS" \ pnpm --filter @openwork-ee/den-web exec next start --hostname 0.0.0.0 --port "$DEN_WEB_PORT" > /tmp/den-web.log 2>&1 & wait_for_http "http://127.0.0.1:$DEN_WEB_PORT/api/den/health" "Den Web" 180 cat > .openwork-daytona/server-env <