diff --git a/.dockerignore b/.dockerignore index 333d6134f2..d4441fb599 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,14 @@ .git .github .opencode +.codenomad node_modules **/node_modules tmp dist **/dist +artifacts +apps/desktop/src-tauri/target +**/target .env .env.* diff --git a/ee/apps/den-api/package.json b/ee/apps/den-api/package.json index 830ec53628..23d53a5b7e 100644 --- a/ee/apps/den-api/package.json +++ b/ee/apps/den-api/package.json @@ -9,7 +9,7 @@ "build:email": "pnpm --filter @openwork/email build", "build:den-db": "pnpm --filter @openwork-ee/den-db build", "backfill:desktop-policies": "pnpm run build:den-db && tsx scripts/backfill-desktop-policies.ts", - "seed:demo-org": "pnpm run build:den-db && sh -lc 'DEN_WEB_PORT=${DEN_WEB_PORT:-3005}; OPENWORK_DEV_MODE=${OPENWORK_DEV_MODE:-1} DATABASE_URL=${DATABASE_URL:-mysql://root:password@127.0.0.1:3306/openwork_den} DEN_DB_ENCRYPTION_KEY=${DEN_DB_ENCRYPTION_KEY:-local-dev-db-encryption-key-please-change-1234567890} BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-local-dev-secret-not-for-production-use!!} BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:$DEN_WEB_PORT} tsx scripts/seed-demo-org.ts'", + "seed:demo-org": "pnpm run build:den-db && node --import tsx scripts/seed-demo-org-runner.mjs", "start": "node dist/server.js" }, "dependencies": { @@ -38,12 +38,12 @@ "nanoid": "^5.1.11", "openapi-types": "^12.1.3", "stripe": "^22.1.1", + "tsx": "^4.15.7", "zod": "^4.3.6" }, "devDependencies": { "@types/json-schema": "^7.0.15", "@types/node": "^20.11.30", - "tsx": "^4.15.7", "typescript": "^5.5.4" } } diff --git a/ee/apps/den-api/scripts/seed-demo-org-runner.mjs b/ee/apps/den-api/scripts/seed-demo-org-runner.mjs new file mode 100644 index 0000000000..96037bc953 --- /dev/null +++ b/ee/apps/den-api/scripts/seed-demo-org-runner.mjs @@ -0,0 +1,9 @@ +const denWebPort = process.env.DEN_WEB_PORT?.trim() || "3005" + +process.env.OPENWORK_DEV_MODE ??= "1" +process.env.DATABASE_URL ??= "mysql://root:password@127.0.0.1:3306/openwork_den" +process.env.DEN_DB_ENCRYPTION_KEY ??= "local-dev-db-encryption-key-please-change-1234567890" +process.env.BETTER_AUTH_SECRET ??= "local-dev-secret-not-for-production-use!!" +process.env.BETTER_AUTH_URL ??= `http://localhost:${denWebPort}` + +await import("./seed-demo-org.ts") diff --git a/ee/apps/den-api/scripts/seed-demo-org.ts b/ee/apps/den-api/scripts/seed-demo-org.ts index e611cad669..47d2c0249b 100644 --- a/ee/apps/den-api/scripts/seed-demo-org.ts +++ b/ee/apps/den-api/scripts/seed-demo-org.ts @@ -1085,7 +1085,8 @@ async function main() { log("✓", `done in ${elapsedSeconds}s`) log(" ", `${memberIdsByEmail.size} members · ${teamIdsByName.size} teams · ${seededPlugins} plugins · ${seededObjects} config objects`) console.log() - log("→", `login: ${DEMO_OWNER_EMAIL} / ${DEMO_OWNER_PASSWORD}`) + log("→", `login email: ${DEMO_OWNER_EMAIL}`) + log("→", "login password: use the DEN_DEMO_OWNER_PASSWORD value supplied to this seed run") log("→", "open: /organization or /dashboard") console.log() } diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 62df069914..650c03534e 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -1,6 +1,11 @@ FROM node:22-bookworm-slim -ARG OPENWORK_ORCHESTRATOR_VERSION=0.11.22 +ARG TARGETARCH +ARG BUN_VERSION=1.3.8 +ARG BUN_DOWNLOAD_URL= +ARG BUN_SHA256= + +WORKDIR /repo RUN apt-get update \ && apt-get install -y --no-install-recommends \ @@ -9,9 +14,63 @@ RUN apt-get update \ git \ tar \ unzip \ + && npm install -g pnpm@11.4.0 \ + && case "${TARGETARCH:-amd64}" in \ + amd64) bun_artifact=bun-linux-x64.zip; bun_sha=0322b17f0722da76a64298aad498225aedcbf6df1008a1dee45e16ecb226a3f1 ;; \ + arm64) bun_artifact=bun-linux-aarch64.zip; bun_sha=4e9deb6814a7ec7f68725ddd97d0d7b4065bcda9a850f69d497567e995a7fa33 ;; \ + *) echo "Unsupported Bun TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && bun_url="${BUN_DOWNLOAD_URL:-https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${bun_artifact}}" \ + && bun_sha="${BUN_SHA256:-$bun_sha}" \ + && curl -fsSLo "/tmp/${bun_artifact}" "$bun_url" \ + && echo "$bun_sha /tmp/${bun_artifact}" | sha256sum -c - \ + && unzip -q "/tmp/${bun_artifact}" -d /tmp \ + && install -m 0755 "/tmp/${bun_artifact%.zip}/bun" /usr/local/bin/bun \ + && bun --version | grep -qx "$BUN_VERSION" \ + && rm -rf "/tmp/${bun_artifact%.zip}" "/tmp/${bun_artifact}" \ && rm -rf /var/lib/apt/lists/* -RUN npm install -g "openwork-orchestrator@${OPENWORK_ORCHESTRATOR_VERSION}" +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +RUN mkdir -p \ + apps/app \ + apps/desktop \ + apps/opencode-router \ + apps/orchestrator \ + apps/server \ + apps/ui-demo \ + ee/apps/den-api \ + ee/apps/den-web \ + ee/apps/den-worker-proxy \ + ee/apps/inference \ + ee/apps/landing \ + ee/packages/den-db \ + ee/packages/utils \ + packages/email \ + packages/handsfree \ + packages/openwork-ui-mcp \ + packages/types \ + packages/ui +COPY apps/app/package.json apps/app/package.json +COPY apps/desktop/package.json apps/desktop/package.json +COPY apps/opencode-router/package.json apps/opencode-router/package.json +COPY apps/orchestrator/package.json apps/orchestrator/package.json +COPY apps/server/package.json apps/server/package.json +COPY apps/ui-demo/package.json apps/ui-demo/package.json +COPY ee/apps/den-api/package.json ee/apps/den-api/package.json +COPY ee/apps/den-web/package.json ee/apps/den-web/package.json +COPY ee/apps/den-worker-proxy/package.json ee/apps/den-worker-proxy/package.json +COPY ee/apps/inference/package.json ee/apps/inference/package.json +COPY ee/apps/landing/package.json ee/apps/landing/package.json +COPY ee/packages/den-db/package.json ee/packages/den-db/package.json +COPY ee/packages/utils/package.json ee/packages/utils/package.json +COPY packages/email/package.json packages/email/package.json +COPY packages/handsfree/package.json packages/handsfree/package.json +COPY packages/openwork-ui-mcp/package.json packages/openwork-ui-mcp/package.json +COPY packages/types/package.json packages/types/package.json +COPY packages/ui/package.json packages/ui/package.json +RUN pnpm install --frozen-lockfile + +COPY . . # Persistent directories (mount volumes here on PaaS/SSH). ENV OPENWORK_DATA_DIR=/data/openwork-orchestrator @@ -33,16 +92,8 @@ VOLUME ["/workspace", "/data"] # - OpenCode stays internal (127.0.0.1:4096) # - OpenWork server proxies OpenCode via localhost # - OpenCode Router disabled by default -CMD [ - "openwork", - "serve", - "--workspace", "/workspace", - "--remote-access", - "--openwork-port", "8787", - "--opencode-host", "127.0.0.1", - "--opencode-port", "4096", - "--connect-host", "127.0.0.1", - "--cors", "*", - "--approval", "manual", - "--no-opencode-router" +CMD [ \ + "sh", \ + "-lc", \ + "TOKEN_FILE=/data/openwork-worker.env; ENV_OPENWORK_TOKEN=\"${OPENWORK_TOKEN:-}\"; ENV_OPENWORK_HOST_TOKEN=\"${OPENWORK_HOST_TOKEN:-}\"; FILE_OPENWORK_TOKEN=; FILE_OPENWORK_HOST_TOKEN=; if [ -f \"$TOKEN_FILE\" ]; then . \"$TOKEN_FILE\"; FILE_OPENWORK_TOKEN=\"${OPENWORK_TOKEN:-}\"; FILE_OPENWORK_HOST_TOKEN=\"${OPENWORK_HOST_TOKEN:-}\"; fi; OPENWORK_TOKEN=\"${ENV_OPENWORK_TOKEN:-$FILE_OPENWORK_TOKEN}\"; OPENWORK_HOST_TOKEN=\"${ENV_OPENWORK_HOST_TOKEN:-$FILE_OPENWORK_HOST_TOKEN}\"; GENERATED_TOKEN=0; if [ -z \"$OPENWORK_TOKEN\" ]; then OPENWORK_TOKEN=owc_$(node -e \"console.log(require('crypto').randomBytes(32).toString('base64url'))\"); FILE_OPENWORK_TOKEN=\"$OPENWORK_TOKEN\"; GENERATED_TOKEN=1; fi; if [ -z \"$OPENWORK_HOST_TOKEN\" ]; then OPENWORK_HOST_TOKEN=owh_$(node -e \"console.log(require('crypto').randomBytes(32).toString('base64url'))\"); FILE_OPENWORK_HOST_TOKEN=\"$OPENWORK_HOST_TOKEN\"; GENERATED_TOKEN=1; fi; export OPENWORK_TOKEN OPENWORK_HOST_TOKEN; if [ \"$GENERATED_TOKEN\" = \"1\" ] || [ ! -f \"$TOKEN_FILE\" ]; then umask 077; { if [ -z \"$ENV_OPENWORK_TOKEN\" ]; then printf 'OPENWORK_TOKEN=%s\\n' \"$FILE_OPENWORK_TOKEN\"; fi; if [ -z \"$ENV_OPENWORK_HOST_TOKEN\" ]; then printf 'OPENWORK_HOST_TOKEN=%s\\n' \"$FILE_OPENWORK_HOST_TOKEN\"; fi; } > \"$TOKEN_FILE\"; fi; if [ -n \"$ENV_OPENWORK_TOKEN$ENV_OPENWORK_HOST_TOKEN\" ]; then echo \"OpenWork worker env-supplied tokens are active and were not persisted\"; else echo \"OpenWork worker fallback tokens are stored in $TOKEN_FILE\"; fi; OPENWORK_CORS_ORIGINS=\"${OPENWORK_CORS_ORIGINS:-http://localhost:${OPENWORK_PORT:-8787},http://127.0.0.1:${OPENWORK_PORT:-8787}}\"; exec bun apps/orchestrator/src/cli.ts serve --workspace /workspace --remote-access --openwork-port \"${OPENWORK_PORT:-8787}\" --opencode-host 127.0.0.1 --opencode-port 4096 --connect-host \"${OPENWORK_CONNECT_HOST:-127.0.0.1}\" --cors \"$OPENWORK_CORS_ORIGINS\" --approval \"${OPENWORK_APPROVAL_MODE:-manual}\" --no-opencode-router" \ ] diff --git a/packaging/docker/Dockerfile.den b/packaging/docker/Dockerfile.den index 733e0d546e..4bd4b801c5 100644 --- a/packaging/docker/Dockerfile.den +++ b/packaging/docker/Dockerfile.den @@ -5,8 +5,8 @@ RUN corepack enable WORKDIR /app COPY package.json pnpm-lock.yaml pnpm-workspace.yaml /app/ -COPY .npmrc /app/.npmrc COPY patches /app/patches +COPY packages/email/package.json /app/packages/email/package.json COPY packages/types/package.json /app/packages/types/package.json COPY ee/packages/utils/package.json /app/ee/packages/utils/package.json COPY ee/packages/den-db/package.json /app/ee/packages/den-db/package.json @@ -14,7 +14,9 @@ COPY ee/apps/den-api/package.json /app/ee/apps/den-api/package.json RUN pnpm install --frozen-lockfile +COPY packages/email /app/packages/email COPY packages/types /app/packages/types +COPY apps/desktop/package.json /app/apps/desktop/package.json COPY ee/packages/utils /app/ee/packages/utils COPY ee/packages/den-db /app/ee/packages/den-db COPY ee/apps/den-api /app/ee/apps/den-api diff --git a/packaging/docker/Dockerfile.den-web b/packaging/docker/Dockerfile.den-web index c9ff26e374..9aa1ef93d7 100644 --- a/packaging/docker/Dockerfile.den-web +++ b/packaging/docker/Dockerfile.den-web @@ -5,7 +5,6 @@ RUN corepack enable WORKDIR /app COPY package.json pnpm-lock.yaml pnpm-workspace.yaml /app/ -COPY .npmrc /app/.npmrc COPY patches /app/patches COPY packages/types/package.json /app/packages/types/package.json COPY packages/ui/package.json /app/packages/ui/package.json diff --git a/packaging/docker/Dockerfile.den-worker-proxy b/packaging/docker/Dockerfile.den-worker-proxy index 671312e70e..00a2e9fdea 100644 --- a/packaging/docker/Dockerfile.den-worker-proxy +++ b/packaging/docker/Dockerfile.den-worker-proxy @@ -5,7 +5,6 @@ RUN corepack enable WORKDIR /app COPY package.json pnpm-lock.yaml pnpm-workspace.yaml /app/ -COPY .npmrc /app/.npmrc COPY patches /app/patches COPY packages/types/package.json /app/packages/types/package.json COPY ee/packages/utils/package.json /app/ee/packages/utils/package.json diff --git a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md new file mode 100644 index 0000000000..d7deeb89aa --- /dev/null +++ b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md @@ -0,0 +1,211 @@ +# On-prem Den static runbook + +Use this runbook to deploy Den on your own network when the worker runtimes already exist and Den should allocate them from a fixed pool. + +This runbook uses: + +- `packaging/docker/docker-compose.yml` for the worker runtime +- `packaging/docker/docker-compose.den-static.yml` for the Den stack + +In `static` mode: + +- you start and manage the OpenWork worker runtimes yourself +- Den allocates a free worker URL from `DEN_STATIC_WORKER_URLS` +- Den verifies worker health and the configured token pair before marking a worker `healthy` +- Den does not create Docker containers, worker VMs, or worker hosts for you + +The Den web URL is the normal browser-facing entrypoint. The Den API should remain internal to the Den host unless you intentionally expose it to another trusted client. +The production static Compose file publishes only the Den web port by default; MySQL, the Den API, and the worker-proxy are reachable on the Compose network for dependent services but are not bound to host ports unless an operator adds an explicit override. + +Use HTTPS for the browser-facing Den web URL whenever possible. If you intentionally use HTTP on a private LAN, use that exact HTTP origin consistently for `DEN_WEB_ORIGIN`, `DEN_BETTER_AUTH_URL`, trusted origins, and any client configuration. + +## Prerequisites + +- One host for Den and one or more hosts for OpenWork workers. These can be separate machines or the same machine if your deployment model allows it. +- Docker Engine with Compose v2 on the Den host and on every worker host. +- The OpenWork repository checkout or a release artifact on each target host. Use the exact source checkout or artifact you intend to deploy. +- A browser-facing Den web URL, for example `https://den.company.local`. +- One stable URL per worker runtime, for example `http://worker-01.company.local:8787`. +- A persistent workspace directory for each worker, mounted at `/workspace` inside the worker container. +- A persistent data directory for each worker, mounted at `/data` inside the worker container. +- One `OPENWORK_TOKEN` and one `OPENWORK_HOST_TOKEN` for each worker runtime. +- One `DEN_BETTER_AUTH_SECRET` for Den. +- One `DEN_DB_ENCRYPTION_KEY` for Den. +- One `DEN_STATIC_WORKER_TOKEN_MAP_JSON` that maps each worker URL to its `clientToken` and `hostToken`. +- Network reachability from: + - browsers to the Den web URL + - Den to every worker URL on port `8787` +- A production email provider for normal sign-up, invitation, and verification flows. +- `DEN_MYSQL_ROOT_PASSWORD` for the Den database container. + +All commands below assume the OpenWork repository root is available on the target host. Replace `/path/to/openwork` with the real path on your host. + +## Inputs To Collect + +Prepare these values before launching anything: + +- `DEN_WEB_ORIGIN`, for example `https://den.company.local` +- One worker URL per runtime, for example `http://worker-01.company.local:8787` +- One workspace path per worker +- One data path per worker +- `OPENWORK_TOKEN` per worker +- `OPENWORK_HOST_TOKEN` per worker +- `DEN_BETTER_AUTH_SECRET` +- `DEN_DB_ENCRYPTION_KEY` +- `DEN_MYSQL_ROOT_PASSWORD` +- `DEN_STATIC_WORKER_URLS`, containing all worker URLs as a comma-separated list +- `DEN_STATIC_WORKER_TOKEN_MAP_JSON`, containing the token pair for each worker URL + +The browser-facing Den web URL must match the value used for `DEN_BETTER_AUTH_URL`. If users open Den at `https://den.company.local`, then `DEN_BETTER_AUTH_URL` must be exactly `https://den.company.local`. If you intentionally use HTTP on a private LAN, then use that exact HTTP origin consistently. + +## Start One Worker + +Run this on the worker host from `packaging/docker`: + +```bash +cd /path/to/openwork/packaging/docker +export OPENWORK_HOST_PORT=8787 +export OPENWORK_CONNECT_HOST=worker-01.company.local +export OPENWORK_WORKSPACE_DIR=/srv/openwork/worker-01/workspace +export OPENWORK_DATA_DIR_HOST=/srv/openwork/worker-01/data +export OPENWORK_TOKEN='' +export OPENWORK_HOST_TOKEN='' +export OPENWORK_CORS_ORIGINS=https://den.company.local +docker compose -p openwork-worker-1 up --build -d +docker compose -p openwork-worker-1 ps +curl http://worker-01.company.local:8787/health +curl -H "Authorization: Bearer $OPENWORK_TOKEN" http://worker-01.company.local:8787/workspaces +curl -H "X-OpenWork-Host-Token: $OPENWORK_HOST_TOKEN" http://worker-01.company.local:8787/env/keys +``` + +Expected result: + +- the container is running +- `curl` returns HTTP 200 JSON from the OpenWork server +- `/workspaces` returns at least one selectable workspace for the configured client token +- `/env/keys` returns HTTP 200 for the configured host token + +This worker URL is what Den will later use in `DEN_STATIC_WORKER_URLS`. + +## Start Additional Workers + +If you need more than one worker runtime, repeat the worker launch with: + +- a different Compose project name +- a different worker URL or host port +- a different workspace path +- a different data path +- a different `OPENWORK_TOKEN` +- a different `OPENWORK_HOST_TOKEN` + +On separate hosts, you can keep the container port at `8787` on each host and vary only the hostname, for example: + +- `http://worker-01.company.local:8787` +- `http://worker-02.company.local:8787` + +If multiple workers share one host, use a unique host port per worker and a unique Compose project per worker. + +## Start Den In Static Mode + +Run this on the Den host from the repository root. + +Export the variables and run `docker compose` in the same shell session. + +In Bash, export `DEN_STATIC_WORKER_TOKEN_MAP_JSON` as a single-quoted JSON string so it reaches the container unchanged. + +Keep these `DEN_*` exports in the current shell until you finish `docker compose ps`, `logs`, and the health checks below. If you open a new shell, re-export the same values before running follow-up Compose commands. + +```bash +cd /path/to/openwork +export DEN_WEB_ORIGIN=https://den.company.local +export DEN_PROVISIONER_MODE=static +export DEN_BETTER_AUTH_URL=$DEN_WEB_ORIGIN +export DEN_BETTER_AUTH_TRUSTED_ORIGINS=$DEN_WEB_ORIGIN +export DEN_CORS_ORIGINS=$DEN_WEB_ORIGIN +export DEN_STATIC_WORKER_URLS=http://worker-01.company.local:8787,http://worker-02.company.local:8787 +export DEN_STATIC_WORKER_HEALTH_PATH=/health +export DEN_STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS=10000 +# Optional: only needed if operators will use the admin static-attach fallback with LAN/private URLs. +export DEN_STATIC_WORKER_ATTACH_ALLOW_PRIVATE=false +export DEN_STATIC_WORKER_ATTACH_ALLOWED_HOSTS= +export DEN_STATIC_WORKER_ATTACH_ALLOWED_CIDRS= +export DEN_BETTER_AUTH_SECRET='' +export DEN_DB_ENCRYPTION_KEY='' +export DEN_MYSQL_ROOT_PASSWORD='' +# DATABASE_URL is separate from DEN_MYSQL_ROOT_PASSWORD so URL-special password characters can be percent-encoded. +# Example for password p@ss:word: mysql://root:p%40ss%3Aword@mysql:3306/openwork_den +export DEN_DATABASE_URL='mysql://root:@mysql:3306/openwork_den' +export DEN_EMAIL_FROM='OpenWork Den ' +export DEN_STATIC_WORKER_TOKEN_MAP_JSON='{"http://worker-01.company.local:8787":{"clientToken":"","hostToken":""},"http://worker-02.company.local:8787":{"clientToken":"","hostToken":""}}' +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-static.yml up --build -d +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-static.yml ps +``` + +`DEN_STATIC_WORKER_URLS` is the pool of worker runtimes that Den can allocate. + +`DEN_STATIC_WORKER_TOKEN_MAP_JSON` must contain one entry for every worker URL that Den is allowed to attach as a shared worker. + +When worker containers receive `OPENWORK_TOKEN` and `OPENWORK_HOST_TOKEN` from environment variables or a secret manager, those supplied values remain runtime-only and are not persisted by the image. Only generated fallback token values are written to `/data/openwork-worker.env`, and that fallback should be used only for development or an operator-approved bootstrap. + +`DEN_STATIC_WORKER_ATTACH_ALLOW_PRIVATE=true` allows admin static-attach requests for LAN/private URLs. Prefer `DEN_STATIC_WORKER_ATTACH_ALLOWED_HOSTS` or `DEN_STATIC_WORKER_ATTACH_ALLOWED_CIDRS` when you only need to allow specific internal workers. + +## Verify Deployment + +Run these checks after the worker and Den are up: + +```bash +curl http://worker-01.company.local:8787/health +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-static.yml ps +curl $DEN_WEB_ORIGIN/api/den/health +``` + +Expected result: + +- the worker health endpoint returns HTTP 200 JSON +- the `den` and `web` services are `healthy` in `docker compose ps` +- the Den web health endpoint returns HTTP 200 JSON + +At this point, the deployment is up. + +## First Use + +Open the Den web URL in a browser: + +- `https://` + +Create the first account and complete email verification. + +Configure SMTP or Resend before the first real sign-up so the verification email is delivered normally. + +After the first account is verified, create the first organization. + +When the first organization is created in `static` mode and `DEN_STATIC_WORKER_URLS` is not empty, Den automatically attempts to create one default shared/static worker for that organization. + +Expected behavior: + +- Den picks the first free worker URL from `DEN_STATIC_WORKER_URLS` +- Den calls `/health` on that worker URL +- Den verifies the configured client token against `/workspaces` using `Authorization: Bearer ` +- Den verifies the configured host token against `/env/keys` using `X-OpenWork-Host-Token: ` +- Den marks the worker `healthy` only after the runtime contract succeeds + +You can also add another shared worker from the Den UI later. In `static` mode, the UI can allocate a free worker URL from the pre-provisioned pool, but it cannot create a new runtime worker. + +If you need more capacity than the remaining free URLs: + +1. start another runtime worker +2. add its URL to `DEN_STATIC_WORKER_URLS` +3. add its token pair to `DEN_STATIC_WORKER_TOKEN_MAP_JSON` +4. restart Den + +## Minimal Troubleshooting + +- `Invalid origin` during sign-up or email verification: + - `DEN_BETTER_AUTH_URL`, `DEN_BETTER_AUTH_TRUSTED_ORIGINS`, and `DEN_CORS_ORIGINS` do not match the real browser-facing Den URL +- worker stays `Starting` or becomes `failed`: + - run `curl http://:8787/health` + - inspect Den logs with `docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-static.yml logs den` +- `No available static worker URL remains`: + - every URL in `DEN_STATIC_WORKER_URLS` is already in use, so add another pre-running worker runtime and restart Den +- `STATIC_WORKER_TOKEN_MAP_JSON must be valid JSON`: + - export it as a single-quoted JSON string in Bash diff --git a/packaging/docker/README.md b/packaging/docker/README.md index 52afe42c46..77e56e6e38 100644 --- a/packaging/docker/README.md +++ b/packaging/docker/README.md @@ -1,5 +1,7 @@ # OpenWork Host (Docker) +For production LAN/on-prem Den deployments, start with [`ONPREM_DEN_STATIC_RUNBOOK.md`](./ONPREM_DEN_STATIC_RUNBOOK.md). It covers Den `PROVISIONER_MODE=static`, operator-managed worker secrets, real OpenWork worker containers, multiple worker URLs, validation, cleanup, decommissioning, and troubleshooting. + ## Den local stack (Docker) One command for the Den control plane, local MySQL, and the cloud web app. @@ -62,8 +64,51 @@ Optional env vars (via `.env` or `export`): - `DEN_MCP_RESOURCE_URL` — API-facing MCP resource URL (defaults to `http://localhost:/mcp`) - `DEN_BETTER_AUTH_TRUSTED_ORIGINS` — trusted origins for Better Auth (defaults to `DEN_CORS_ORIGINS`) - `DEN_CORS_ORIGINS` — trusted origins for Express CORS (defaults include hostname, localhost, `127.0.0.1`, `0.0.0.0`, and detected LAN IPv4) -- `DEN_PROVISIONER_MODE` — `stub` or `render` (defaults to `stub`) +- `DEN_PROVISIONER_MODE` — `stub`, `static`, `render`, or `daytona` (defaults to `stub`) - `DEN_WORKER_URL_TEMPLATE` — stub worker URL template with `{workerId}` placeholder +- `DEN_STATIC_WORKER_URLS` — comma-separated LAN/local OpenWork worker URLs used when `DEN_PROVISIONER_MODE=static`; each URL is assigned to at most one active static worker instance +- `DEN_STATIC_WORKER_TOKEN_MAP_JSON` — JSON map of each static worker URL to `{ "clientToken": "...", "hostToken": "..." }`; required in static mode so Den validates `/workspaces` and `/env/keys` before marking workers healthy +- `DEN_STATIC_WORKER_HEALTH_PATH` — health path checked for static workers (defaults to `/health`) +- `DEN_STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS` — static worker health timeout (defaults to `10000`) + +### On-prem/static worker mode + +Use static mode when Den is self-hosted on a LAN and workers are already running on local infrastructure. Den does not launch those workers; it assigns one configured URL that is not already used by an active static `worker_instance` to each cloud/shared worker request, checks the worker health endpoint, records a `worker_instance`, and marks the worker `healthy` only when the endpoint responds successfully. + +The real worker container path is the production container in this directory (`Dockerfile` + `docker-compose.yml`). The image builds the worker from the source checkout used as the Docker build context; use an approved checkout or release artifact for the version you intend to support. + +Run Den against a real LAN worker: + +```bash +export DEN_PROVISIONER_MODE=static +export DEN_STATIC_WORKER_URLS=http://192.168.1.50:8787 +export DEN_STATIC_WORKER_TOKEN_MAP_JSON='{"http://192.168.1.50:8787":{"clientToken":"","hostToken":""}}' +./packaging/docker/den-dev-up.sh +``` + +`DEN_STATIC_WORKER_TOKEN_MAP_JSON` is required for real LAN workers. The URL keys must exactly match `DEN_STATIC_WORKER_URLS` after trimming trailing slashes, and the values must contain the worker's client token for `/workspaces` plus host token for `/env/keys`. Without this map Den must fail the static reservation instead of marking an unreachable or unauthenticated worker healthy. + +If you need a non-production compose-only smoke test before wiring a real OpenWork runtime, start the bundled health-only worker simulation: + +```bash +export DEN_PROVISIONER_MODE=static +export DEN_STATIC_WORKER_URLS=http://static-worker-smoke:8787 +export DEN_STATIC_WORKER_TOKEN_MAP_JSON='{"http://static-worker-smoke:8787":{"clientToken":"static-smoke-client-token","hostToken":"static-smoke-host-token"}}' +docker compose --profile static-worker-smoke -p openwork-den-static \ + -f packaging/docker/docker-compose.den-dev.yml up --build +``` + +Validate the sample endpoint from the host: + +```bash +curl http://127.0.0.1:${DEN_STATIC_WORKER_SMOKE_PORT:-8787}/health +curl -H "Authorization: Bearer static-smoke-client-token" http://127.0.0.1:${DEN_STATIC_WORKER_SMOKE_PORT:-8787}/workspaces +curl -H "X-OpenWork-Host-Token: static-smoke-host-token" http://127.0.0.1:${DEN_STATIC_WORKER_SMOKE_PORT:-8787}/env/keys +``` + +Then create a cloud/shared worker in the Den web UI. With a reachable static worker URL, the worker should move from `provisioning` to `healthy` and show a `static` instance. If `DEN_STATIC_WORKER_URLS` is empty or the health check fails, Den marks the worker `failed` and logs a clear provisioning error instead of leaving it stuck on `Starting`. + +The `static-worker-smoke` service is intentionally only a provisioning-contract simulation. It serves `/health`, `/workspaces`, and `/env/keys` with coherent smoke-test tokens so Den static provisioning validates token mapping, but it is not a production OpenWork runtime and will not satisfy session APIs. ### Faster inner-loop alternative @@ -144,7 +189,7 @@ Useful overrides: ## Production container -This is a minimal packaging template to run the OpenWork Host contract in a single container. +This is a minimal packaging template to run the OpenWork Host contract in a single container. In Den static deployments, each instance of this container is a real worker URL for `DEN_STATIC_WORKER_URLS`. It runs: @@ -163,22 +208,56 @@ Then open: - `http://127.0.0.1:8787/ui` +For LAN use, set a stable host port and connect host before launch: + +```bash +OPENWORK_HOST_PORT=8787 \ +OPENWORK_CONNECT_HOST=192.168.1.50 \ +OPENWORK_WORKSPACE_DIR=./workspace-worker-1 \ +OPENWORK_DATA_DIR_HOST=./data-worker-1 \ +docker compose -p openwork-worker-1 up --build -d +``` + +On Windows PowerShell: + +```powershell +$env:OPENWORK_HOST_PORT = "8787" +$env:OPENWORK_CONNECT_HOST = "192.168.1.50" +$env:OPENWORK_WORKSPACE_DIR = "./workspace-worker-1" +$env:OPENWORK_DATA_DIR_HOST = "./data-worker-1" +docker compose -p openwork-worker-1 up --build -d +``` + +Validate the worker before adding it to Den: + +```bash +curl http://192.168.1.50:8787/health +``` + +For production, set `OPENWORK_TOKEN` and `OPENWORK_HOST_TOKEN` from a secret manager or equivalent secure operator channel. Env/secret-manager supplied tokens take precedence over `/data/openwork-worker.env` and are not written back to disk by the container. If a token is unset, the image can generate a stable per-worker fallback token and persist only generated fallback values in `/data/openwork-worker.env`; use that fallback path only for development or an operator-approved bootstrap. Treat `/data/openwork-worker.env` as sensitive bearer-secret material. + ### Config -Recommended env vars: +Required secret inputs for production secret management: - `OPENWORK_TOKEN` (client token) - `OPENWORK_HOST_TOKEN` (host/owner token) +- `OPENWORK_CORS_ORIGINS` set to the exact Den/app browser origins that may call this worker. Do not use `*` with bearer or `X-OpenWork-Host-Token` traffic in production. Optional: +- `OPENWORK_HOST_PORT=8787` (host port mapped to container port 8787) +- `OPENWORK_CONNECT_HOST=` (host embedded in pairing/connect output) +- `OPENWORK_WORKSPACE_DIR=./workspace-worker-1` (host workspace mount) +- `OPENWORK_DATA_DIR_HOST=./data-worker-1` (host data mount) - `OPENWORK_APPROVAL_MODE=auto|manual` - `OPENWORK_APPROVAL_TIMEOUT_MS=30000` +- `OPENWORK_CORS_ORIGINS=http://localhost:8787,http://127.0.0.1:8787` (local-safe default; override for production origins) Persistence: - Workspace is mounted at `/workspace` -- Host data dir is mounted at `/data` (OpenCode caches + OpenWork server config/tokens) +- Host data dir is mounted at `/data` (persistent sidecar and OpenWork server state, including fallback tokens when generated) ### Notes diff --git a/packaging/docker/den-dev-up.sh b/packaging/docker/den-dev-up.sh index ff95c7986a..01b4f7e93f 100755 --- a/packaging/docker/den-dev-up.sh +++ b/packaging/docker/den-dev-up.sh @@ -241,6 +241,11 @@ if ! DEN_API_PORT="$DEN_API_PORT" \ DEN_BETTER_AUTH_TRUSTED_ORIGINS="$DEN_BETTER_AUTH_TRUSTED_ORIGINS" \ DEN_PROVISIONER_MODE="$DEN_PROVISIONER_MODE" \ DEN_WORKER_URL_TEMPLATE="$DEN_WORKER_URL_TEMPLATE" \ + DEN_STATIC_WORKER_URLS="${DEN_STATIC_WORKER_URLS:-}" \ + DEN_STATIC_WORKER_TOKEN_MAP_JSON="${DEN_STATIC_WORKER_TOKEN_MAP_JSON:-}" \ + DEN_STATIC_WORKER_HEALTH_PATH="${DEN_STATIC_WORKER_HEALTH_PATH:-}" \ + DEN_STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS="${DEN_STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS:-}" \ + DEN_STATIC_WORKER_HEALTHCHECK_INTERVAL_MS="${DEN_STATIC_WORKER_HEALTHCHECK_INTERVAL_MS:-}" \ DEN_DAYTONA_WORKER_PROXY_BASE_URL="$DEN_DAYTONA_WORKER_PROXY_BASE_URL" \ DAYTONA_API_URL="${DAYTONA_API_URL:-}" \ DAYTONA_API_KEY="${DAYTONA_API_KEY:-}" \ diff --git a/packaging/docker/docker-compose.den-dev.yml b/packaging/docker/docker-compose.den-dev.yml index 9b0b89caeb..be7cf3cf4a 100644 --- a/packaging/docker/docker-compose.den-dev.yml +++ b/packaging/docker/docker-compose.den-dev.yml @@ -18,8 +18,21 @@ # DEN_BETTER_AUTH_URL — browser-facing auth origin (default: http://:) # DEN_BETTER_AUTH_TRUSTED_ORIGINS — Better Auth trusted origins (defaults to DEN_CORS_ORIGINS) # DEN_CORS_ORIGINS — comma-separated trusted origins for Better Auth + CORS -# DEN_PROVISIONER_MODE — stub, render, or daytona (default: stub) +# DEN_PROVISIONER_MODE — stub, static, render, or daytona (default: stub) # DEN_WORKER_URL_TEMPLATE — worker URL template used by stub provisioning +# DEN_STATIC_WORKER_URLS — comma-separated LAN/OpenWork worker URLs for static mode +# — each URL is assigned to at most one active worker_instance +# — for the smoke-test service below, use http://static-worker-smoke:8787 +# DEN_STATIC_WORKER_TOKEN_MAP_JSON +# — JSON map of static worker URLs to {clientToken,hostToken}; required for static mode +# DEN_STATIC_WORKER_HEALTH_PATH / DEN_STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS +# — optional static worker health-check overrides +# DEN_ENTRA_TENANT_ID / DEN_ENTRA_CLIENT_ID / DEN_ENTRA_CLIENT_SECRET +# — optional Microsoft Entra ID SSO provider config +# DEN_ENTRA_AUTO_JOIN_ENABLED / DEN_ENTRA_AUTO_JOIN_ORG_ID +# — optional SSO organization auto-join target +# DEN_ENTRA_ADMIN_GROUP_IDS / DEN_ENTRA_MEMBER_GROUP_IDS +# — comma-separated Entra group object IDs mapped to roles # DAYTONA_API_URL / DAYTONA_API_KEY / DAYTONA_TARGET / DAYTONA_SNAPSHOT # — optional Daytona passthrough vars when DEN_PROVISIONER_MODE=daytona # POLAR_FEATURE_GATE_ENABLED / POLAR_API_BASE / POLAR_ACCESS_TOKEN @@ -81,6 +94,26 @@ services: CORS_ORIGINS: ${DEN_CORS_ORIGINS:-http://localhost:3005,http://127.0.0.1:3005,http://0.0.0.0:3005,http://localhost:5173,http://127.0.0.1:5173,http://localhost:8788,http://127.0.0.1:8788} PROVISIONER_MODE: ${DEN_PROVISIONER_MODE:-stub} WORKER_URL_TEMPLATE: ${DEN_WORKER_URL_TEMPLATE:-} + STATIC_WORKER_URLS: ${DEN_STATIC_WORKER_URLS:-} + STATIC_WORKER_TOKEN_MAP_JSON: ${DEN_STATIC_WORKER_TOKEN_MAP_JSON:-} + STATIC_WORKER_HEALTH_PATH: ${DEN_STATIC_WORKER_HEALTH_PATH:-/health} + STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS: ${DEN_STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS:-10000} + STATIC_WORKER_HEALTHCHECK_INTERVAL_MS: ${DEN_STATIC_WORKER_HEALTHCHECK_INTERVAL_MS:-1000} + DEN_ENTRA_TENANT_ID: ${DEN_ENTRA_TENANT_ID:-} + DEN_ENTRA_CLIENT_ID: ${DEN_ENTRA_CLIENT_ID:-} + DEN_ENTRA_CLIENT_SECRET: ${DEN_ENTRA_CLIENT_SECRET:-} + DEN_ENTRA_AUTO_JOIN_ENABLED: ${DEN_ENTRA_AUTO_JOIN_ENABLED:-false} + DEN_ENTRA_AUTO_JOIN_ORG_ID: ${DEN_ENTRA_AUTO_JOIN_ORG_ID:-} + DEN_ENTRA_AUTO_JOIN_ORG_SLUG: ${DEN_ENTRA_AUTO_JOIN_ORG_SLUG:-} + DEN_ENTRA_ADMIN_GROUP_IDS: ${DEN_ENTRA_ADMIN_GROUP_IDS:-} + DEN_ENTRA_MEMBER_GROUP_IDS: ${DEN_ENTRA_MEMBER_GROUP_IDS:-} + EMAIL_FROM: ${DEN_EMAIL_FROM:-OpenWork Den } + RESEND_API_KEY: ${DEN_RESEND_API_KEY:-} + SMTP_HOST: ${DEN_SMTP_HOST:-} + SMTP_PORT: ${DEN_SMTP_PORT:-} + SMTP_USER: ${DEN_SMTP_USER:-} + SMTP_PASS: ${DEN_SMTP_PASS:-} + SMTP_SECURE: ${DEN_SMTP_SECURE:-false} POLAR_FEATURE_GATE_ENABLED: ${POLAR_FEATURE_GATE_ENABLED:-false} POLAR_API_BASE: ${POLAR_API_BASE:-} POLAR_ACCESS_TOKEN: ${POLAR_ACCESS_TOKEN:-} @@ -146,5 +179,55 @@ services: DEN_AUTH_ORIGIN: ${DEN_BETTER_AUTH_URL:-http://localhost:3005} NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL: ${DEN_BETTER_AUTH_URL:-http://localhost:3005} + static-worker-smoke: + <<: *shared + image: node:20-alpine + profiles: ["static-worker-smoke"] + command: + - node + - -e + - | + const clientToken = process.env.OPENWORK_TOKEN || 'static-smoke-client-token'; + const hostToken = process.env.OPENWORK_HOST_TOKEN || 'static-smoke-host-token'; + const json = (res, status, body) => { + res.writeHead(status, {'content-type':'application/json'}); + res.end(JSON.stringify(body)); + }; + require('http').createServer((req,res)=>{ + if (req.url === '/health') { + json(res, 200, {ok:true, mode:'static-worker-smoke'}); + return; + } + if (req.url === '/workspaces') { + const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim(); + if (bearer !== clientToken) { + json(res, 401, {error:'invalid_client_token'}); + return; + } + json(res, 200, {workspaces:[{id:'static-smoke-workspace', name:'Static Smoke Workspace', path:'/workspace'}]}); + return; + } + if (req.url === '/env/keys') { + if ((req.headers['x-openwork-host-token'] || '').trim() !== hostToken) { + json(res, 401, {error:'invalid_host_token'}); + return; + } + json(res, 200, {keys:[], source:'static-worker-smoke'}); + return; + } + json(res, 404, {error:'not_found'}); + }).listen(8787, '0.0.0.0') + environment: + OPENWORK_TOKEN: ${DEN_STATIC_WORKER_SMOKE_OPENWORK_TOKEN:-static-smoke-client-token} + OPENWORK_HOST_TOKEN: ${DEN_STATIC_WORKER_SMOKE_OPENWORK_HOST_TOKEN:-static-smoke-host-token} + ports: + - "${DEN_STATIC_WORKER_SMOKE_PORT:-8787}:8787" + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8787/health').then((res)=>process.exit(res.ok?0:1)).catch(()=>process.exit(1))"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 10s + volumes: den-mysql-data: diff --git a/packaging/docker/docker-compose.den-static.yml b/packaging/docker/docker-compose.den-static.yml new file mode 100644 index 0000000000..9b08c3ceae --- /dev/null +++ b/packaging/docker/docker-compose.den-static.yml @@ -0,0 +1,152 @@ +# docker-compose.den-static.yml — Den static deployment stack +# +# Use this file for self-hosted Den deployments where worker runtimes already +# exist and Den should allocate them from a fixed pool. + +x-shared: &shared + restart: unless-stopped + +services: + mysql: + image: mysql:8.4 + command: + - --performance_schema=OFF + - --innodb-buffer-pool-size=64M + - --innodb-log-buffer-size=8M + - --tmp-table-size=16M + - --max-heap-table-size=16M + environment: + MYSQL_ROOT_PASSWORD: ${DEN_MYSQL_ROOT_PASSWORD:?DEN_MYSQL_ROOT_PASSWORD is required} + MYSQL_DATABASE: openwork_den + healthcheck: + test: ["CMD-SHELL", "MYSQL_PWD=\"$${MYSQL_ROOT_PASSWORD}\" mysqladmin ping -h 127.0.0.1 --silent"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 10s + expose: + - "3306" + volumes: + - den-mysql-data:/var/lib/mysql + + den: + <<: *shared + build: + context: ../../ + dockerfile: packaging/docker/Dockerfile.den + depends_on: + mysql: + condition: service_healthy + expose: + - "8788" + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8788/health').then((res)=>process.exit(res.ok?0:1)).catch(()=>process.exit(1))"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 120s + environment: + CI: "true" + OPENWORK_DEV_MODE: ${OPENWORK_DEV_MODE:-0} + # Provide a complete URL so passwords with URL-special characters can be percent-encoded. + # Example: mysql://root:p%40ss%3Aword@mysql:3306/openwork_den + DATABASE_URL: ${DEN_DATABASE_URL:?DEN_DATABASE_URL is required; percent-encode URL-special password characters} + BETTER_AUTH_SECRET: ${DEN_BETTER_AUTH_SECRET:?DEN_BETTER_AUTH_SECRET is required} + DEN_DB_ENCRYPTION_KEY: ${DEN_DB_ENCRYPTION_KEY:?DEN_DB_ENCRYPTION_KEY is required} + BETTER_AUTH_URL: ${DEN_BETTER_AUTH_URL:?DEN_BETTER_AUTH_URL is required} + DEN_MCP_RESOURCE_URL: ${DEN_MCP_RESOURCE_URL:-} + DEN_BETTER_AUTH_TRUSTED_ORIGINS: ${DEN_BETTER_AUTH_TRUSTED_ORIGINS:?DEN_BETTER_AUTH_TRUSTED_ORIGINS is required} + PORT: "8788" + CORS_ORIGINS: ${DEN_CORS_ORIGINS:?DEN_CORS_ORIGINS is required} + PROVISIONER_MODE: ${DEN_PROVISIONER_MODE:?DEN_PROVISIONER_MODE is required} + WORKER_URL_TEMPLATE: ${DEN_WORKER_URL_TEMPLATE:-} + STATIC_WORKER_URLS: ${DEN_STATIC_WORKER_URLS:?DEN_STATIC_WORKER_URLS is required} + STATIC_WORKER_TOKEN_MAP_JSON: ${DEN_STATIC_WORKER_TOKEN_MAP_JSON:?DEN_STATIC_WORKER_TOKEN_MAP_JSON is required} + STATIC_WORKER_HEALTH_PATH: ${DEN_STATIC_WORKER_HEALTH_PATH:-/health} + STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS: ${DEN_STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS:-10000} + STATIC_WORKER_HEALTHCHECK_INTERVAL_MS: ${DEN_STATIC_WORKER_HEALTHCHECK_INTERVAL_MS:-1000} + STATIC_WORKER_ATTACH_ALLOW_PRIVATE: ${DEN_STATIC_WORKER_ATTACH_ALLOW_PRIVATE:-false} + STATIC_WORKER_ATTACH_ALLOWED_HOSTS: ${DEN_STATIC_WORKER_ATTACH_ALLOWED_HOSTS:-} + STATIC_WORKER_ATTACH_ALLOWED_CIDRS: ${DEN_STATIC_WORKER_ATTACH_ALLOWED_CIDRS:-} + DEN_ENTRA_TENANT_ID: ${DEN_ENTRA_TENANT_ID:-} + DEN_ENTRA_CLIENT_ID: ${DEN_ENTRA_CLIENT_ID:-} + DEN_ENTRA_CLIENT_SECRET: ${DEN_ENTRA_CLIENT_SECRET:-} + DEN_ENTRA_AUTO_JOIN_ENABLED: ${DEN_ENTRA_AUTO_JOIN_ENABLED:-false} + DEN_ENTRA_AUTO_JOIN_ORG_ID: ${DEN_ENTRA_AUTO_JOIN_ORG_ID:-} + DEN_ENTRA_AUTO_JOIN_ORG_SLUG: ${DEN_ENTRA_AUTO_JOIN_ORG_SLUG:-} + DEN_ENTRA_ADMIN_GROUP_IDS: ${DEN_ENTRA_ADMIN_GROUP_IDS:-} + DEN_ENTRA_MEMBER_GROUP_IDS: ${DEN_ENTRA_MEMBER_GROUP_IDS:-} + EMAIL_FROM: ${DEN_EMAIL_FROM:?DEN_EMAIL_FROM is required} + RESEND_API_KEY: ${DEN_RESEND_API_KEY:-} + SMTP_HOST: ${DEN_SMTP_HOST:-} + SMTP_PORT: ${DEN_SMTP_PORT:-} + SMTP_USER: ${DEN_SMTP_USER:-} + SMTP_PASS: ${DEN_SMTP_PASS:-} + SMTP_SECURE: ${DEN_SMTP_SECURE:-false} + POLAR_FEATURE_GATE_ENABLED: ${POLAR_FEATURE_GATE_ENABLED:-false} + POLAR_API_BASE: ${POLAR_API_BASE:-} + POLAR_ACCESS_TOKEN: ${POLAR_ACCESS_TOKEN:-} + POLAR_PRODUCT_ID: ${POLAR_PRODUCT_ID:-} + POLAR_BENEFIT_ID: ${POLAR_BENEFIT_ID:-} + POLAR_SUCCESS_URL: ${POLAR_SUCCESS_URL:-} + POLAR_RETURN_URL: ${POLAR_RETURN_URL:-} + DAYTONA_API_URL: ${DAYTONA_API_URL:-} + DAYTONA_API_KEY: ${DAYTONA_API_KEY:-} + DAYTONA_TARGET: ${DAYTONA_TARGET:-} + DAYTONA_SNAPSHOT: ${DAYTONA_SNAPSHOT:-} + DAYTONA_WORKER_PROXY_BASE_URL: ${DEN_DAYTONA_WORKER_PROXY_BASE_URL:-http://localhost:8789} + + worker-proxy: + <<: *shared + build: + context: ../../ + dockerfile: packaging/docker/Dockerfile.den-worker-proxy + depends_on: + mysql: + condition: service_healthy + expose: + - "8789" + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8789/unknown').then((res)=>process.exit([404,502].includes(res.status)?0:1)).catch(()=>process.exit(1))"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 90s + environment: + CI: "true" + DATABASE_URL: ${DEN_DATABASE_URL:?DEN_DATABASE_URL is required; percent-encode URL-special password characters} + PORT: "8789" + OPENWORK_DAYTONA_ENV_PATH: ${OPENWORK_DAYTONA_ENV_PATH:-} + DAYTONA_API_URL: ${DAYTONA_API_URL:-} + DAYTONA_API_KEY: ${DAYTONA_API_KEY:-} + DAYTONA_TARGET: ${DAYTONA_TARGET:-} + DAYTONA_OPENWORK_PORT: ${DAYTONA_OPENWORK_PORT:-8787} + DAYTONA_SIGNED_PREVIEW_EXPIRES_SECONDS: ${DAYTONA_SIGNED_PREVIEW_EXPIRES_SECONDS:-86400} + + web: + <<: *shared + build: + context: ../../ + dockerfile: packaging/docker/Dockerfile.den-web + command: ["sh", "-lc", "npm run build && npm run start"] + depends_on: + den: + condition: service_healthy + ports: + - "${DEN_WEB_PORT:-3005}:3005" + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3005/api/den/health').then((res)=>process.exit(res.ok?0:1)).catch(()=>process.exit(1))"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 180s + environment: + CI: "true" + OPENWORK_DEV_MODE: ${OPENWORK_DEV_MODE:-0} + DEN_API_BASE: http://den:8788 + DEN_AUTH_FALLBACK_BASE: http://den:8788 + DEN_AUTH_ORIGIN: ${DEN_BETTER_AUTH_URL:?DEN_BETTER_AUTH_URL is required} + NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL: ${DEN_BETTER_AUTH_URL:?DEN_BETTER_AUTH_URL is required} + +volumes: + den-mysql-data: diff --git a/packaging/docker/docker-compose.yml b/packaging/docker/docker-compose.yml index 5e50de5fc2..6a4ff623f4 100644 --- a/packaging/docker/docker-compose.yml +++ b/packaging/docker/docker-compose.yml @@ -1,23 +1,34 @@ services: openwork-host: build: - context: . - dockerfile: Dockerfile - args: - # Keep this in sync with apps/orchestrator/package.json if you want a pinned release. - OPENWORK_ORCHESTRATOR_VERSION: 0.11.22 + context: ../../ + dockerfile: packaging/docker/Dockerfile ports: - - "8787:8787" + - "${OPENWORK_HOST_PORT:-8787}:8787" environment: - # Set these explicitly for stable sharing. - # OPENWORK_TOKEN: "..." - # OPENWORK_HOST_TOKEN: "..." + # Optional. If unset, the image generates stable per-worker tokens in /data/openwork-worker.env. + OPENWORK_TOKEN: ${OPENWORK_TOKEN:-} + OPENWORK_HOST_TOKEN: ${OPENWORK_HOST_TOKEN:-} + # Set to the LAN IP/DNS name clients and Den should use for this worker. + OPENWORK_CONNECT_HOST: ${OPENWORK_CONNECT_HOST:-127.0.0.1} # Optional: OPENWORK_APPROVAL_MODE: "auto" # Optional: OPENWORK_APPROVAL_TIMEOUT_MS: "30000" + OPENWORK_APPROVAL_MODE: ${OPENWORK_APPROVAL_MODE:-manual} + OPENWORK_APPROVAL_TIMEOUT_MS: ${OPENWORK_APPROVAL_TIMEOUT_MS:-30000} + # Set this to the exact browser/app origins that may call this worker. + # Wildcard CORS with bearer/host-token headers is intentionally not the default. + OPENWORK_CORS_ORIGINS: "${OPENWORK_CORS_ORIGINS:-http://localhost:8787,http://127.0.0.1:8787}" + OPENWORK_PORT: 8787 OPENWORK_DATA_DIR: /data/openwork-orchestrator OPENWORK_SIDECAR_DIR: /data/sidecars volumes: # Mount an existing project/workspace here. - - ./workspace:/workspace + - ${OPENWORK_WORKSPACE_DIR:-./workspace}:/workspace # Persistent host data (OpenCode caches, server config, tokens). - - ./data:/data + - ${OPENWORK_DATA_DIR_HOST:-./data}:/data + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8787/health').then((res)=>process.exit(res.ok?0:1)).catch(()=>process.exit(1))"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 60s diff --git a/packaging/docker/microsandbox-entrypoint.sh b/packaging/docker/microsandbox-entrypoint.sh index 40045d37a0..bec0f5a436 100755 --- a/packaging/docker/microsandbox-entrypoint.sh +++ b/packaging/docker/microsandbox-entrypoint.sh @@ -9,7 +9,7 @@ OPENWORK_OPENCODE_PORT="${OPENWORK_OPENCODE_PORT:-4096}" OPENWORK_TOKEN="${OPENWORK_TOKEN:-microsandbox-token}" OPENWORK_HOST_TOKEN="${OPENWORK_HOST_TOKEN:-microsandbox-host-token}" OPENWORK_APPROVAL_MODE="${OPENWORK_APPROVAL_MODE:-auto}" -OPENWORK_CORS_ORIGINS="${OPENWORK_CORS_ORIGINS:-*}" +OPENWORK_CORS_ORIGINS="${OPENWORK_CORS_ORIGINS:-http://localhost:$OPENWORK_PORT,http://127.0.0.1:$OPENWORK_PORT}" OPENWORK_CONNECT_HOST="${OPENWORK_CONNECT_HOST:-127.0.0.1}" HOME="${HOME:-/root}" USER="${USER:-root}" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5220e340ec..ea76469350 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -480,6 +480,9 @@ importers: stripe: specifier: ^22.1.1 version: 22.1.1(@types/node@20.12.12) + tsx: + specifier: ^4.15.7 + version: 4.21.0 zod: specifier: ^4.3.6 version: 4.3.6 @@ -490,9 +493,6 @@ importers: '@types/node': specifier: ^20.11.30 version: 20.12.12 - tsx: - specifier: ^4.15.7 - version: 4.21.0 typescript: specifier: ^5.5.4 version: 5.9.3