源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)

This commit is contained in:
agent-radar
2026-09-02 01:05:41 +08:00
commit 04bac1f6e2
2200 changed files with 489415 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
# Full Electron + Den dev environment for Daytona sandboxes.
#
# Clones the repo from GitHub, installs deps, and provides
# Xvfb + noVNC so you can see/steer the real Electron app
# from your browser.
FROM node:20-bookworm
# System deps for Electron / Chromium + virtual display + utils
RUN apt-get update && apt-get install -y --no-install-recommends \
git unzip dbus dbus-x11 \
libgtk-3-0 libnotify-dev libnss3 libxss1 libasound2 \
libxtst6 libatk-bridge2.0-0 libdrm2 libgbm1 libxrandr2 \
libxcomposite1 libxdamage1 libxfixes3 libcups2 \
libpango-1.0-0 libcairo2 \
xvfb x11vnc novnc websockify fluxbox xterm \
default-mysql-client \
&& rm -rf /var/lib/apt/lists/*
# pnpm + bun
RUN corepack enable && corepack prepare pnpm@latest --activate
# Pin bun to the CI-proven 1.3 line; unpinned installs picked up bun 1.4.0,
# which breaks the Den/desktop sandbox stack (sign-in grant exchange fails).
RUN npm install -g bun@1.3.14
# noVNC index
RUN ln -sf /usr/share/novnc/vnc.html /usr/share/novnc/index.html
ENV DISPLAY=:99
ENV ELECTRON_DISABLE_SANDBOX=1
ENV PNPM_HOME=/root/.local/share/pnpm
ENV PATH=$PNPM_HOME:$PATH
# Clone and install
ARG REPO_URL=https://github.com/different-ai/openwork.git
ARG BRANCH=dev
WORKDIR /workspace
RUN git clone --depth 1 --branch $BRANCH $REPO_URL . && pnpm install --frozen-lockfile || pnpm install
EXPOSE 3005 5173 6080 8788 9825
+73
View File
@@ -0,0 +1,73 @@
# Daytona image for the OpenWork Den server stack.
#
# This intentionally does not run Docker inside Daytona. The sandbox runs the
# same services as packaging/docker/docker-compose.den-dev.yml directly:
# MySQL, Den API, and Den Web.
FROM daytonaio/sandbox:0.6.0
USER root
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
curl \
default-mysql-client \
default-mysql-server \
git \
procps \
sudo \
&& /usr/local/share/nvm/current/bin/npm install -g pnpm@10.27.0 bun@1.3.14 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /root/.cache /root/.npm /tmp/* \
&& printf 'daytona ALL=(ALL) NOPASSWD:ALL\n' >/etc/sudoers.d/daytona-openwork \
&& chmod 0440 /etc/sudoers.d/daytona-openwork \
&& install -d -o daytona -g daytona /workspace
WORKDIR /workspace
ENV PATH="/usr/local/share/nvm/current/bin:$PATH"
# Everything under /workspace is created as the daytona user so no later
# chown layer is needed: a recursive chown in its own layer copy-on-write
# duplicates the entire baked workspace and pushed the image past the 20 GB
# snapshot limit.
USER daytona
RUN git clone --depth 1 --branch dev https://github.com/different-ai/openwork.git . \
&& mkdir -p /workspace/.openwork-daytona \
&& sha256sum /workspace/pnpm-lock.yaml | cut -d ' ' -f 1 > /workspace/.openwork-daytona/pnpm-lock.sha256 \
&& git rev-parse HEAD > /workspace/.openwork-daytona/base-commit \
&& git gc --prune=now
# Bake dependencies and the sandbox-invariant Den builds so a boot from this
# snapshot only pays checkout + delta install + service start. The Den Web
# production build bakes no per-sandbox values: NEXT_PUBLIC_* inputs are fixed
# and every DEN_* value is runtime env consumed by `next start`.
# .devcontainer/start-daytona-server.sh skips these rebuilds when the git tree
# hashes recorded here still match the checked-out ref.
# Only the server packages' dependency graph is installed, and Electron and
# browser binaries are skipped: the server stack never runs them, and the
# smaller node_modules keeps the image inside the snapshot size limits. The
# full-lockfile sha marker stays valid because a filtered install uses the
# same lockfile; a ref that changes the lockfile reinstalls at boot.
RUN CI=1 ELECTRON_SKIP_BINARY_DOWNLOAD=1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
PUPPETEER_SKIP_DOWNLOAD=1 CYPRESS_INSTALL_BINARY=0 \
pnpm install --store-dir /workspace/.openwork-daytona/pnpm-store --frozen-lockfile \
--filter @openwork-ee/den-api... \
--filter @openwork-ee/den-web... \
--filter @openwork-ee/den-db... \
--filter @openwork/ui... \
--filter @openwork-ee/utils... \
--filter @openwork/mcp-apps... \
&& NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_API_KEY= \
pnpm --filter @openwork/ui build \
&& NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_API_KEY= \
pnpm --filter @openwork-ee/utils build \
&& NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_API_KEY= \
pnpm --filter @openwork-ee/den-web build \
&& pnpm --filter @openwork-ee/den-api run build:mcp-apps \
&& { git rev-parse HEAD:packages/ui HEAD:ee/packages/utils HEAD:ee/apps/den-web; cat /workspace/.openwork-daytona/pnpm-lock.sha256; } \
| sha256sum | cut -d ' ' -f 1 > /workspace/.openwork-daytona/den-web-build.tree \
&& { git rev-parse HEAD:packages/mcp-apps; cat /workspace/.openwork-daytona/pnpm-lock.sha256; } \
| sha256sum | cut -d ' ' -f 1 > /workspace/.openwork-daytona/den-api-assets.tree \
&& rm -rf /home/daytona/.cache /home/daytona/.npm /home/daytona/.local/share/pnpm/store
+29
View File
@@ -0,0 +1,29 @@
# Daytona VNC/Computer Use image with OpenWork preinstalled.
#
# Use this for Electron/noVNC tests instead of the generic devcontainer image.
# The base image already contains the desktop stack Daytona expects:
# Xvfb, XFCE, x11vnc, noVNC, websockify, and dbus-x11.
FROM daytonaio/sandbox:0.6.0
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*
RUN /usr/local/share/nvm/current/bin/npm install -g pnpm@10.27.0 bun
WORKDIR /workspace
RUN git clone --depth 1 --branch dev https://github.com/different-ai/openwork.git . \
&& mkdir -p /workspace/.openwork-daytona \
&& sha256sum /workspace/pnpm-lock.yaml | cut -d ' ' -f 1 > /workspace/.openwork-daytona/pnpm-lock.sha256 \
&& /usr/local/share/nvm/current/bin/npm cache clean --force \
&& git gc --prune=now \
&& rm -rf /root/.cache /root/.npm \
/home/daytona/.cache /home/daytona/.npm /home/daytona/.local/share/pnpm/store \
/workspace/node_modules /tmp/*
RUN chown -R daytona:daytona /workspace
USER daytona
+159
View File
@@ -0,0 +1,159 @@
# Daytona / Dev Container Setup
Full-stack dev environment that runs the **real Electron app** + Den stack in a cloud sandbox. You see and steer the desktop app through your browser via noVNC.
## What's included
| Service | Port | Description |
|---------|------|-------------|
| **Desktop App (noVNC)** | 6080 | The real Electron app rendered in a virtual display, accessible in your browser |
| **Den Web** | 3005 | Admin dashboard for managing orgs, restrictions, providers |
| **Den API** | 8788 | Control plane API |
| **CDP Debug** | 9825 | Chrome DevTools Protocol — for app and browser automation |
| **Vite HMR** | 5173 | Hot module replacement for the React UI |
| **MySQL** | 3306 | Database (internal) |
## Quick start with Daytona Electron/noVNC
```bash
bash .devcontainer/create-daytona-openwork-snapshot.sh # one-time / refresh when deps change
bash .devcontainer/test-on-daytona.sh [branch-or-commit]
```
The test script creates a sandbox from the reusable `openwork-eval-vnc` snapshot
when present, checks out the target ref, skips `pnpm install` if the lockfile is
unchanged, starts XFCE/noVNC, Vite, and Electron, then prints the noVNC and CDP
URLs. If the snapshot is missing, it fails fast and tells you to create it. The
snapshot intentionally does not bake `node_modules`; installs use the reusable
`openwork-eval-pnpm-store` volume so the image stays under Daytona's 20 GB limit.
For provider evals, create/populate the reusable Daytona secrets volume once:
```bash
bash .devcontainer/setup-daytona-secrets-volume.sh .newtoken
bash .devcontainer/setup-daytona-secrets-volume.sh .anthropic anthropic.env
```
Future Daytona test sandboxes mount `openwork-eval-secrets:/daytona-secrets`
and source every `/daytona-secrets/*.env` file automatically before Electron
starts. Use this volume for provider keys and other eval-only secrets; never
commit those files into the repo.
For downloadable eval artifacts or optional video recording, use:
```bash
bash .devcontainer/test-on-daytona.sh [branch-or-commit] --artifacts-volume
bash .devcontainer/test-on-daytona.sh [branch-or-commit] --record-video
```
The artifacts flow mounts `openwork-eval-artifacts:/daytona-artifacts`, starts a
static download server on port 8090, and prints a Daytona preview URL. Recording
writes mp4 files to `/daytona-artifacts/recordings` and prints the direct video
URL. Screenshots write png files to `/daytona-artifacts/screenshots` for quick
AI/human validation checkpoints. Stop recording with
`.devcontainer/stop-daytona-recording.sh` so ffmpeg finalizes the file cleanly.
Do not use the generic `daytona create https://github.com/different-ai/openwork`
flow for Electron/noVNC tests. The default resource size is too small and the
generic image path does not guarantee the desktop stack we need.
## Quick start with Daytona server
```bash
bash .devcontainer/create-daytona-openwork-server-snapshot.sh # one-time / refresh when deps change
bash .devcontainer/test-server-on-daytona.sh [branch-or-commit]
```
The server helper creates a separate public Daytona sandbox for the Den stack:
MySQL, Den API, and Den Web. It prints public preview URLs and
the exact Electron command to point a desktop sandbox at that server:
```bash
bash .devcontainer/test-on-daytona.sh [branch-or-commit] \
--den-base-url https://3005-...daytonaproxy... \
--den-api-base-url https://8788-...daytonaproxy...
```
This keeps the architecture simple: the server sandbox owns cloud auth, orgs,
policies, workers, and persistence; the Electron sandbox stays a real desktop
client and talks to the server through public Daytona preview URLs.
## How it works
1. `.devcontainer/Dockerfile.daytona-vnc` starts from `daytonaio/sandbox:0.6.0`,
which includes Daytona's expected desktop packages: Xvfb, XFCE, x11vnc,
noVNC, websockify, and dbus-x11.
2. `.devcontainer/create-daytona-openwork-snapshot.sh` bakes that image into
`openwork-eval-vnc` without `node_modules`.
3. `/opt/openwork-daytona/start-daytona-vnc.sh` starts Xvfb, XFCE, x11vnc, and
noVNC on display `:99`.
4. `test-on-daytona.sh` installs dependencies through the reusable
`openwork-eval-pnpm-store` volume when `node_modules` is missing or the
lockfile changed.
5. Vite serves the React UI on port 5173.
6. `/opt/openwork-daytona/start-daytona-electron.sh` sources optional secrets,
applies Daytona-safe Chromium flags, and starts Electron on display `:99`.
7. **CDP on port 9825** enables Chrome MCP and browser-tool automation.
8. Optional artifact capture mounts `/daytona-artifacts`, serves it on port 8090,
records display `:99` with ffmpeg when `--record-video` is passed, and can
capture screenshot checkpoints with `.devcontainer/capture-daytona-screenshot.sh`.
## Validation Evidence
Use three layers of evidence for Daytona UI work:
- **CDP assertions:** use browser tools against port 9825 to inspect text, URL,
state, and accessibility snapshots. This is the primary AI validation path.
- **Screenshots:** run `daytona exec "$SANDBOX" -- 'bash .devcontainer/capture-daytona-screenshot.sh'` after important states. These png files live in `/daytona-artifacts/screenshots`.
- **Recordings:** start with `--record-video --recording-name <name>` for flows
that need PR evidence. These mp4 files live in `/daytona-artifacts/recordings`.
Recordings prove the flow to humans. CDP assertions and screenshots give the AI
fast checkpoints to decide whether behavior is correct before reporting success.
## AI Skills
The Daytona toolbox is exposed to opencode through focused skills:
- `daytona-dev`: overview of the Daytona setup and when to use each piece.
- `daytona-cloud-server`: Den Web/API, marketplace, cloud auth, and org policy flows.
- `daytona-secrets-volume`: add and verify provider keys or eval-only secrets in `/daytona-secrets`.
- `daytona-electron-test`: run and drive the real Electron app through CDP/noVNC.
- `daytona-recording-artifacts`: screenshots, recordings, before/after videos, and PR evidence.
- `run-tests`: runs `evals/specs` coverage locally or with the relevant Daytona environment.
## Testing the customization system
1. Open **Den Web** (port 3005) in a separate tab
2. Sign up → create org → Org Settings → UI Customization
3. Set overrides → Save
4. In the **Electron app** (noVNC on port 6080):
- Cloud → developer mode → base URL `http://localhost:3005`
- Sign in → Settings → see the desktop policy banner
## Architecture
```
Your Browser
│
├── :6080 noVNC ──▶ x11vnc ──▶ XFCE/Xvfb ──▶ Electron App
│ │
│ ├── CDP :9825 (automatable)
│ └── Vite HMR :5173
│
├── :3005 Den Web (Next.js)
│
└── :8788 Den API (Hono) ──▶ MySQL :3306
```
With a separate server sandbox, the Electron box uses Daytona preview URLs for
Den Web/API instead of `localhost`, while the server sandbox still keeps its
internal service graph local.
## Automation
The Electron app exposes CDP on port 9825. You can:
- Connect Playwright: `const browser = await chromium.connectOverCDP('ws://localhost:9825')`
- Connect Chrome MCP for AI agent testing
- Take screenshots, run UI tests, etc.
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -euo pipefail
# Capture a single Daytona Electron display frame as a PNG artifact. Screenshots
# are for quick AI/human validation; videos are still the durable PR evidence.
OUTPUT="/daytona-artifacts/screenshots/daytona-screenshot-$(date +%Y%m%d-%H%M%S).png"
SIZE="1920x1080"
while [ "$#" -gt 0 ]; do
case "$1" in
--output)
shift
OUTPUT="${1:?missing output path}"
;;
--size)
shift
SIZE="${1:?missing screenshot size}"
;;
--help|-h)
printf '%s\n' \
"Usage: capture-daytona-screenshot.sh [--output PATH] [--size WxH]" \
"" \
"Captures DISPLAY, defaulting to :99, and writes a PNG file."
exit 0
;;
--*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
echo "Unexpected argument: $1" >&2
exit 1
;;
esac
shift
done
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "ERROR: ffmpeg is required for Daytona screenshots." >&2
exit 1
fi
if [[ ! "$SIZE" =~ ^[0-9]+x[0-9]+$ ]]; then
echo "ERROR: screenshot size must use WxH format, for example 1920x1080" >&2
exit 1
fi
FINAL_OUTPUT="$OUTPUT"
CAPTURE_OUTPUT="$OUTPUT"
if [[ "$OUTPUT" = /daytona-artifacts/* ]]; then
CAPTURE_OUTPUT="/tmp/daytona-screenshot-$(basename "$OUTPUT")"
fi
mkdir -p "$(dirname "$CAPTURE_OUTPUT")"
ffmpeg -y -f x11grab -video_size "$SIZE" -i "${DISPLAY:-:99}" -frames:v 1 "$CAPTURE_OUTPUT" >/tmp/daytona-screenshot.log 2>&1
if [ "$CAPTURE_OUTPUT" != "$FINAL_OUTPUT" ]; then
mkdir -p "$(dirname "$FINAL_OUTPUT")"
cp "$CAPTURE_OUTPUT" "$FINAL_OUTPUT"
fi
echo "Screenshot saved: $FINAL_OUTPUT"
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
# Build/refresh the reusable Daytona snapshot used by Den server sandboxes.
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SNAPSHOT_NAME="${DAYTONA_SERVER_SNAPSHOT:-openwork-server}"
REGION="${DAYTONA_TARGET:-us}"
snapshot_id() {
daytona snapshot list -f json | node -e 'const name = process.argv[1]; let input = ""; process.stdin.on("data", (chunk) => input += chunk); process.stdin.on("end", () => { const snapshot = JSON.parse(input).find((item) => item.name === name); if (snapshot) process.stdout.write(snapshot.id || snapshot.name); });' "$1"
}
existing_snapshot_id="$(snapshot_id "$SNAPSHOT_NAME")"
if [ -n "$existing_snapshot_id" ]; then
echo "==> Deleting existing snapshot: $SNAPSHOT_NAME"
daytona snapshot delete "$existing_snapshot_id" >/dev/null <<< "y"
for _ in $(seq 1 60); do
if [ -z "$(snapshot_id "$SNAPSHOT_NAME")" ]; then
break
fi
sleep 5
done
fi
echo "==> Creating Daytona server snapshot: $SNAPSHOT_NAME"
# 10 GB is the per-sandbox maximum in this org; the baked node_modules and
# Den build outputs fit because the image skips Electron/browser binaries.
daytona snapshot create "$SNAPSHOT_NAME" \
--dockerfile "$ROOT_DIR/.devcontainer/Dockerfile.daytona-server" \
--cpu 4 \
--memory 8 \
--disk 10 \
--region "$REGION"
echo "Snapshot ready: $SNAPSHOT_NAME"
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
# Build/refresh the reusable Daytona VNC snapshot used by eval sandboxes from
# the prebuilt GHCR image. The image is built by GitHub Actions on dev pushes.
SNAPSHOT_NAME="${DAYTONA_EVAL_SNAPSHOT:-openwork-eval-vnc}"
IMAGE="${DAYTONA_EVAL_IMAGE:-ghcr.io/different-ai/openwork-eval-vnc:dev}"
REGION="${DAYTONA_TARGET:-us}"
existing_snapshot_id="$(daytona snapshot list -f json | node -e 'const name = process.argv[1]; let input = ""; process.stdin.on("data", (chunk) => input += chunk); process.stdin.on("end", () => { const snapshot = JSON.parse(input).find((item) => item.name === name); if (snapshot) process.stdout.write(snapshot.id || snapshot.name); });' "$SNAPSHOT_NAME")"
if [ -n "$existing_snapshot_id" ]; then
echo "==> Deleting existing snapshot: $SNAPSHOT_NAME"
daytona snapshot delete "$existing_snapshot_id" >/dev/null <<< "y"
for _ in $(seq 1 60); do
if ! daytona snapshot list -f json | node -e 'const name = process.argv[1]; let input = ""; process.stdin.on("data", (chunk) => input += chunk); process.stdin.on("end", () => { const snapshot = JSON.parse(input).find((item) => item.name === name); process.exit(snapshot ? 1 : 0); });' "$SNAPSHOT_NAME"; then
sleep 5
else
break
fi
done
fi
echo "==> Creating Daytona eval snapshot: $SNAPSHOT_NAME"
daytona snapshot create "$SNAPSHOT_NAME" \
--image "$IMAGE" \
--cpu 4 \
--memory 8 \
--disk 10 \
--region "$REGION"
echo "Snapshot ready: $SNAPSHOT_NAME"
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
# Start the OpenWork desktop dev stack inside a Daytona/devcontainer sandbox.
# This is the sandbox equivalent of `pnpm dev`: it prepares the virtual display
# and launches the desktop dev runner in the background with CDP enabled.
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
printf '%s\n' \
"Usage: pnpm dev:sandbox" \
"" \
"Starts the OpenWork desktop dev stack inside a Daytona/devcontainer sandbox." \
"Use .devcontainer/test-on-daytona.sh [branch-or-commit] to provision a new Daytona sandbox."
exit 0
fi
if [ "$#" -gt 0 ]; then
echo "Unexpected argument: $1" >&2
echo "Use --help for usage." >&2
exit 1
fi
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"
export OPENWORK_WORKSPACE_DIR="$REPO_DIR"
export OPENWORK_DEV_MODE="${OPENWORK_DEV_MODE:-1}"
export DISPLAY="${DISPLAY:-:99}"
export ELECTRON_DISABLE_SANDBOX="${ELECTRON_DISABLE_SANDBOX:-1}"
export OPENWORK_REACT_DEVTOOLS="${OPENWORK_REACT_DEVTOOLS:-0}"
export OPENWORK_ELECTRON_REMOTE_DEBUG_PORT="${OPENWORK_ELECTRON_REMOTE_DEBUG_PORT:-9825}"
export DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS="${DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS:---disable-gpu --disable-dev-shm-usage --enable-unsafe-swiftshader}"
VNC_SCRIPT="$REPO_DIR/.devcontainer/start-daytona-vnc.sh"
ELECTRON_SCRIPT="$REPO_DIR/.devcontainer/start-daytona-electron.sh"
echo "==> Starting Daytona display stack..."
bash "$VNC_SCRIPT"
echo "==> Starting OpenWork dev stack in background..."
bash "$ELECTRON_SCRIPT" --detach
echo "==> Waiting for Electron CDP on :$OPENWORK_ELECTRON_REMOTE_DEBUG_PORT..."
for _ in $(seq 1 30); do
if curl -sf "http://127.0.0.1:$OPENWORK_ELECTRON_REMOTE_DEBUG_PORT/json/list" >/dev/null 2>&1; then
echo "OpenWork sandbox dev stack is ready."
echo "Electron log: /tmp/electron.log"
exit 0
fi
sleep 5
done
echo "ERROR: Electron CDP did not become ready." >&2
echo "Check logs with: tail -80 /tmp/electron.log" >&2
exit 1
+29
View File
@@ -0,0 +1,29 @@
{
"name": "OpenWork Full Stack (Electron)",
"dockerComposeFile": "docker-compose.yml",
"service": "workspace",
"workspaceFolder": "/workspace",
"forwardPorts": [
3005,
5173,
6080,
8788,
9825
],
"portsAttributes": {
"5173": { "label": "App (Vite HMR)", "onAutoForward": "silent" },
"3005": { "label": "Den Web", "onAutoForward": "notify" },
"6080": { "label": "Desktop App (noVNC)", "onAutoForward": "notify" },
"8788": { "label": "Den API", "onAutoForward": "silent" },
"9825": { "label": "CDP Debug", "onAutoForward": "silent" }
},
"postCreateCommand": "pnpm install",
"postStartCommand": ".devcontainer/start-services.sh",
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "bash"
}
}
}
}
+76
View File
@@ -0,0 +1,76 @@
# Full-stack dev environment for Daytona / Codespaces / devcontainers.
#
# Runs the real Electron app with a virtual display accessible via noVNC.
#
# Services:
# - MySQL 8.4 for the Den database
# - Workspace container with Node 20, Electron deps, Xvfb, noVNC
#
# Ports:
# - 5173 App Vite dev server (HMR)
# - 3005 Den Web dashboard
# - 6080 noVNC — see the Electron app in your browser
# - 8788 Den API
# - 9825 CDP remote debugging (for Chrome MCP / automation)
services:
mysql:
image: mysql:8.4
command:
- --performance_schema=OFF
- --innodb-buffer-pool-size=64M
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: openwork_den
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -ppassword --silent"]
interval: 5s
timeout: 5s
retries: 30
start_period: 10s
volumes:
- mysql-data:/var/lib/mysql
workspace:
build:
context: .
dockerfile: Dockerfile
depends_on:
mysql:
condition: service_healthy
volumes:
- ..:/workspace:cached
ports:
- "5173:5173"
- "3005:3005"
- "6080:6080"
- "8788:8788"
- "9825:9825"
environment:
# Display
DISPLAY: ":99"
ELECTRON_DISABLE_SANDBOX: "1"
# Electron CDP debugging
OPENWORK_ELECTRON_REMOTE_DEBUG_PORT: "9825"
# Den API config
DATABASE_URL: mysql://root:password@mysql:3306/openwork_den
DEN_DB_ENCRYPTION_KEY: local-dev-db-encryption-key-please-change-1234567890
BETTER_AUTH_SECRET: local-dev-secret-not-for-production-use!!
BETTER_AUTH_URL: http://localhost:3005
OPENWORK_DEV_MODE: "1"
DEN_PASSWORD_BREACH_SCREENING_ENABLED: "false"
PORT: "8788"
PROVISIONER_MODE: stub
# Den Web config
DEN_API_BASE: http://localhost:8788
DEN_AUTH_ORIGIN: http://localhost:3005
DEN_AUTH_FALLBACK_BASE: http://localhost:8788
DEN_WEB_PORT: "3005"
DEN_API_PORT: "8788"
# CORS
CORS_ORIGINS: http://localhost:3005,http://localhost:5173,http://localhost:8788
DEN_BETTER_AUTH_TRUSTED_ORIGINS: http://localhost:3005,http://localhost:5173,http://localhost:8788
command: sleep infinity
volumes:
mysql-data:
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
set -euo pipefail
# Create the reusable Daytona secrets volume once. If a local env file is
# provided, copy it into the mounted volume without printing it.
#
# Usage:
# bash .devcontainer/setup-daytona-secrets-volume.sh [.newtoken] [openai.env]
VOLUME_NAME="${DAYTONA_SECRETS_VOLUME:-openwork-eval-secrets}"
MOUNT_PATH="${DAYTONA_SECRETS_MOUNT:-/daytona-secrets}"
LOCAL_ENV_FILE="${1:-.newtoken}"
DEST_ENV_FILE="${2:-openai.env}"
SANDBOX="openwork-secrets-setup-$(date +%Y%m%d-%H%M%S)"
SNAPSHOT_NAME="${DAYTONA_EVAL_SNAPSHOT:-openwork-eval-vnc}"
volume_field() {
local name="$1"
local field="$2"
daytona volume list -f json | node -e 'const name = process.argv[1]; const field = process.argv[2]; let input = ""; process.stdin.on("data", (chunk) => input += chunk); process.stdin.on("end", () => { const volume = JSON.parse(input).find((item) => item.name === name); if (!volume) process.exit(1); process.stdout.write(String(volume[field] ?? "")); });' "$name" "$field"
}
wait_for_volume_ready() {
local name="$1"
local state=""
for _ in $(seq 1 60); do
state="$(volume_field "$name" state 2>/dev/null || true)"
if [ "$state" = "ready" ]; then
return 0
fi
if [ "$state" = "error" ]; then
echo "ERROR: volume '$name' is in error state" >&2
exit 1
fi
sleep 5
done
echo "ERROR: volume '$name' did not become ready (last state: ${state:-missing})" >&2
exit 1
}
if [[ ! "$DEST_ENV_FILE" =~ ^[A-Za-z0-9._-]+\.env$ ]]; then
echo "ERROR: destination env file must be a simple .env filename, for example openai.env" >&2
exit 1
fi
cleanup() {
yes | daytona delete "$SANDBOX" >/dev/null 2>&1 || true
}
trap cleanup EXIT
volume_create_output="$(daytona volume create "$VOLUME_NAME" --size 1 2>&1 || true)"
if [ -n "$volume_create_output" ] && ! printf '%s' "$volume_create_output" | grep -qi "already exists"; then
printf '%s\n' "$volume_create_output"
fi
wait_for_volume_ready "$VOLUME_NAME"
VOLUME_ID="$(volume_field "$VOLUME_NAME" id)"
if [ ! -f "$LOCAL_ENV_FILE" ]; then
echo "Volume ready: $VOLUME_NAME"
echo "No local env file found at $LOCAL_ENV_FILE; skipping secret copy."
exit 0
fi
SECRET_ENV_B64="$(base64 < "$LOCAL_ENV_FILE" | tr -d '\n')"
SNAPSHOT_ID="$(daytona snapshot list -f json | node -e 'const name = process.argv[1]; let input = ""; process.stdin.on("data", (chunk) => input += chunk); process.stdin.on("end", () => { const snapshot = JSON.parse(input).find((item) => item.name === name); if (snapshot) process.stdout.write(snapshot.id || snapshot.name); });' "$SNAPSHOT_NAME")"
if [ -z "$SNAPSHOT_ID" ]; then
echo "ERROR: Daytona snapshot '$SNAPSHOT_NAME' not found." >&2
echo "Create it with: bash .devcontainer/create-daytona-openwork-snapshot.sh" >&2
exit 1
fi
daytona create \
--name "$SANDBOX" \
--snapshot "$SNAPSHOT_ID" \
--volume "${VOLUME_ID}:${MOUNT_PATH}" \
--auto-stop 15 \
--public \
--target us >/dev/null
exec_ready=0
for _ in $(seq 1 60); do
if daytona exec "$SANDBOX" -- "bash -lc 'true'" >/dev/null 2>&1; then
exec_ready=1
break
fi
sleep 5
done
if [ "$exec_ready" -ne 1 ]; then
echo "ERROR: sandbox did not become exec-ready" >&2
exit 1
fi
daytona exec "$SANDBOX" -- "bash -lc 'mkdir -p \"$MOUNT_PATH\" && umask 177 && printf %s \"$SECRET_ENV_B64\" | base64 -d > \"$MOUNT_PATH/$DEST_ENV_FILE\"'" >/dev/null
daytona exec "$SANDBOX" -- "bash -lc 'test -s \"$MOUNT_PATH/$DEST_ENV_FILE\" && chmod 600 \"$MOUNT_PATH/$DEST_ENV_FILE\"'" >/dev/null
echo "Volume ready: $VOLUME_NAME"
echo "Secret env installed at ${MOUNT_PATH}/${DEST_ENV_FILE}"
echo "Electron sources every ${MOUNT_PATH}/*.env file by default."
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euo pipefail
# Start the real Electron app in Daytona/headless Linux with CDP enabled.
# This centralizes the graphics-safe Chromium flags and optional secret-volume
# env loading used by Daytona evals.
cd "${OPENWORK_WORKSPACE_DIR:-/workspace}"
if [ "${1:-}" = "--detach" ]; then
shift
SCRIPT_PATH="${BASH_SOURCE[0]}"
LOG_PATH="${DAYTONA_ELECTRON_LOG:-/tmp/electron.log}"
python3 - "$SCRIPT_PATH" "$LOG_PATH" "$@" <<'PY'
import os
import subprocess
import sys
script_path, log_path, *args = sys.argv[1:]
log = open(log_path, "ab", buffering=0)
subprocess.Popen(
["bash", script_path, *args],
cwd=os.environ.get("OPENWORK_WORKSPACE_DIR", "/workspace"),
env=os.environ.copy(),
stdin=subprocess.DEVNULL,
stdout=log,
stderr=subprocess.STDOUT,
start_new_session=True,
close_fds=True,
)
PY
exit 0
fi
DAYTONA_SECRETS_ENV="${DAYTONA_SECRETS_ENV:-/daytona-secrets}"
DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS="${DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS:---disable-gpu --disable-dev-shm-usage --enable-unsafe-swiftshader}"
source_secret_env_file() {
local env_file="$1"
if [ -f "$env_file" ]; then
set -a
# shellcheck disable=SC1090
source "$env_file"
set +a
fi
}
if [ -d "$DAYTONA_SECRETS_ENV" ]; then
shopt -s nullglob
for env_file in "$DAYTONA_SECRETS_ENV"/*.env; do
source_secret_env_file "$env_file"
done
shopt -u nullglob
elif [ -f "$DAYTONA_SECRETS_ENV" ]; then
set -a
# shellcheck disable=SC1090
source "$DAYTONA_SECRETS_ENV"
set +a
fi
export DISPLAY="${DISPLAY:-:99}"
export ELECTRON_DISABLE_SANDBOX="${ELECTRON_DISABLE_SANDBOX:-1}"
export ELECTRON_EXTRA_LAUNCH_ARGS="${ELECTRON_EXTRA_LAUNCH_ARGS:-$DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS}"
export OPENWORK_REACT_DEVTOOLS="${OPENWORK_REACT_DEVTOOLS:-0}"
export OPENWORK_DEV_MODE="${OPENWORK_DEV_MODE:-1}"
export OPENWORK_ELECTRON_REMOTE_DEBUG_PORT="${OPENWORK_ELECTRON_REMOTE_DEBUG_PORT:-9825}"
export OPENWORK_ELECTRON_FAKE_MEDIA="${OPENWORK_ELECTRON_FAKE_MEDIA:-0}"
if [ -n "${OPENWORK_ELECTRON_USERDATA:-}" ]; then
export OPENWORK_ELECTRON_USERDATA
fi
if [ -n "${OPENWORK_ELECTRON_APP_IDENTIFIER:-}" ]; then
export OPENWORK_ELECTRON_APP_IDENTIFIER
fi
if [ -n "${OPENWORK_ELECTRON_APP_NAME:-}" ]; then
export OPENWORK_ELECTRON_APP_NAME
fi
if [ -n "${OPENWORK_EVAL_ELECTRON_BINARY:-}" ]; then
if [ ! -x "$OPENWORK_EVAL_ELECTRON_BINARY" ]; then
echo "OPENWORK_EVAL_ELECTRON_BINARY is not an executable file: $OPENWORK_EVAL_ELECTRON_BINARY" >&2
exit 1
fi
exec "$OPENWORK_EVAL_ELECTRON_BINARY"
fi
exec pnpm --filter @openwork/desktop dev:electron
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -euo pipefail
# Record the Daytona Electron display to an mp4 artifact. Stop with SIGINT so
# ffmpeg finalizes the file cleanly.
OUTPUT="/daytona-artifacts/recordings/daytona-recording.mp4"
SIZE="1920x1080"
FPS="15"
LOG_PATH="/tmp/daytona-recording.log"
DETACH=0
while [ "$#" -gt 0 ]; do
case "$1" in
--detach)
DETACH=1
;;
--output)
shift
OUTPUT="${1:?missing output path}"
;;
--size)
shift
SIZE="${1:?missing recording size}"
;;
--fps)
shift
FPS="${1:?missing recording fps}"
;;
--log)
shift
LOG_PATH="${1:?missing log path}"
;;
--help|-h)
printf '%s\n' \
"Usage: start-daytona-recording.sh [--detach] [--output PATH] [--size WxH] [--fps N]" \
"" \
"Records DISPLAY, defaulting to :99, and writes an mp4 file."
exit 0
;;
--*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
echo "Unexpected argument: $1" >&2
exit 1
;;
esac
shift
done
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "ERROR: ffmpeg is required for Daytona display recording." >&2
exit 1
fi
if [ "$DETACH" -eq 1 ]; then
SCRIPT_PATH="${BASH_SOURCE[0]}"
nohup bash "$SCRIPT_PATH" --output "$OUTPUT" --size "$SIZE" --fps "$FPS" >"$LOG_PATH" 2>&1 &
echo "Recording started: $OUTPUT"
echo "Recording log: $LOG_PATH"
exit 0
fi
FINAL_OUTPUT="$OUTPUT"
RECORD_OUTPUT="$OUTPUT"
if [[ "$OUTPUT" = /daytona-artifacts/* ]]; then
RECORD_OUTPUT="/tmp/daytona-recording-$(basename "$OUTPUT")"
fi
mkdir -p "$(dirname "$RECORD_OUTPUT")"
FFMPEG_ARGS=(
-y
-f x11grab
-video_size "$SIZE"
-framerate "$FPS"
-i "${DISPLAY:-:99}"
-codec:v libx264
-preset veryfast
-pix_fmt yuv420p
"$RECORD_OUTPUT"
)
set +e
ffmpeg "${FFMPEG_ARGS[@]}"
status="$?"
set -e
if [ "$RECORD_OUTPUT" != "$FINAL_OUTPUT" ]; then
mkdir -p "$(dirname "$FINAL_OUTPUT")"
cp "$RECORD_OUTPUT" "$FINAL_OUTPUT"
fi
exit "$status"
+308
View File
@@ -0,0 +1,308 @@
#!/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 <<EOF
DEN_API_URL=$DEN_API_PUBLIC_URL
DEN_WEB_URL=$DEN_WEB_PUBLIC_URL
BETTER_AUTH_URL=$BETTER_AUTH_URL
DEN_MCP_RESOURCE_URL=$DEN_MCP_RESOURCE_URL
DEN_PROVISIONER_MODE=$DEN_PROVISIONER_MODE
EOF
echo ""
echo "============================================"
echo " OpenWork Daytona server stack ready"
echo ""
echo " Den Web: $DEN_WEB_PUBLIC_URL"
echo " Den API: $DEN_API_PUBLIC_URL"
echo ""
echo " Logs:"
echo " /tmp/den-api.log"
echo " /tmp/den-web.log"
echo " /tmp/den-db-push.log"
echo "============================================"
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
# Start Daytona-compatible desktop services for Electron tests.
# Assumes the daytonaio/sandbox base image packages are present.
DISPLAY_NUM="${DISPLAY_NUM:-99}"
DISPLAY=":${DISPLAY_NUM}"
RESOLUTION="${DISPLAY_RESOLUTION:-1920x1080x24}"
VNC_PORT="${VNC_PORT:-5900}"
NOVNC_PORT="${NOVNC_PORT:-6080}"
export DISPLAY
mkdir -p /tmp/.X11-unix
if ! pgrep -f "Xvfb ${DISPLAY}" >/dev/null 2>&1; then
rm -f "/tmp/.X${DISPLAY_NUM}-lock" "/tmp/.X11-unix/X${DISPLAY_NUM}" 2>/dev/null || true
echo "==> Starting Xvfb on ${DISPLAY} (${RESOLUTION})..."
Xvfb "${DISPLAY}" -screen 0 "${RESOLUTION}" -ac > /tmp/xvfb.log 2>&1 &
sleep 1
fi
if ! pgrep -f "xfce4-session" >/dev/null 2>&1; then
echo "==> Starting XFCE on ${DISPLAY}..."
DISPLAY="${DISPLAY}" startxfce4 > /tmp/xfce.log 2>&1 &
sleep 2
fi
if ! pgrep -f "x11vnc .*${VNC_PORT}" >/dev/null 2>&1; then
echo "==> Starting x11vnc on :${VNC_PORT}..."
x11vnc -display "${DISPLAY}" -forever -nopw -shared -rfbport "${VNC_PORT}" > /tmp/x11vnc.log 2>&1 &
sleep 1
fi
if ! pgrep -f "websockify .*${NOVNC_PORT}" >/dev/null 2>&1; then
echo "==> Starting noVNC on :${NOVNC_PORT}..."
websockify --web /usr/share/novnc "${NOVNC_PORT}" "localhost:${VNC_PORT}" > /tmp/novnc.log 2>&1 &
fi
echo "Desktop ready. DISPLAY=${DISPLAY}, noVNC=http://localhost:${NOVNC_PORT}"
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euo pipefail
# Start all services for the Daytona/devcontainer workspace.
# Launches the real Electron app with a virtual display.
#
# Usage: bash .devcontainer/start-services.sh
#
# Services started:
# - Xvfb + noVNC (port 6080) — see the Electron app in your browser
# - Vite dev server (port 5173) — React UI with HMR
# - Electron app — the real desktop app on the virtual display
# - CDP debugging (port 9825) — for automation
#
# Optional (if MySQL is available):
# - Den API (port 8788)
# - Den Web (port 3005)
cd /workspace
# ── 1. Virtual display ──
echo "==> Starting virtual display..."
.devcontainer/start-daytona-vnc.sh
sleep 2
# ── 2. Vite dev server on 0.0.0.0 (so Electron can reach it via 127.0.0.1) ──
echo "==> Starting Vite on :5173..."
cd apps/app
OPENWORK_DEV_MODE=1 nohup npx vite --host 0.0.0.0 --port 5173 > /tmp/vite.log 2>&1 &
cd /workspace
sleep 3
# ── 3. Electron app ──
echo "==> Starting Electron app..."
bash .devcontainer/start-daytona-electron.sh --detach
# ── 4. Wait for Electron to be ready ──
echo "==> Waiting for Electron..."
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:9825/json/list >/dev/null 2>&1; then
echo "Electron CDP ready."
break
fi
sleep 2
done
# ── 5. Optional: Den stack (only if MySQL is reachable) ──
if mysql -h mysql -u root -ppassword -e "SELECT 1" >/dev/null 2>&1; then
echo "==> MySQL found, starting Den stack..."
echo " Pushing DB schema..."
pnpm --filter @openwork-ee/den-db db:push 2>&1 || echo " DB push failed (may be up to date)"
echo " Starting Den API on :8788..."
pnpm dev:den:api > /tmp/den-api.log 2>&1 &
for i in $(seq 1 20); do
if curl -sf http://localhost:8788/health >/dev/null 2>&1; then
echo " Den API healthy."
break
fi
sleep 2
done
echo " Starting Den Web on :3005..."
pnpm dev:den:web > /tmp/den-web.log 2>&1 &
else
echo "==> MySQL not found, skipping Den stack."
fi
echo ""
echo "============================================"
echo " All services running!"
echo ""
echo " Desktop App (noVNC): http://localhost:6080"
echo " CDP Debug: ws://127.0.0.1:9825"
echo " Vite HMR: http://localhost:5173"
if mysql -h mysql -u root -ppassword -e "SELECT 1" >/dev/null 2>&1; then
echo " Den Web: http://localhost:3005"
echo " Den API: http://localhost:8788"
fi
echo "============================================"
echo ""
# Keep alive
wait
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
# Stop a running Daytona display recording. Sends SIGINT to ffmpeg so it
# finalizes the mp4 cleanly.
#
# Usage:
# # Stop from inside the sandbox:
# bash .devcontainer/stop-daytona-recording.sh
#
# # Stop from the host:
# daytona exec $SANDBOX -- 'bash .devcontainer/stop-daytona-recording.sh'
WAIT="${1:-5}"
mapfile -t pids < <(pgrep -f 'ffmpeg.*x11grab' 2>/dev/null || true)
if [ "${#pids[@]}" -eq 0 ]; then
echo "No active recording found."
exit 0
fi
for pid in "${pids[@]}"; do
kill -INT "$pid" 2>/dev/null || true
done
echo "Sent SIGINT to ffmpeg (pids: ${pids[*]}). Waiting ${WAIT}s for finalization..."
sleep "$WAIT"
for pid in "${pids[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
echo "WARNING: ffmpeg still running after ${WAIT}s; sending SIGTERM to $pid."
kill "$pid" 2>/dev/null || true
fi
done
echo "Recording stopped."
latest_recording=""
if [ -d /daytona-artifacts/recordings ]; then
shopt -s nullglob
for candidate in /daytona-artifacts/recordings/*.mp4; do
if [ -z "$latest_recording" ] || [ "$candidate" -nt "$latest_recording" ]; then
latest_recording="$candidate"
fi
done
shopt -u nullglob
fi
if [ -n "$latest_recording" ]; then
if [ ! -s "$latest_recording" ]; then
echo "WARNING: latest recording is empty: $latest_recording" >&2
exit 1
fi
echo "Latest recording: $latest_recording"
ls -lh "$latest_recording"
if command -v ffprobe >/dev/null 2>&1; then
duration="$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$latest_recording" 2>/dev/null || true)"
if [ -n "$duration" ]; then
echo "Duration seconds: $duration"
else
echo "WARNING: could not read recording duration with ffprobe" >&2
fi
fi
fi
+305
View File
@@ -0,0 +1,305 @@
#!/usr/bin/env bash
set -euo pipefail
# test-on-daytona.sh — one-command Daytona Electron test sandbox
#
# Usage:
# bash .devcontainer/test-on-daytona.sh [branch-or-commit]
# bash .devcontainer/test-on-daytona.sh [branch-or-commit] --force-install
# bash .devcontainer/test-on-daytona.sh [branch-or-commit] --artifacts-volume
# bash .devcontainer/test-on-daytona.sh [branch-or-commit] --record-video
# bash .devcontainer/setup-daytona-secrets-volume.sh [.newtoken]
#
# Creates a Daytona sandbox from the reusable VNC snapshot, checks out the given
# ref, installs dependencies with a workspace-local pnpm store when needed,
# starts `pnpm dev:sandbox`, and prints CDP/noVNC URLs. Optionally mounts an
# artifacts volume and records the Electron display.
REF=""
FORCE_INSTALL=0
DEN_BASE_URL=""
DEN_API_BASE_URL=""
DEN_REQUIRE_SIGNIN=0
ARTIFACTS_ENABLED=0
RECORD_VIDEO=0
RECORDING_NAME=""
RECORDING_FPS="15"
RECORDING_SIZE="1920x1080"
while [ "$#" -gt 0 ]; do
case "$1" in
--force-install)
FORCE_INSTALL=1
;;
--snapshot)
shift
DAYTONA_EVAL_SNAPSHOT="${1:?missing snapshot name}"
;;
--den-base-url)
shift
DEN_BASE_URL="${1:?missing Den base URL}"
;;
--den-api-base-url)
shift
DEN_API_BASE_URL="${1:?missing Den API base URL}"
;;
--require-signin)
DEN_REQUIRE_SIGNIN=1
;;
--artifacts-volume)
ARTIFACTS_ENABLED=1
;;
--record-video)
ARTIFACTS_ENABLED=1
RECORD_VIDEO=1
;;
--recording-name)
shift
RECORDING_NAME="${1:?missing recording name}"
;;
--recording-fps)
shift
RECORDING_FPS="${1:?missing recording fps}"
;;
--recording-size)
shift
RECORDING_SIZE="${1:?missing recording size}"
;;
--help|-h)
sed -n '1,16p' "$0"
exit 0
;;
--*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
if [ -n "$REF" ]; then
echo "Unexpected extra argument: $1" >&2
exit 1
fi
REF="$1"
;;
esac
shift
done
if [ -z "$REF" ]; then
REF="$(git branch --show-current 2>/dev/null || true)"
REF="${REF:-$(git rev-parse HEAD)}"
fi
SANDBOX="openwork-test-$(date +%Y%m%d-%H%M%S)"
CDP_PORT=9825
NOVNC_PORT=6080
ARTIFACTS_PORT=8090
MAX_WAIT=90
DAYTONA_EVAL_SNAPSHOT="${DAYTONA_EVAL_SNAPSHOT:-openwork-eval-vnc}"
DAYTONA_SECRETS_VOLUME="${DAYTONA_SECRETS_VOLUME:-openwork-eval-secrets}"
DAYTONA_SECRETS_MOUNT="${DAYTONA_SECRETS_MOUNT:-/daytona-secrets}"
DAYTONA_SECRETS_ENV="${DAYTONA_SECRETS_ENV:-${DAYTONA_SECRETS_MOUNT}}"
DAYTONA_ARTIFACTS_VOLUME="${DAYTONA_ARTIFACTS_VOLUME:-openwork-eval-artifacts}"
DAYTONA_ARTIFACTS_MOUNT="${DAYTONA_ARTIFACTS_MOUNT:-/daytona-artifacts}"
DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS="${DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS:---disable-gpu --disable-dev-shm-usage --enable-unsafe-swiftshader}"
OPENWORK_ELECTRON_FAKE_MEDIA="${OPENWORK_ELECTRON_FAKE_MEDIA:-0}"
if [ -z "$RECORDING_NAME" ]; then
RECORDING_NAME="$SANDBOX"
fi
if [[ ! "$RECORDING_NAME" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "ERROR: recording name may only contain letters, numbers, dots, underscores, and dashes" >&2
exit 1
fi
if [[ ! "$RECORDING_FPS" =~ ^[0-9]+$ ]]; then
echo "ERROR: recording fps must be a positive integer" >&2
exit 1
fi
if [[ ! "$RECORDING_SIZE" =~ ^[0-9]+x[0-9]+$ ]]; then
echo "ERROR: recording size must use WxH format, for example 1920x1080" >&2
exit 1
fi
volume_field() {
local name="$1"
local field="$2"
daytona volume list -f json | node -e 'const name = process.argv[1]; const field = process.argv[2]; let input = ""; process.stdin.on("data", (chunk) => input += chunk); process.stdin.on("end", () => { const volume = JSON.parse(input).find((item) => item.name === name); if (!volume) process.exit(1); process.stdout.write(String(volume[field] ?? "")); });' "$name" "$field"
}
wait_for_volume_ready() {
local name="$1"
local state=""
for _ in $(seq 1 60); do
state="$(volume_field "$name" state 2>/dev/null || true)"
if [ "$state" = "ready" ]; then
return 0
fi
if [ "$state" = "error" ]; then
echo "ERROR: volume '$name' is in error state" >&2
exit 1
fi
sleep 5
done
echo "ERROR: volume '$name' did not become ready (last state: ${state:-missing})" >&2
exit 1
}
snapshot_id() {
daytona snapshot list -f json | node -e 'const name = process.argv[1]; let input = ""; process.stdin.on("data", (chunk) => input += chunk); process.stdin.on("end", () => { const snapshot = JSON.parse(input).find((item) => item.name === name); if (snapshot) process.stdout.write(snapshot.id || snapshot.name); });' "$1"
}
echo "==> Creating sandbox: $SANDBOX"
echo " Ref: $REF"
echo ""
echo "==> Ensuring reusable secrets volume: $DAYTONA_SECRETS_VOLUME"
volume_create_output="$(daytona volume create "$DAYTONA_SECRETS_VOLUME" --size 1 2>&1 || true)"
if [ -n "$volume_create_output" ] && ! printf '%s' "$volume_create_output" | grep -qi "already exists"; then
printf '%s\n' "$volume_create_output"
fi
wait_for_volume_ready "$DAYTONA_SECRETS_VOLUME"
DAYTONA_SECRETS_VOLUME_ID="$(volume_field "$DAYTONA_SECRETS_VOLUME" id)"
DAYTONA_ARTIFACTS_VOLUME_ID=""
if [ "$ARTIFACTS_ENABLED" -eq 1 ]; then
echo ""
echo "==> Ensuring reusable artifacts volume: $DAYTONA_ARTIFACTS_VOLUME"
volume_create_output="$(daytona volume create "$DAYTONA_ARTIFACTS_VOLUME" --size 5 2>&1 || true)"
if [ -n "$volume_create_output" ] && ! printf '%s' "$volume_create_output" | grep -qi "already exists"; then
printf '%s\n' "$volume_create_output"
fi
wait_for_volume_ready "$DAYTONA_ARTIFACTS_VOLUME"
DAYTONA_ARTIFACTS_VOLUME_ID="$(volume_field "$DAYTONA_ARTIFACTS_VOLUME" id)"
fi
SNAPSHOT_ID="$(snapshot_id "$DAYTONA_EVAL_SNAPSHOT")"
if [ -z "$SNAPSHOT_ID" ]; then
echo "ERROR: Daytona snapshot '$DAYTONA_EVAL_SNAPSHOT' not found." >&2
echo "Create it with: bash .devcontainer/create-daytona-openwork-snapshot.sh" >&2
exit 1
fi
echo "==> Using Daytona snapshot: $DAYTONA_EVAL_SNAPSHOT"
DAYTONA_CREATE_ARGS=(
--name "$SANDBOX"
--snapshot "$SNAPSHOT_ID"
--volume "${DAYTONA_SECRETS_VOLUME_ID}:${DAYTONA_SECRETS_MOUNT}"
--auto-stop 60
--public
--target us
)
if [ "$ARTIFACTS_ENABLED" -eq 1 ]; then
DAYTONA_CREATE_ARGS+=(--volume "${DAYTONA_ARTIFACTS_VOLUME_ID}:${DAYTONA_ARTIFACTS_MOUNT}")
fi
daytona create "${DAYTONA_CREATE_ARGS[@]}"
echo ""
echo "==> Waiting for sandbox exec readiness..."
exec_ready=0
for _ in $(seq 1 60); do
if daytona exec "$SANDBOX" -- "bash -lc 'true'" >/dev/null 2>&1; then
exec_ready=1
break
fi
sleep 5
done
if [ "$exec_ready" -ne 1 ]; then
echo "ERROR: sandbox did not become exec-ready" >&2
exit 1
fi
if [ "$ARTIFACTS_ENABLED" -eq 1 ]; then
echo ""
echo "==> Starting artifacts download server..."
daytona exec "$SANDBOX" -- "bash -lc 'set -euo pipefail; mkdir -p \"$DAYTONA_ARTIFACTS_MOUNT/recordings\" \"$DAYTONA_ARTIFACTS_MOUNT/screenshots\" \"$DAYTONA_ARTIFACTS_MOUNT/validation\"; nohup python3 -m http.server $ARTIFACTS_PORT --directory \"$DAYTONA_ARTIFACTS_MOUNT\" >/tmp/daytona-artifacts.log 2>&1 &'"
fi
echo ""
echo "==> Checking out $REF..."
daytona exec "$SANDBOX" -- "bash -lc 'set -euo pipefail; cd /workspace; REF=\"$REF\"; FORCE_INSTALL=\"$FORCE_INSTALL\"; PNPM_STORE=/workspace/.openwork-daytona/pnpm-store; if git fetch origin \"\$REF\"; then git checkout --detach FETCH_HEAD; else git fetch origin dev --depth 50 || true; git checkout \"\$REF\"; fi; git rev-parse --short HEAD; mkdir -p \"\$PNPM_STORE\" .openwork-daytona; baseline=.openwork-daytona/pnpm-lock.sha256; current=\$(sha256sum pnpm-lock.yaml | cut -d \" \" -f 1); if [ \"\$FORCE_INSTALL\" = 1 ] || [ ! -d node_modules ] || [ ! -f \"\$baseline\" ] || [ \"\$(cat \"\$baseline\")\" != \"\$current\" ]; then echo \"==> Installing deps (missing node_modules, lockfile changed, or forced)...\"; CI=1 pnpm install --store-dir \"\$PNPM_STORE\" --frozen-lockfile || CI=1 pnpm install --store-dir \"\$PNPM_STORE\"; printf \"%s\" \"\$current\" > \"\$baseline\"; else echo \"==> Skipping pnpm install (node_modules present and lockfile unchanged).\"; fi'"
echo ""
echo "==> Starting OpenWork sandbox dev stack..."
daytona exec "$SANDBOX" -- "bash -lc 'set -euo pipefail; cd /workspace; DEN_BASE_URL=\"$DEN_BASE_URL\"; DEN_API_BASE_URL=\"$DEN_API_BASE_URL\"; DEN_REQUIRE_SIGNIN=\"$DEN_REQUIRE_SIGNIN\"; if [ -n \"\$DEN_BASE_URL\" ] || [ -n \"\$DEN_API_BASE_URL\" ] || [ \"\$DEN_REQUIRE_SIGNIN\" = 1 ]; then mkdir -p /workspace/.openwork-daytona; DEN_BASE_URL=\"\$DEN_BASE_URL\" DEN_API_BASE_URL=\"\$DEN_API_BASE_URL\" DEN_REQUIRE_SIGNIN=\"\$DEN_REQUIRE_SIGNIN\" node -e '\''const fs = require(\"node:fs\"); const baseUrl = process.env.DEN_BASE_URL || \"https://app.openworklabs.com\"; const apiBaseUrl = process.env.DEN_API_BASE_URL || null; const requireSignin = process.env.DEN_REQUIRE_SIGNIN === \"1\"; fs.writeFileSync(\"/workspace/.openwork-daytona/desktop-bootstrap.json\", JSON.stringify({ baseUrl, apiBaseUrl, requireSignin }, null, 2) + \"\\n\");'\''; fi; export DAYTONA_SECRETS_ENV=\"$DAYTONA_SECRETS_ENV\" DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS=\"$DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS\" OPENWORK_ELECTRON_REMOTE_DEBUG_PORT=$CDP_PORT OPENWORK_ELECTRON_FAKE_MEDIA=\"$OPENWORK_ELECTRON_FAKE_MEDIA\" OPENWORK_WORKSPACE_DIR=/workspace OPENWORK_GOOGLE_WORKSPACE_ALLOW_PLAINTEXT_VAULT=1; if [ -f /workspace/.openwork-daytona/desktop-bootstrap.json ]; then export OPENWORK_DESKTOP_BOOTSTRAP_PATH=/workspace/.openwork-daytona/desktop-bootstrap.json; fi; pnpm dev:sandbox'"
echo ""
echo "==> Waiting for Electron CDP on port $CDP_PORT (up to ${MAX_WAIT}s)..."
elapsed=0
while [ "$elapsed" -lt "$MAX_WAIT" ]; do
if daytona exec "$SANDBOX" -- "bash -lc 'curl -sf http://127.0.0.1:$CDP_PORT/json/list >/dev/null 2>&1'" 2>/dev/null; then
echo " CDP ready after ${elapsed}s."
break
fi
sleep 5
elapsed=$((elapsed + 5))
printf " %ds...\r" "$elapsed"
done
if [ "$elapsed" -ge "$MAX_WAIT" ]; then
echo ""
echo "WARNING: CDP did not become ready within ${MAX_WAIT}s."
echo "Check logs:"
echo " daytona exec $SANDBOX -- 'tail -80 /tmp/start-vnc.log'"
echo " daytona exec $SANDBOX -- 'tail -80 /tmp/electron.log'"
echo " daytona exec $SANDBOX -- 'tail -80 /tmp/vite.log'"
exit 1
fi
RECORDING_PATH=""
if [ "$RECORD_VIDEO" -eq 1 ]; then
RECORDING_PATH="${DAYTONA_ARTIFACTS_MOUNT}/recordings/${RECORDING_NAME}.mp4"
echo ""
echo "==> Starting Daytona display recording..."
daytona exec "$SANDBOX" -- "bash -lc 'cd /workspace; DISPLAY=:99 .devcontainer/start-daytona-recording.sh --detach --output \"$RECORDING_PATH\" --size \"$RECORDING_SIZE\" --fps \"$RECORDING_FPS\"'"
echo " Recording is active. Begin the user-visible flow after the ready banner below."
fi
echo ""
if daytona exec "$SANDBOX" -- "bash -lc 'ps aux | grep opencode | grep -v grep'" 2>/dev/null | grep -q opencode; then
echo "==> opencode sidecar: running"
else
echo "==> opencode sidecar: not running (workspace may need creation first)"
fi
echo "==> Secrets env: ${DAYTONA_SECRETS_ENV} (file or directory, sourced if present)"
if [ -n "$DEN_BASE_URL" ]; then
echo "==> Den base URL: $DEN_BASE_URL"
fi
CDP_URL=$(daytona preview-url "$SANDBOX" -p "$CDP_PORT" 2>/dev/null | grep -v "^time=")
NOVNC_URL=$(daytona preview-url "$SANDBOX" -p "$NOVNC_PORT" 2>/dev/null | grep -v "^time=")
ARTIFACTS_URL=""
RECORDING_URL=""
if [ "$ARTIFACTS_ENABLED" -eq 1 ]; then
ARTIFACTS_URL=$(daytona preview-url "$SANDBOX" -p "$ARTIFACTS_PORT" 2>/dev/null | grep -v "^time=")
fi
if [ "$RECORD_VIDEO" -eq 1 ]; then
RECORDING_URL="${ARTIFACTS_URL%/}/recordings/${RECORDING_NAME}.mp4"
fi
echo ""
echo "============================================"
echo " Sandbox ready: $SANDBOX"
echo ""
echo " Electron CDP: $CDP_URL"
echo " noVNC visual: $NOVNC_URL"
if [ "$ARTIFACTS_ENABLED" -eq 1 ]; then
echo " Artifacts: $ARTIFACTS_URL"
echo " Screenshot:"
echo " daytona exec $SANDBOX -- 'bash .devcontainer/capture-daytona-screenshot.sh'"
fi
if [ "$RECORD_VIDEO" -eq 1 ]; then
echo " Recording: $RECORDING_URL"
echo ""
echo " Stop recording:"
echo " daytona exec $SANDBOX -- 'bash .devcontainer/stop-daytona-recording.sh'"
echo " Validate recording duration before using it as evidence."
fi
echo ""
echo " Connect browser tools:"
echo " browser_list({ browser_url: \"$CDP_URL\" })"
echo ""
echo " Cleanup:"
echo " daytona delete $SANDBOX"
echo "============================================"
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env bash
set -euo pipefail
# test-server-on-daytona.sh — one-command Daytona Den server sandbox.
#
# Usage:
# bash .devcontainer/test-server-on-daytona.sh [branch-or-commit]
# bash .devcontainer/test-server-on-daytona.sh [branch-or-commit] --force-install
# bash .devcontainer/test-server-on-daytona.sh [branch-or-commit] --seed
#
# Creates a Daytona sandbox, starts MySQL + Den API + Den Web,
# waits for health checks, and prints public preview URLs.
REF=""
FORCE_INSTALL=0
RUN_SEED=0
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Pid + random suffix so parallel invocations (multiple features/worktrees)
# never collide on the second-granularity timestamp.
SANDBOX="openwork-server-$(date +%Y%m%d-%H%M%S)-$$-$(od -An -N2 -tx2 /dev/urandom | tr -d ' ')"
DAYTONA_SERVER_SNAPSHOT="${DAYTONA_SERVER_SNAPSHOT:-openwork-server}"
DAYTONA_TARGET="${DAYTONA_TARGET:-us}"
DEN_API_PORT="${DEN_API_PORT:-8788}"
DEN_WEB_PORT="${DEN_WEB_PORT:-3005}"
MAX_WAIT="${DAYTONA_SERVER_MAX_WAIT:-240}"
DEN_GENERATED_ARTIFACT_VIEWS_ENABLED="${DEN_GENERATED_ARTIFACT_VIEWS_ENABLED:-}"
if [ -z "$DEN_GENERATED_ARTIFACT_VIEWS_ENABLED" ]; then
if [ "${OPENWORK_EVAL_GENERATED_ARTIFACT_VIEWS_E2E_TEST:-0}" = "1" ]; then
DEN_GENERATED_ARTIFACT_VIEWS_ENABLED="true"
else
DEN_GENERATED_ARTIFACT_VIEWS_ENABLED="false"
fi
fi
case "$DEN_GENERATED_ARTIFACT_VIEWS_ENABLED" in
true|false) ;;
*) echo "ERROR: DEN_GENERATED_ARTIFACT_VIEWS_ENABLED must be true or false" >&2; exit 1 ;;
esac
while [ "$#" -gt 0 ]; do
case "$1" in
--force-install)
FORCE_INSTALL=1
;;
--seed)
RUN_SEED=1
;;
--snapshot)
shift
DAYTONA_SERVER_SNAPSHOT="${1:?missing snapshot name}"
;;
--name)
shift
SANDBOX="${1:?missing sandbox name}"
;;
--help|-h)
sed -n '1,13p' "$0"
exit 0
;;
--*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
if [ -n "$REF" ]; then
echo "Unexpected extra argument: $1" >&2
exit 1
fi
REF="$1"
;;
esac
shift
done
if [ -z "$REF" ]; then
REF="$(git branch --show-current 2>/dev/null || true)"
REF="${REF:-$(git rev-parse HEAD)}"
fi
# The ref is interpolated into a remote shell assignment below, so anything
# outside git's safe alphabet (or a leading dash) is remote code execution.
case "$REF" in
""|-*|*[!A-Za-z0-9._/-]*)
echo "ERROR: unsafe ref '$REF' — only letters, digits and . _ / - are allowed." >&2
exit 1
;;
esac
snapshot_id() {
daytona snapshot list -f json | node -e 'const name = process.argv[1]; let input = ""; process.stdin.on("data", (chunk) => input += chunk); process.stdin.on("end", () => { const snapshot = JSON.parse(input).find((item) => item.name === name); if (snapshot) process.stdout.write(snapshot.id || snapshot.name); });' "$1"
}
SNAPSHOT_ID="$(snapshot_id "$DAYTONA_SERVER_SNAPSHOT")"
CREATE_ARGS=(--name "$SANDBOX" --auto-stop 60 --public --target "$DAYTONA_TARGET")
if [ -n "$SNAPSHOT_ID" ]; then
echo "==> Using Daytona server snapshot: $DAYTONA_SERVER_SNAPSHOT"
CREATE_ARGS+=(--snapshot "$SNAPSHOT_ID")
else
echo "==> Daytona server snapshot '$DAYTONA_SERVER_SNAPSHOT' not found; building from Dockerfile."
CREATE_ARGS+=(--dockerfile "$ROOT_DIR/.devcontainer/Dockerfile.daytona-server" --cpu 4 --memory 8 --disk 10)
fi
echo "==> Creating server sandbox: $SANDBOX"
echo " Ref: $REF"
daytona create "${CREATE_ARGS[@]}"
echo "==> Waiting for sandbox exec readiness..."
exec_ready=0
for _ in $(seq 1 60); do
if daytona exec "$SANDBOX" -- "bash -lc 'true'" >/dev/null 2>&1; then
exec_ready=1
break
fi
sleep 5
done
if [ "$exec_ready" -ne 1 ]; then
echo "ERROR: sandbox did not become exec-ready" >&2
exit 1
fi
DEN_WEB_URL="$(daytona preview-url "$SANDBOX" -p "$DEN_WEB_PORT" --expires 86400 2>/dev/null | grep -v "^time=")"
DEN_API_URL="$(daytona preview-url "$SANDBOX" -p "$DEN_API_PORT" --expires 86400 2>/dev/null | grep -v "^time=")"
# These exact URLs become the Den's public identity (OAuth issuer + MCP
# resource). Every `daytona preview-url` call signs a fresh hostname, so a
# caller that re-derives them later gets a *different* host and RFC 9728
# validating MCP clients (opencode) refuse the mismatched resource. Hand the
# baked URLs to the caller through a trusted runner-side file instead; this
# write happens on the runner from daytona CLI output only, so sandbox (ref
# controlled) output can never influence it.
if [ -n "${OPENWORK_DEN_URLS_FILE:-}" ]; then
printf 'DEN_WEB_URL=%s\nDEN_API_URL=%s\n' \
"$DEN_WEB_URL" "$DEN_API_URL" > "$OPENWORK_DEN_URLS_FILE"
fi
echo "==> Checking out $REF..."
daytona exec "$SANDBOX" -- "bash -lc 'set -euo pipefail; cd /workspace; REF=\"$REF\"; FORCE_INSTALL=\"$FORCE_INSTALL\"; git reset --hard HEAD; if git fetch origin \"\$REF\"; then git checkout --detach FETCH_HEAD; else git fetch origin dev --depth 50 || true; git checkout \"\$REF\"; fi; git rev-parse --short HEAD; if [ \"\$FORCE_INSTALL\" = 1 ]; then rm -f .openwork-daytona/pnpm-lock.sha256 .openwork-daytona/den-web-build.tree .openwork-daytona/den-api-assets.tree; fi'"
echo "==> Uploading server start script..."
START_SCRIPT_B64="$(base64 < "$ROOT_DIR/.devcontainer/start-daytona-server.sh" | tr -d '\n')"
daytona exec "$SANDBOX" -- "bash -lc 'set -euo pipefail; cd /workspace; mkdir -p .devcontainer; printf %s $START_SCRIPT_B64 | base64 -d > .devcontainer/start-daytona-server.sh; chmod +x .devcontainer/start-daytona-server.sh'"
echo "==> Starting OpenWork Den server stack..."
BOOTSTRAP_ADMIN_EMAILS_B64="$(printf %s "${DEN_BOOTSTRAP_ADMIN_EMAILS:-}" | base64 | tr -d '\n')"
daytona exec "$SANDBOX" -- "bash -lc 'set -euo pipefail; cd /workspace; DEN_BOOTSTRAP_ADMIN_EMAILS=\"\$(printf %s $BOOTSTRAP_ADMIN_EMAILS_B64 | base64 -d)\" DEN_GENERATED_ARTIFACT_VIEWS_ENABLED=\"$DEN_GENERATED_ARTIFACT_VIEWS_ENABLED\" DEN_WEB_PUBLIC_URL=\"$DEN_WEB_URL\" DEN_API_PUBLIC_URL=\"$DEN_API_URL\" DEN_WEB_PORT=$DEN_WEB_PORT DEN_API_PORT=$DEN_API_PORT RUN_SEED=$RUN_SEED bash .devcontainer/start-daytona-server.sh'"
echo "==> Waiting for public Den Web health (up to ${MAX_WAIT}s)..."
elapsed=0
while [ "$elapsed" -lt "$MAX_WAIT" ]; do
if curl -sf "$DEN_WEB_URL/api/den/health" >/dev/null 2>&1; then
echo " Den Web ready after ${elapsed}s."
break
fi
sleep 5
elapsed=$((elapsed + 5))
printf " %ds...\r" "$elapsed"
done
if [ "$elapsed" -ge "$MAX_WAIT" ]; then
echo ""
echo "WARNING: Den Web did not become ready within ${MAX_WAIT}s." >&2
echo "Check logs:" >&2
echo " daytona exec $SANDBOX -- 'tail -120 /tmp/den-api.log'" >&2
echo " daytona exec $SANDBOX -- 'tail -120 /tmp/den-web.log'" >&2
exit 1
fi
echo ""
echo "============================================"
echo " Server sandbox ready: $SANDBOX"
echo ""
echo " Den Web: $DEN_WEB_URL"
echo " Den API: $DEN_API_URL"
echo ""
echo " Start Electron against this server:"
echo " bash .devcontainer/test-on-daytona.sh $REF --den-base-url $DEN_WEB_URL --den-api-base-url $DEN_API_URL"
echo ""
echo " Cleanup:"
echo " daytona delete $SANDBOX"
echo "============================================"