源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Executable
+63
@@ -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"
|
||||
@@ -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
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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."
|
||||
Executable
+86
@@ -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
|
||||
Executable
+96
@@ -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"
|
||||
Executable
+308
@@ -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 "============================================"
|
||||
Executable
+41
@@ -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}"
|
||||
Executable
+86
@@ -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
|
||||
Executable
+65
@@ -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
|
||||
Executable
+305
@@ -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 "============================================"
|
||||
Executable
+179
@@ -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 "============================================"
|
||||
Reference in New Issue
Block a user