From 1304e7d1750cb46b27b69c41087485121f5b25f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Fri, 22 May 2026 09:21:11 +0200 Subject: [PATCH 01/13] docs(docker): clean up on-prem runbooks --- .dockerignore | 4 + packaging/docker/Dockerfile | 27 +- packaging/docker/Dockerfile.den | 3 + packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md | 408 ++++++++++++++++++ packaging/docker/README.md | 78 +++- packaging/docker/docker-compose.den-dev.yml | 58 ++- packaging/docker/docker-compose.yml | 31 +- 7 files changed, 578 insertions(+), 31 deletions(-) create mode 100644 packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md 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/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 62df069914..eb9a5e21c8 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -1,17 +1,22 @@ -FROM node:22-bookworm-slim +FROM oven/bun:1.2.20 -ARG OPENWORK_ORCHESTRATOR_VERSION=0.11.22 +WORKDIR /repo RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ git \ + nodejs \ + npm \ tar \ unzip \ + && npm install -g pnpm@10.27.0 \ && rm -rf /var/lib/apt/lists/* -RUN npm install -g "openwork-orchestrator@${OPENWORK_ORCHESTRATOR_VERSION}" +COPY . . + +RUN pnpm install --frozen-lockfile # Persistent directories (mount volumes here on PaaS/SSH). ENV OPENWORK_DATA_DIR=/data/openwork-orchestrator @@ -33,16 +38,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; if [ -f \"$TOKEN_FILE\" ]; then . \"$TOKEN_FILE\"; fi; if [ -z \"${OPENWORK_TOKEN:-}\" ]; then OPENWORK_TOKEN=owc_$(node -e \"console.log(require('crypto').randomBytes(32).toString('base64url'))\"); export OPENWORK_TOKEN; fi; if [ -z \"${OPENWORK_HOST_TOKEN:-}\" ]; then OPENWORK_HOST_TOKEN=owh_$(node -e \"console.log(require('crypto').randomBytes(32).toString('base64url'))\"); export OPENWORK_HOST_TOKEN; fi; umask 077; printf 'OPENWORK_TOKEN=%s\\nOPENWORK_HOST_TOKEN=%s\\n' \"$OPENWORK_TOKEN\" \"$OPENWORK_HOST_TOKEN\" > \"$TOKEN_FILE\"; echo \"OpenWork worker tokens are stored in $TOKEN_FILE\"; 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..ed6178658d 100644 --- a/packaging/docker/Dockerfile.den +++ b/packaging/docker/Dockerfile.den @@ -7,6 +7,7 @@ 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 +15,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/ONPREM_DEN_STATIC_RUNBOOK.md b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md new file mode 100644 index 0000000000..0645acd3c4 --- /dev/null +++ b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md @@ -0,0 +1,408 @@ +# On-prem Den static worker production runbook + +This runbook describes an operator-supported LAN/on-prem deployment where Den is the control plane and one or more pre-running OpenWork Host containers are the worker runtimes. Den does not create worker containers in `static` mode; it assigns each shared/cloud worker request to a healthy URL from `DEN_STATIC_WORKER_URLS`. + +Semaphore is used below only as an example managed deployment tool. Equivalent MDM, configuration management, or release automation systems can apply the same inputs. + +## Components and ports + +- Den API/control plane: `http://:8788` +- Den web UI: `http://:3005` +- OpenWork worker: `http://:8787` + +The production worker image is built from `packaging/docker/Dockerfile`. For source deployments, the Dockerfile builds the `openwork` orchestrator binary from the checked-out repository so approved server/runtime fixes are included in the worker image. + +## Prerequisites + +- Docker Engine with Compose v2 on each Den and worker host. +- CPU features compatible with the bundled sidecar runtime, including AVX-capable CPU flags exposed to the worker host. +- DNS, routing, and firewall rules that allow Den to reach each worker on port `8787` and browsers to reach Den web on port `3005`. +- A persistent workspace directory per worker, mounted at `/workspace` inside the worker container. +- A persistent data directory per worker, mounted at `/data` inside the worker container. +- Operator-managed worker bearer tokens. Production deployments should set `OPENWORK_TOKEN` and `OPENWORK_HOST_TOKEN` from a secret manager or equivalent secure configuration channel. +- A production email provider for sign-up, invitation, and verification email delivery. Configure `DEN_SMTP_*` or `DEN_RESEND_API_KEY` plus `DEN_EMAIL_FROM` before relying on user-facing email flows. + +Generated worker tokens are a fallback for development or an operator-approved bootstrap only. When token generation is used, `/data/openwork-worker.env` contains sensitive bearer secrets and must be protected like any other secret file. + +All commands below assume the repository root is available on the target host. Adjust paths to match your release checkout or deployment artifact location. + +## 1. Plan worker identity and secrets + +For each worker, record these operator-owned inputs before launch: + +- Stable worker URL, for example `http://worker-01.company.local:8787`. +- Host port, usually `8787` per worker host. +- Workspace mount path. +- Data mount path. +- `OPENWORK_TOKEN` client bearer token from the secret manager. +- `OPENWORK_HOST_TOKEN` host/admin bearer token from the secret manager. + +Use unique tokens per worker unless your security policy explicitly requires a different rotation model. Store the tokens outside source control and inject them at container launch time through the platform's secret mechanism. + +## 2. Start one OpenWork worker + +PowerShell: + +```powershell +Set-Location D:\openwork\packaging\docker +$env:OPENWORK_HOST_PORT = "8787" +$env:OPENWORK_CONNECT_HOST = "worker-01.company.local" +$env:OPENWORK_WORKSPACE_DIR = "D:\openwork-data\worker-01\workspace" +$env:OPENWORK_DATA_DIR_HOST = "D:\openwork-data\worker-01\data" +$env:OPENWORK_TOKEN = "" +$env:OPENWORK_HOST_TOKEN = "" +docker compose -p openwork-worker-1 up --build -d +docker compose -p openwork-worker-1 ps +curl.exe http://worker-01.company.local:8787/health +``` + +Bash: + +```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='' +docker compose -p openwork-worker-1 up --build -d +docker compose -p openwork-worker-1 ps +curl http://worker-01.company.local:8787/health +``` + +Expected health result: HTTP 200 JSON from the OpenWork server. This URL is what Den should receive for the worker. + +If an operator deliberately uses generated bootstrap tokens, retrieve them only through an approved secure access path and immediately store/rotate them according to site policy. Do not share `/data/openwork-worker.env`; it is sensitive because it contains bearer credentials. + +## 3. Start multiple OpenWork workers + +Run each worker with a unique Compose project, host port, workspace/data directory, token pair, and URL. Example for two workers on one host: + +PowerShell: + +```powershell +Set-Location D:\openwork\packaging\docker + +$env:OPENWORK_HOST_PORT = "8787" +$env:OPENWORK_CONNECT_HOST = "worker-host.company.local" +$env:OPENWORK_WORKSPACE_DIR = "D:\openwork-data\worker-01\workspace" +$env:OPENWORK_DATA_DIR_HOST = "D:\openwork-data\worker-01\data" +$env:OPENWORK_TOKEN = "" +$env:OPENWORK_HOST_TOKEN = "" +docker compose -p openwork-worker-1 up --build -d + +$env:OPENWORK_HOST_PORT = "8788" +$env:OPENWORK_CONNECT_HOST = "worker-host.company.local" +$env:OPENWORK_WORKSPACE_DIR = "D:\openwork-data\worker-02\workspace" +$env:OPENWORK_DATA_DIR_HOST = "D:\openwork-data\worker-02\data" +$env:OPENWORK_TOKEN = "" +$env:OPENWORK_HOST_TOKEN = "" +docker compose -p openwork-worker-2 up --build -d + +curl.exe http://worker-host.company.local:8787/health +curl.exe http://worker-host.company.local:8788/health +``` + +Bash: + +```bash +cd /path/to/openwork/packaging/docker + +OPENWORK_HOST_PORT=8787 OPENWORK_CONNECT_HOST=worker-host.company.local \ +OPENWORK_WORKSPACE_DIR=/srv/openwork/worker-01/workspace \ +OPENWORK_DATA_DIR_HOST=/srv/openwork/worker-01/data \ +OPENWORK_TOKEN='' \ +OPENWORK_HOST_TOKEN='' \ + docker compose -p openwork-worker-1 up --build -d + +OPENWORK_HOST_PORT=8788 OPENWORK_CONNECT_HOST=worker-host.company.local \ +OPENWORK_WORKSPACE_DIR=/srv/openwork/worker-02/workspace \ +OPENWORK_DATA_DIR_HOST=/srv/openwork/worker-02/data \ +OPENWORK_TOKEN='' \ +OPENWORK_HOST_TOKEN='' \ + docker compose -p openwork-worker-2 up --build -d + +curl http://worker-host.company.local:8787/health +curl http://worker-host.company.local:8788/health +``` + +For separate hosts, keep the container port at `8787` on each host and use each host's DNS name in Den, for example `http://worker-01.company.local:8787,http://worker-02.company.local:8787`. + +## 4. Start Den in static mode + +PowerShell: + +```powershell +Set-Location D:\openwork +$env:DEN_PROVISIONER_MODE = "static" +$env:DEN_STATIC_WORKER_URLS = "http://worker-01.company.local:8787,http://worker-02.company.local:8787" +$env:DEN_STATIC_WORKER_HEALTH_PATH = "/health" +$env:DEN_STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS = "10000" +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml up --build -d +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml ps +curl.exe http://127.0.0.1:8788/health +curl.exe http://127.0.0.1:3005/api/den/health +``` + +Bash: + +```bash +cd /path/to/openwork +export DEN_PROVISIONER_MODE=static +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 +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml up --build -d +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml ps +curl http://127.0.0.1:8788/health +curl http://127.0.0.1:3005/api/den/health +``` + +`DEN_STATIC_WORKER_URLS` is comma-separated. Den trims trailing slashes, probes each URL's configured health path, and assigns one not-already-active static URL per worker request. + +The default Compose stack does not start a local SMTP inbox. For production, configure `DEN_SMTP_HOST`, `DEN_SMTP_PORT`, `DEN_SMTP_USER`, `DEN_SMTP_PASS`, `DEN_SMTP_SECURE`, and `DEN_EMAIL_FROM`, or configure `DEN_RESEND_API_KEY` plus `DEN_EMAIL_FROM`. + +## 5. Managed desktop deployment + +The desktop installer is generic. Do not rebuild the desktop for each Den, network, customer, or site. A managed deployment tool should install the same desktop build and provide the Den endpoint as managed configuration during deployment. + +Recommended Windows managed config file: + +```text +%ProgramData%\OpenWork\desktop-bootstrap.json +``` + +Desktop bootstrap precedence is deterministic: `OPENWORK_DESKTOP_BOOTSTRAP_PATH` when explicitly set by an operator, then the machine-wide managed config above, then user/developer config, then build defaults. Managed enterprise deployments should use the machine-wide ProgramData file rather than per-user files. + +Example contents: + +```json +{ + "baseUrl": "http://den.company.local:3005", + "apiBaseUrl": "http://den.company.local:3005/api/den", + "requireSignin": true +} +``` + +The Den URL can be a DNS name or LAN IP that is valid for that site. Prefer DNS, for example `http://den.company.local:3005`, so workers and clients can move without changing every desktop profile. + +Example Semaphore deployment steps for a Windows client machine: + +```powershell +$installer = "C:\Deploy\openwork-win-x64-0.13.11.exe" +$configDir = "$env:ProgramData\OpenWork" +$denHost = "http://den.company.local:3005" + +New-Item -ItemType Directory -Force -Path $configDir | Out-Null +@{ + baseUrl = $denHost + apiBaseUrl = "$denHost/api/den" + requireSignin = $true +} | ConvertTo-Json | Set-Content -Encoding UTF8 "$configDir\desktop-bootstrap.json" + +Start-Process -FilePath $installer -ArgumentList "/S" -Wait +``` + +After installation, the user opens OpenWork and signs in to the configured Den. Worker credentials remain operator-managed secrets; end users should not handle worker bearer tokens or select infrastructure endpoints during normal sign-in. + +## 6. Configure Microsoft Entra ID SSO for on-prem Den + +Email/password sign-in stays enabled for break-glass administrator access. Microsoft Entra ID SSO is enabled only when all provider variables below are present. + +This batch does not implement a first-admin/org bootstrap flow. Before enabling Entra auto-join in production, ensure a first Den administrator and target organization already exist through a supported setup path for your deployment. A dedicated first-admin/org bootstrap capability remains a future setup prerequisite. Entra auto-join only adds Microsoft users to an existing organization; it never creates the initial organization and never assigns `owner`. + +### Entra app registration + +1. In Microsoft Entra admin center, create or open an App registration for Den. +2. Add a Web redirect URI using the Den browser-facing auth origin and Better Auth callback path: + - `http://den.company.local:3005/api/auth/callback/microsoft` + - For this Compose runbook's local defaults, use `http://localhost:3005/api/auth/callback/microsoft`. +3. Create a client secret and keep it outside source control. +4. Configure ID token optional claims so Den receives `email` where available and `groups` for group object IDs. Den only reads the token `groups` claim; Microsoft Graph overage lookup is intentionally out of scope. +5. Record the fixed tenant GUID. Do not use `common`, `organizations`, or `consumers` for on-prem Den SSO. + +### Den environment variables + +Set these before starting Den: + +```bash +export DEN_BETTER_AUTH_URL=http://den.company.local:3005 +export DEN_ENTRA_TENANT_ID=00000000-0000-0000-0000-000000000000 +export DEN_ENTRA_CLIENT_ID=11111111-1111-1111-1111-111111111111 +export DEN_ENTRA_CLIENT_SECRET=replace-with-client-secret +``` + +Use HTTPS for production Den auth origins where available, for example `https://den.company.local`. When the browser-facing Den web/auth origin is an HTTP LAN address such as `http://den.company.local:3005`, use that exact origin consistently for `DEN_BETTER_AUTH_URL`, trusted origins, desktop bootstrap, and the Entra redirect URI. Plain HTTP is accepted only for localhost, loopback, private LAN IPs, or `.local` hostnames used in LAN/on-prem testing. Do not configure wildcard Better Auth trusted origins (`*`) while Entra SSO is enabled; set explicit browser-facing origins such as `DEN_BETTER_AUTH_TRUSTED_ORIGINS=http://den.company.local:3005`. + +Optional organization auto-join maps all Microsoft SSO sign-ins into one Den organization. Prefer the canonical organization ID; `DEN_ENTRA_AUTO_JOIN_ORG_SLUG` is available only as an operator convenience and must resolve to exactly one org. + +```bash +export DEN_ENTRA_AUTO_JOIN_ENABLED=true +export DEN_ENTRA_AUTO_JOIN_ORG_ID=organization_replace_me +# Optional alternative, only if no org ID is set and the slug is unambiguous: +# export DEN_ENTRA_AUTO_JOIN_ORG_SLUG=platform-team +``` + +Set exactly one selector when auto-join is enabled: either `DEN_ENTRA_AUTO_JOIN_ORG_ID` or `DEN_ENTRA_AUTO_JOIN_ORG_SLUG`, never both and never neither. + +Optional Entra group-to-role mapping uses comma-separated Entra group object IDs from the token `groups` claim: + +```bash +export DEN_ENTRA_ADMIN_GROUP_IDS=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa +export DEN_ENTRA_MEMBER_GROUP_IDS=bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb,cccccccc-cccc-cccc-cccc-cccccccccccc +``` + +Role behavior: + +- Default SSO auto-join role is `member` when no group mapping matches. +- If both admin and member groups match, `admin` wins. +- SSO never assigns `owner`; existing owner memberships are preserved. +- Token `roles` claims are ignored; only the Entra `groups` claim participates in Den role mapping. +- If Entra omits `email`, Den falls back to `preferred_username`/UPN where Better Auth's Microsoft provider profile mapping allows it. + +### Validate SSO + +1. Restart Den after setting the environment variables. +2. Confirm the Microsoft sign-in option appears on the auth screen. +3. Sign in with a tenant user and verify the callback URL is accepted by Entra. +4. Confirm the user is a member of the configured Den organization with the expected `admin` or `member` role. +5. Confirm an existing admin can still sign in with email/password. + +Troubleshooting: + +- Missing Microsoft sign-in option: confirm `DEN_ENTRA_TENANT_ID`, `DEN_ENTRA_CLIENT_ID`, and `DEN_ENTRA_CLIENT_SECRET` are all set and that tenant ID is not `common`. +- Entra callback error: confirm the redirect URI exactly matches `http://den.company.local:3005/api/auth/callback/microsoft` for the configured Den auth origin. +- User joined as `member`: confirm Entra emitted a `groups` claim and the configured group object ID matches `DEN_ENTRA_ADMIN_GROUP_IDS`. +- User not auto-joined: confirm `DEN_ENTRA_AUTO_JOIN_ENABLED=true` and `DEN_ENTRA_AUTO_JOIN_ORG_ID` points to an existing Den organization. + +## 7. Validate Den UI behavior and add a shared workspace + +1. Open `http://:3005`. +2. Sign in with an administrator account or an SSO user that has the expected organization membership. +3. Create or open the target organization/team workspace area. +4. Add a shared/cloud worker from the Den UI. +5. Expected behavior: + - The worker briefly appears as `provisioning`/`Starting`. + - Den calls `/health` on the first unassigned URL from `DEN_STATIC_WORKER_URLS`. + - The worker becomes `healthy` and its instance/provider metadata shows `static`. + - Adding another shared worker consumes the next URL in `DEN_STATIC_WORKER_URLS`. + - If every static URL is already assigned to an active worker, the new request fails with a clear "No available static worker URL remains" error. + +Use the supported Den UI/API flows for worker lifecycle actions. Do not edit database rows, token files, or browser/session state as part of normal operations. + +## 8. Compose validation before launch + +PowerShell: + +```powershell +Set-Location D:\openwork +docker compose -p openwork-den-default -f packaging/docker/docker-compose.den-dev.yml config + +$env:DEN_PROVISIONER_MODE = "static" +$env:DEN_STATIC_WORKER_URLS = "http://worker-01.company.local:8787,http://worker-02.company.local:8787" +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml config + +Set-Location D:\openwork\packaging\docker +$env:OPENWORK_HOST_PORT = "8787" +docker compose -p openwork-worker-1 config +``` + +Bash: + +```bash +cd /path/to/openwork +docker compose -p openwork-den-default -f packaging/docker/docker-compose.den-dev.yml config + +DEN_PROVISIONER_MODE=static \ +DEN_STATIC_WORKER_URLS=http://worker-01.company.local:8787,http://worker-02.company.local:8787 \ +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml config + +cd packaging/docker +OPENWORK_HOST_PORT=8787 \ +docker compose -p openwork-worker-1 config +``` + +In rendered output, confirm Den has `PROVISIONER_MODE: static` and `STATIC_WORKER_URLS` set to the real worker URLs, and the worker service is built from `packaging/docker/Dockerfile`. + +## 9. Restart, stop, cleanup, and decommission + +Restart Den after configuration changes: + +```bash +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml restart den web +``` + +Restart a worker after token rotation or host configuration changes: + +```bash +cd packaging/docker +docker compose -p openwork-worker-1 restart openwork-host +``` + +Follow logs: + +```bash +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml logs -f den web +cd packaging/docker && docker compose -p openwork-worker-1 logs -f openwork-host +``` + +Stop without deleting persistent volumes: + +```bash +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml down +cd packaging/docker && docker compose -p openwork-worker-1 down +``` + +Use destructive volume removal only for disposable/test stacks where data loss is intentional: + +```bash +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml down -v +cd packaging/docker && docker compose -p openwork-worker-1 down -v +``` + +Worker decommission checklist: + +1. Remove or disable the worker through supported Den UI/API administration flows. +2. Remove the worker URL from `DEN_STATIC_WORKER_URLS` and restart Den. +3. Stop the worker container. +4. Revoke or rotate the worker's `OPENWORK_TOKEN` and `OPENWORK_HOST_TOKEN` in the secret manager. +5. Archive or delete workspace/data directories according to retention policy. + +Test data cleanup should use supported application flows: delete test workers, organizations, users, invites, and sessions through Den UI/API/admin tooling available for the deployment. Avoid direct database edits or manual token-file changes during normal operations; reserve destructive container/volume reset for disposable environments. + +## 10. Troubleshooting + +- Worker remains `Starting`: from the Den host, run `curl http://:8787/health`. Fix DNS, routing, firewall, or the worker container before retrying. +- Worker becomes `failed`: inspect Den logs with `docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml logs den` and confirm `DEN_STATIC_WORKER_URLS` is non-empty and points to OpenWork workers. +- No static worker available: add another URL to `DEN_STATIC_WORKER_URLS`, decommission stale workers through supported admin flows, or stop requesting additional shared workers. +- Auth/CORS problems: set `DEN_PUBLIC_HOST`, `DEN_BETTER_AUTH_URL`, `DEN_BETTER_AUTH_TRUSTED_ORIGINS`, and `DEN_CORS_ORIGINS` to the Den web/API origins before starting Den. +- Wrong connect host in printed worker URLs: set `OPENWORK_CONNECT_HOST` to the worker DNS name and recreate the worker container. +- Desktop opens the wrong Den: confirm the managed deployment created `%ProgramData%\OpenWork\desktop-bootstrap.json` with the expected Den URL, then redeploy or repair through the same management channel. +- Version mismatch: rebuild and redeploy the worker image from the approved source checkout or release artifact you intend to support. + +## Appendix A. Non-production static worker smoke simulation + +Use `static-worker-smoke` only to validate Den static provisioning mechanics without a real OpenWork runtime. It is a tiny `/health` HTTP service, not an OpenWork worker, and it cannot run workspaces or sessions. + +PowerShell: + +```powershell +Set-Location D:\openwork +$env:DEN_PROVISIONER_MODE = "static" +$env:DEN_STATIC_WORKER_URLS = "http://static-worker-smoke:8787" +docker compose --profile static-worker-smoke -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml up --build -d +curl.exe http://127.0.0.1:8787/health +``` + +Bash: + +```bash +cd /path/to/openwork +DEN_PROVISIONER_MODE=static DEN_STATIC_WORKER_URLS=http://static-worker-smoke:8787 \ + docker compose --profile static-worker-smoke -p openwork-den-static \ + -f packaging/docker/docker-compose.den-dev.yml up --build -d +curl http://127.0.0.1:8787/health +``` + +Do not use `static-worker-smoke` for production or workspace/session validation. diff --git a/packaging/docker/README.md b/packaging/docker/README.md index 52afe42c46..c5f8ce2a7a 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,44 @@ 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_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 +./packaging/docker/den-dev-up.sh +``` + +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 +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 +``` + +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 health-check simulation for provisioning validation; it is not a production OpenWork runtime and will not satisfy workspace/session APIs. ### Faster inner-loop alternative @@ -144,7 +182,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 +201,54 @@ 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. If both variables are unset, the image generates stable per-worker fallback tokens and persists them in `/data/openwork-worker.env`; use that 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) 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` 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/docker-compose.den-dev.yml b/packaging/docker/docker-compose.den-dev.yml index 9b0b89caeb..88de8bdac4 100644 --- a/packaging/docker/docker-compose.den-dev.yml +++ b/packaging/docker/docker-compose.den-dev.yml @@ -18,8 +18,19 @@ # 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_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 +92,25 @@ 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_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 +176,31 @@ 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 + - | + require('http').createServer((req,res)=>{ + if (req.url === '/health') { + res.writeHead(200, {'content-type':'application/json'}); + res.end(JSON.stringify({ok:true, mode:'static-worker-smoke'})); + return; + } + res.writeHead(404); + res.end('not found'); + }).listen(8787, '0.0.0.0') + 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.yml b/packaging/docker/docker-compose.yml index 5e50de5fc2..e42a16cb3f 100644 --- a/packaging/docker/docker-compose.yml +++ b/packaging/docker/docker-compose.yml @@ -1,23 +1,32 @@ 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} + OPENWORK_CORS_ORIGINS: ${OPENWORK_CORS_ORIGINS:-*} + 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 From 928a08f7dd73256345f8abc94a63dc7aa6630c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 23 May 2026 03:35:01 +0200 Subject: [PATCH 02/13] fix(den): make demo seed script portable --- ee/apps/den-api/package.json | 4 ++-- ee/apps/den-api/scripts/seed-demo-org-runner.mjs | 9 +++++++++ ee/apps/den-api/scripts/seed-demo-org.ts | 3 ++- packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md | 14 ++++++++++++++ pnpm-lock.yaml | 6 +++--- 5 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 ee/apps/den-api/scripts/seed-demo-org-runner.mjs diff --git a/ee/apps/den-api/package.json b/ee/apps/den-api/package.json index 796c5bba42..cf5f140e1e 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": { @@ -36,12 +36,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 42c18d75aa..8b60a4a645 100644 --- a/ee/apps/den-api/scripts/seed-demo-org.ts +++ b/ee/apps/den-api/scripts/seed-demo-org.ts @@ -1037,7 +1037,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/ONPREM_DEN_STATIC_RUNBOOK.md b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md index 0645acd3c4..f6ca364f6b 100644 --- a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md +++ b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md @@ -212,6 +212,20 @@ Email/password sign-in stays enabled for break-glass administrator access. Micro This batch does not implement a first-admin/org bootstrap flow. Before enabling Entra auto-join in production, ensure a first Den administrator and target organization already exist through a supported setup path for your deployment. A dedicated first-admin/org bootstrap capability remains a future setup prerequisite. Entra auto-join only adds Microsoft users to an existing organization; it never creates the initial organization and never assigns `owner`. +For disposable E2E and operator smoke-test deployments, create a demo owner and organization with the Den seed tool instead of editing database rows manually. Run this from the Compose host after the `den` service is healthy, using a temporary password kept out of shell history where possible: + +```bash +read -rs -p "Demo owner password: " DEN_DEMO_OWNER_PASSWORD; echo +export DEN_DEMO_OWNER_PASSWORD +docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml exec \ + -e DEN_DEMO_OWNER_EMAIL=admin@acme.test \ + -e DEN_DEMO_OWNER_PASSWORD \ + -e DEN_DEMO_SEED_FETCH_GITHUB=0 \ + den pnpm --dir /app/ee/apps/den-api run seed:demo-org +``` + +The seed command prints the demo owner email, organization summary, and object counts, but does not print the supplied password. Use `-- --reset` at the end only for disposable environments when you intentionally want to recreate the demo organization. + ### Entra app registration 1. In Microsoft Entra admin center, create or open an App registration for Den. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c65f37d072..209c217332 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -541,6 +541,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 @@ -551,9 +554,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 From 95fa6818e86dfe23b6c9902ae3d4512d3848d1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Thu, 4 Jun 2026 18:17:22 +0200 Subject: [PATCH 03/13] docs(docker): simplify static deployment runbook --- packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md | 485 +++++------------- .../docker/docker-compose.den-static.yml | 147 ++++++ 2 files changed, 274 insertions(+), 358 deletions(-) create mode 100644 packaging/docker/docker-compose.den-static.yml diff --git a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md index f6ca364f6b..672e356cda 100644 --- a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md +++ b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md @@ -1,62 +1,65 @@ -# On-prem Den static worker production runbook +# On-prem Den static runbook -This runbook describes an operator-supported LAN/on-prem deployment where Den is the control plane and one or more pre-running OpenWork Host containers are the worker runtimes. Den does not create worker containers in `static` mode; it assigns each shared/cloud worker request to a healthy URL from `DEN_STATIC_WORKER_URLS`. +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. -Semaphore is used below only as an example managed deployment tool. Equivalent MDM, configuration management, or release automation systems can apply the same inputs. +This runbook uses: -## Components and ports +- `packaging/docker/docker-compose.yml` for the worker runtime +- `packaging/docker/docker-compose.den-static.yml` for the Den stack -- Den API/control plane: `http://:8788` -- Den web UI: `http://:3005` -- OpenWork worker: `http://:8787` +In `static` mode: -The production worker image is built from `packaging/docker/Dockerfile`. For source deployments, the Dockerfile builds the `openwork` orchestrator binary from the checked-out repository so approved server/runtime fixes are included in the worker image. +- 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 -## Prerequisites - -- Docker Engine with Compose v2 on each Den and worker host. -- CPU features compatible with the bundled sidecar runtime, including AVX-capable CPU flags exposed to the worker host. -- DNS, routing, and firewall rules that allow Den to reach each worker on port `8787` and browsers to reach Den web on port `3005`. -- A persistent workspace directory per worker, mounted at `/workspace` inside the worker container. -- A persistent data directory per worker, mounted at `/data` inside the worker container. -- Operator-managed worker bearer tokens. Production deployments should set `OPENWORK_TOKEN` and `OPENWORK_HOST_TOKEN` from a secret manager or equivalent secure configuration channel. -- A production email provider for sign-up, invitation, and verification email delivery. Configure `DEN_SMTP_*` or `DEN_RESEND_API_KEY` plus `DEN_EMAIL_FROM` before relying on user-facing email flows. - -Generated worker tokens are a fallback for development or an operator-approved bootstrap only. When token generation is used, `/data/openwork-worker.env` contains sensitive bearer secrets and must be protected like any other secret file. - -All commands below assume the repository root is available on the target host. Adjust paths to match your release checkout or deployment artifact location. - -## 1. Plan worker identity and secrets - -For each worker, record these operator-owned inputs before launch: - -- Stable worker URL, for example `http://worker-01.company.local:8787`. -- Host port, usually `8787` per worker host. -- Workspace mount path. -- Data mount path. -- `OPENWORK_TOKEN` client bearer token from the secret manager. -- `OPENWORK_HOST_TOKEN` host/admin bearer token from the secret manager. +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. -Use unique tokens per worker unless your security policy explicitly requires a different rotation model. Store the tokens outside source control and inject them at container launch time through the platform's secret mechanism. +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. -## 2. Start one OpenWork worker - -PowerShell: - -```powershell -Set-Location D:\openwork\packaging\docker -$env:OPENWORK_HOST_PORT = "8787" -$env:OPENWORK_CONNECT_HOST = "worker-01.company.local" -$env:OPENWORK_WORKSPACE_DIR = "D:\openwork-data\worker-01\workspace" -$env:OPENWORK_DATA_DIR_HOST = "D:\openwork-data\worker-01\data" -$env:OPENWORK_TOKEN = "" -$env:OPENWORK_HOST_TOKEN = "" -docker compose -p openwork-worker-1 up --build -d -docker compose -p openwork-worker-1 ps -curl.exe http://worker-01.company.local:8787/health -``` +## Prerequisites -Bash: +- 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 @@ -64,359 +67,125 @@ 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_TOKEN='' +export OPENWORK_HOST_TOKEN='' docker compose -p openwork-worker-1 up --build -d docker compose -p openwork-worker-1 ps curl http://worker-01.company.local:8787/health ``` -Expected health result: HTTP 200 JSON from the OpenWork server. This URL is what Den should receive for the worker. +Expected result: -If an operator deliberately uses generated bootstrap tokens, retrieve them only through an approved secure access path and immediately store/rotate them according to site policy. Do not share `/data/openwork-worker.env`; it is sensitive because it contains bearer credentials. +- the container is running +- `curl` returns HTTP 200 JSON from the OpenWork server -## 3. Start multiple OpenWork workers +This worker URL is what Den will later use in `DEN_STATIC_WORKER_URLS`. -Run each worker with a unique Compose project, host port, workspace/data directory, token pair, and URL. Example for two workers on one host: +## Start Additional Workers -PowerShell: +If you need more than one worker runtime, repeat the worker launch with: -```powershell -Set-Location D:\openwork\packaging\docker +- 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` -$env:OPENWORK_HOST_PORT = "8787" -$env:OPENWORK_CONNECT_HOST = "worker-host.company.local" -$env:OPENWORK_WORKSPACE_DIR = "D:\openwork-data\worker-01\workspace" -$env:OPENWORK_DATA_DIR_HOST = "D:\openwork-data\worker-01\data" -$env:OPENWORK_TOKEN = "" -$env:OPENWORK_HOST_TOKEN = "" -docker compose -p openwork-worker-1 up --build -d - -$env:OPENWORK_HOST_PORT = "8788" -$env:OPENWORK_CONNECT_HOST = "worker-host.company.local" -$env:OPENWORK_WORKSPACE_DIR = "D:\openwork-data\worker-02\workspace" -$env:OPENWORK_DATA_DIR_HOST = "D:\openwork-data\worker-02\data" -$env:OPENWORK_TOKEN = "" -$env:OPENWORK_HOST_TOKEN = "" -docker compose -p openwork-worker-2 up --build -d +On separate hosts, you can keep the container port at `8787` on each host and vary only the hostname, for example: -curl.exe http://worker-host.company.local:8787/health -curl.exe http://worker-host.company.local:8788/health -``` +- `http://worker-01.company.local:8787` +- `http://worker-02.company.local:8787` -Bash: +If multiple workers share one host, use a unique host port per worker and a unique Compose project per worker. -```bash -cd /path/to/openwork/packaging/docker - -OPENWORK_HOST_PORT=8787 OPENWORK_CONNECT_HOST=worker-host.company.local \ -OPENWORK_WORKSPACE_DIR=/srv/openwork/worker-01/workspace \ -OPENWORK_DATA_DIR_HOST=/srv/openwork/worker-01/data \ -OPENWORK_TOKEN='' \ -OPENWORK_HOST_TOKEN='' \ - docker compose -p openwork-worker-1 up --build -d - -OPENWORK_HOST_PORT=8788 OPENWORK_CONNECT_HOST=worker-host.company.local \ -OPENWORK_WORKSPACE_DIR=/srv/openwork/worker-02/workspace \ -OPENWORK_DATA_DIR_HOST=/srv/openwork/worker-02/data \ -OPENWORK_TOKEN='' \ -OPENWORK_HOST_TOKEN='' \ - docker compose -p openwork-worker-2 up --build -d - -curl http://worker-host.company.local:8787/health -curl http://worker-host.company.local:8788/health -``` +## Start Den In Static Mode -For separate hosts, keep the container port at `8787` on each host and use each host's DNS name in Den, for example `http://worker-01.company.local:8787,http://worker-02.company.local:8787`. +Run this on the Den host from the repository root. -## 4. Start Den in static mode +Export the variables and run `docker compose` in the same shell session. -PowerShell: - -```powershell -Set-Location D:\openwork -$env:DEN_PROVISIONER_MODE = "static" -$env:DEN_STATIC_WORKER_URLS = "http://worker-01.company.local:8787,http://worker-02.company.local:8787" -$env:DEN_STATIC_WORKER_HEALTH_PATH = "/health" -$env:DEN_STATIC_WORKER_HEALTHCHECK_TIMEOUT_MS = "10000" -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml up --build -d -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml ps -curl.exe http://127.0.0.1:8788/health -curl.exe http://127.0.0.1:3005/api/den/health -``` - -Bash: +In Bash, export `DEN_STATIC_WORKER_TOKEN_MAP_JSON` as a single-quoted JSON string so it reaches the container unchanged. ```bash cd /path/to/openwork -export DEN_PROVISIONER_MODE=static +export DEN_WEB_ORIGIN=https://den.company.local +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 -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml up --build -d -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml ps -curl http://127.0.0.1:8788/health -curl http://127.0.0.1:3005/api/den/health -``` - -`DEN_STATIC_WORKER_URLS` is comma-separated. Den trims trailing slashes, probes each URL's configured health path, and assigns one not-already-active static URL per worker request. - -The default Compose stack does not start a local SMTP inbox. For production, configure `DEN_SMTP_HOST`, `DEN_SMTP_PORT`, `DEN_SMTP_USER`, `DEN_SMTP_PASS`, `DEN_SMTP_SECURE`, and `DEN_EMAIL_FROM`, or configure `DEN_RESEND_API_KEY` plus `DEN_EMAIL_FROM`. - -## 5. Managed desktop deployment - -The desktop installer is generic. Do not rebuild the desktop for each Den, network, customer, or site. A managed deployment tool should install the same desktop build and provide the Den endpoint as managed configuration during deployment. - -Recommended Windows managed config file: - -```text -%ProgramData%\OpenWork\desktop-bootstrap.json -``` - -Desktop bootstrap precedence is deterministic: `OPENWORK_DESKTOP_BOOTSTRAP_PATH` when explicitly set by an operator, then the machine-wide managed config above, then user/developer config, then build defaults. Managed enterprise deployments should use the machine-wide ProgramData file rather than per-user files. - -Example contents: - -```json -{ - "baseUrl": "http://den.company.local:3005", - "apiBaseUrl": "http://den.company.local:3005/api/den", - "requireSignin": true -} -``` - -The Den URL can be a DNS name or LAN IP that is valid for that site. Prefer DNS, for example `http://den.company.local:3005`, so workers and clients can move without changing every desktop profile. - -Example Semaphore deployment steps for a Windows client machine: - -```powershell -$installer = "C:\Deploy\openwork-win-x64-0.13.11.exe" -$configDir = "$env:ProgramData\OpenWork" -$denHost = "http://den.company.local:3005" - -New-Item -ItemType Directory -Force -Path $configDir | Out-Null -@{ - baseUrl = $denHost - apiBaseUrl = "$denHost/api/den" - requireSignin = $true -} | ConvertTo-Json | Set-Content -Encoding UTF8 "$configDir\desktop-bootstrap.json" - -Start-Process -FilePath $installer -ArgumentList "/S" -Wait +export DEN_BETTER_AUTH_SECRET='' +export DEN_DB_ENCRYPTION_KEY='' +export DEN_MYSQL_ROOT_PASSWORD='' +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 ``` -After installation, the user opens OpenWork and signs in to the configured Den. Worker credentials remain operator-managed secrets; end users should not handle worker bearer tokens or select infrastructure endpoints during normal sign-in. - -## 6. Configure Microsoft Entra ID SSO for on-prem Den +`DEN_STATIC_WORKER_URLS` is the pool of worker runtimes that Den can allocate. -Email/password sign-in stays enabled for break-glass administrator access. Microsoft Entra ID SSO is enabled only when all provider variables below are present. +`DEN_STATIC_WORKER_TOKEN_MAP_JSON` must contain one entry for every worker URL that Den is allowed to attach as a shared worker. -This batch does not implement a first-admin/org bootstrap flow. Before enabling Entra auto-join in production, ensure a first Den administrator and target organization already exist through a supported setup path for your deployment. A dedicated first-admin/org bootstrap capability remains a future setup prerequisite. Entra auto-join only adds Microsoft users to an existing organization; it never creates the initial organization and never assigns `owner`. +## Verify Deployment -For disposable E2E and operator smoke-test deployments, create a demo owner and organization with the Den seed tool instead of editing database rows manually. Run this from the Compose host after the `den` service is healthy, using a temporary password kept out of shell history where possible: +Run these checks after the worker and Den are up: ```bash -read -rs -p "Demo owner password: " DEN_DEMO_OWNER_PASSWORD; echo -export DEN_DEMO_OWNER_PASSWORD -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml exec \ - -e DEN_DEMO_OWNER_EMAIL=admin@acme.test \ - -e DEN_DEMO_OWNER_PASSWORD \ - -e DEN_DEMO_SEED_FETCH_GITHUB=0 \ - den pnpm --dir /app/ee/apps/den-api run seed:demo-org -``` - -The seed command prints the demo owner email, organization summary, and object counts, but does not print the supplied password. Use `-- --reset` at the end only for disposable environments when you intentionally want to recreate the demo organization. - -### Entra app registration - -1. In Microsoft Entra admin center, create or open an App registration for Den. -2. Add a Web redirect URI using the Den browser-facing auth origin and Better Auth callback path: - - `http://den.company.local:3005/api/auth/callback/microsoft` - - For this Compose runbook's local defaults, use `http://localhost:3005/api/auth/callback/microsoft`. -3. Create a client secret and keep it outside source control. -4. Configure ID token optional claims so Den receives `email` where available and `groups` for group object IDs. Den only reads the token `groups` claim; Microsoft Graph overage lookup is intentionally out of scope. -5. Record the fixed tenant GUID. Do not use `common`, `organizations`, or `consumers` for on-prem Den SSO. - -### Den environment variables - -Set these before starting Den: - -```bash -export DEN_BETTER_AUTH_URL=http://den.company.local:3005 -export DEN_ENTRA_TENANT_ID=00000000-0000-0000-0000-000000000000 -export DEN_ENTRA_CLIENT_ID=11111111-1111-1111-1111-111111111111 -export DEN_ENTRA_CLIENT_SECRET=replace-with-client-secret -``` - -Use HTTPS for production Den auth origins where available, for example `https://den.company.local`. When the browser-facing Den web/auth origin is an HTTP LAN address such as `http://den.company.local:3005`, use that exact origin consistently for `DEN_BETTER_AUTH_URL`, trusted origins, desktop bootstrap, and the Entra redirect URI. Plain HTTP is accepted only for localhost, loopback, private LAN IPs, or `.local` hostnames used in LAN/on-prem testing. Do not configure wildcard Better Auth trusted origins (`*`) while Entra SSO is enabled; set explicit browser-facing origins such as `DEN_BETTER_AUTH_TRUSTED_ORIGINS=http://den.company.local:3005`. - -Optional organization auto-join maps all Microsoft SSO sign-ins into one Den organization. Prefer the canonical organization ID; `DEN_ENTRA_AUTO_JOIN_ORG_SLUG` is available only as an operator convenience and must resolve to exactly one org. - -```bash -export DEN_ENTRA_AUTO_JOIN_ENABLED=true -export DEN_ENTRA_AUTO_JOIN_ORG_ID=organization_replace_me -# Optional alternative, only if no org ID is set and the slug is unambiguous: -# export DEN_ENTRA_AUTO_JOIN_ORG_SLUG=platform-team -``` - -Set exactly one selector when auto-join is enabled: either `DEN_ENTRA_AUTO_JOIN_ORG_ID` or `DEN_ENTRA_AUTO_JOIN_ORG_SLUG`, never both and never neither. - -Optional Entra group-to-role mapping uses comma-separated Entra group object IDs from the token `groups` claim: - -```bash -export DEN_ENTRA_ADMIN_GROUP_IDS=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa -export DEN_ENTRA_MEMBER_GROUP_IDS=bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb,cccccccc-cccc-cccc-cccc-cccccccccccc -``` - -Role behavior: - -- Default SSO auto-join role is `member` when no group mapping matches. -- If both admin and member groups match, `admin` wins. -- SSO never assigns `owner`; existing owner memberships are preserved. -- Token `roles` claims are ignored; only the Entra `groups` claim participates in Den role mapping. -- If Entra omits `email`, Den falls back to `preferred_username`/UPN where Better Auth's Microsoft provider profile mapping allows it. - -### Validate SSO - -1. Restart Den after setting the environment variables. -2. Confirm the Microsoft sign-in option appears on the auth screen. -3. Sign in with a tenant user and verify the callback URL is accepted by Entra. -4. Confirm the user is a member of the configured Den organization with the expected `admin` or `member` role. -5. Confirm an existing admin can still sign in with email/password. - -Troubleshooting: - -- Missing Microsoft sign-in option: confirm `DEN_ENTRA_TENANT_ID`, `DEN_ENTRA_CLIENT_ID`, and `DEN_ENTRA_CLIENT_SECRET` are all set and that tenant ID is not `common`. -- Entra callback error: confirm the redirect URI exactly matches `http://den.company.local:3005/api/auth/callback/microsoft` for the configured Den auth origin. -- User joined as `member`: confirm Entra emitted a `groups` claim and the configured group object ID matches `DEN_ENTRA_ADMIN_GROUP_IDS`. -- User not auto-joined: confirm `DEN_ENTRA_AUTO_JOIN_ENABLED=true` and `DEN_ENTRA_AUTO_JOIN_ORG_ID` points to an existing Den organization. - -## 7. Validate Den UI behavior and add a shared workspace - -1. Open `http://:3005`. -2. Sign in with an administrator account or an SSO user that has the expected organization membership. -3. Create or open the target organization/team workspace area. -4. Add a shared/cloud worker from the Den UI. -5. Expected behavior: - - The worker briefly appears as `provisioning`/`Starting`. - - Den calls `/health` on the first unassigned URL from `DEN_STATIC_WORKER_URLS`. - - The worker becomes `healthy` and its instance/provider metadata shows `static`. - - Adding another shared worker consumes the next URL in `DEN_STATIC_WORKER_URLS`. - - If every static URL is already assigned to an active worker, the new request fails with a clear "No available static worker URL remains" error. - -Use the supported Den UI/API flows for worker lifecycle actions. Do not edit database rows, token files, or browser/session state as part of normal operations. - -## 8. Compose validation before launch - -PowerShell: - -```powershell -Set-Location D:\openwork -docker compose -p openwork-den-default -f packaging/docker/docker-compose.den-dev.yml config - -$env:DEN_PROVISIONER_MODE = "static" -$env:DEN_STATIC_WORKER_URLS = "http://worker-01.company.local:8787,http://worker-02.company.local:8787" -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml config - -Set-Location D:\openwork\packaging\docker -$env:OPENWORK_HOST_PORT = "8787" -docker compose -p openwork-worker-1 config -``` - -Bash: - -```bash -cd /path/to/openwork -docker compose -p openwork-den-default -f packaging/docker/docker-compose.den-dev.yml config - -DEN_PROVISIONER_MODE=static \ -DEN_STATIC_WORKER_URLS=http://worker-01.company.local:8787,http://worker-02.company.local:8787 \ -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml config - -cd packaging/docker -OPENWORK_HOST_PORT=8787 \ -docker compose -p openwork-worker-1 config -``` - -In rendered output, confirm Den has `PROVISIONER_MODE: static` and `STATIC_WORKER_URLS` set to the real worker URLs, and the worker service is built from `packaging/docker/Dockerfile`. - -## 9. Restart, stop, cleanup, and decommission - -Restart Den after configuration changes: - -```bash -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml restart den web -``` - -Restart a worker after token rotation or host configuration changes: - -```bash -cd packaging/docker -docker compose -p openwork-worker-1 restart openwork-host +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 ``` -Follow logs: - -```bash -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml logs -f den web -cd packaging/docker && docker compose -p openwork-worker-1 logs -f openwork-host -``` +Expected result: -Stop without deleting persistent volumes: +- 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 -```bash -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml down -cd packaging/docker && docker compose -p openwork-worker-1 down -``` +At this point, the deployment is up. -Use destructive volume removal only for disposable/test stacks where data loss is intentional: +## First Use -```bash -docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml down -v -cd packaging/docker && docker compose -p openwork-worker-1 down -v -``` +Open the Den web URL in a browser: -Worker decommission checklist: +- `https://` -1. Remove or disable the worker through supported Den UI/API administration flows. -2. Remove the worker URL from `DEN_STATIC_WORKER_URLS` and restart Den. -3. Stop the worker container. -4. Revoke or rotate the worker's `OPENWORK_TOKEN` and `OPENWORK_HOST_TOKEN` in the secret manager. -5. Archive or delete workspace/data directories according to retention policy. +Create the first account and complete email verification. -Test data cleanup should use supported application flows: delete test workers, organizations, users, invites, and sessions through Den UI/API/admin tooling available for the deployment. Avoid direct database edits or manual token-file changes during normal operations; reserve destructive container/volume reset for disposable environments. +Configure SMTP or Resend before the first real sign-up so the verification email is delivered normally. -## 10. Troubleshooting +After the first account is verified, create the first organization. -- Worker remains `Starting`: from the Den host, run `curl http://:8787/health`. Fix DNS, routing, firewall, or the worker container before retrying. -- Worker becomes `failed`: inspect Den logs with `docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml logs den` and confirm `DEN_STATIC_WORKER_URLS` is non-empty and points to OpenWork workers. -- No static worker available: add another URL to `DEN_STATIC_WORKER_URLS`, decommission stale workers through supported admin flows, or stop requesting additional shared workers. -- Auth/CORS problems: set `DEN_PUBLIC_HOST`, `DEN_BETTER_AUTH_URL`, `DEN_BETTER_AUTH_TRUSTED_ORIGINS`, and `DEN_CORS_ORIGINS` to the Den web/API origins before starting Den. -- Wrong connect host in printed worker URLs: set `OPENWORK_CONNECT_HOST` to the worker DNS name and recreate the worker container. -- Desktop opens the wrong Den: confirm the managed deployment created `%ProgramData%\OpenWork\desktop-bootstrap.json` with the expected Den URL, then redeploy or repair through the same management channel. -- Version mismatch: rebuild and redeploy the worker image from the approved source checkout or release artifact you intend to support. +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. -## Appendix A. Non-production static worker smoke simulation +Expected behavior: -Use `static-worker-smoke` only to validate Den static provisioning mechanics without a real OpenWork runtime. It is a tiny `/health` HTTP service, not an OpenWork worker, and it cannot run workspaces or sessions. +- 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` +- Den verifies the configured host token against `/env/keys` +- Den marks the worker `healthy` only after the runtime contract succeeds -PowerShell: +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. -```powershell -Set-Location D:\openwork -$env:DEN_PROVISIONER_MODE = "static" -$env:DEN_STATIC_WORKER_URLS = "http://static-worker-smoke:8787" -docker compose --profile static-worker-smoke -p openwork-den-static -f packaging/docker/docker-compose.den-dev.yml up --build -d -curl.exe http://127.0.0.1:8787/health -``` +If you need more capacity than the remaining free URLs: -Bash: +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 -```bash -cd /path/to/openwork -DEN_PROVISIONER_MODE=static DEN_STATIC_WORKER_URLS=http://static-worker-smoke:8787 \ - docker compose --profile static-worker-smoke -p openwork-den-static \ - -f packaging/docker/docker-compose.den-dev.yml up --build -d -curl http://127.0.0.1:8787/health -``` +## Minimal Troubleshooting -Do not use `static-worker-smoke` for production or workspace/session validation. +- `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/docker-compose.den-static.yml b/packaging/docker/docker-compose.den-static.yml new file mode 100644 index 0000000000..f5542a215a --- /dev/null +++ b/packaging/docker/docker-compose.den-static.yml @@ -0,0 +1,147 @@ +# 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", "mysqladmin ping -h 127.0.0.1 -p$DEN_MYSQL_ROOT_PASSWORD --silent"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 10s + ports: + - "${DEN_MYSQL_PORT:-3306}:3306" + volumes: + - den-mysql-data:/var/lib/mysql + + den: + <<: *shared + build: + context: ../../ + dockerfile: packaging/docker/Dockerfile.den + depends_on: + mysql: + condition: service_healthy + ports: + - "${DEN_API_PORT:-8788}: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} + DATABASE_URL: mysql://root:${DEN_MYSQL_ROOT_PASSWORD}@mysql:3306/openwork_den + 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} + 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 + ports: + - "${DEN_WORKER_PROXY_PORT:-8789}: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: mysql://root:${DEN_MYSQL_ROOT_PASSWORD}@mysql:3306/openwork_den + 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: From cf99a3c8015de1e42c1a5cad4b70f41de05c10b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Fri, 5 Jun 2026 06:14:52 +0200 Subject: [PATCH 04/13] fix: TASK-2026-06-05-001 cache docker dependency install Copy workspace manifests before source files so Docker can reuse the pnpm install layer across source-only changes. --- packaging/docker/Dockerfile | 42 +++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index eb9a5e21c8..160bc59ddc 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -14,10 +14,48 @@ RUN apt-get update \ && npm install -g pnpm@10.27.0 \ && rm -rf /var/lib/apt/lists/* -COPY . . - +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 ENV OPENWORK_SIDECAR_DIR=/data/sidecars From 4e40dd321f74642c5f9a89117ce8497226a5cc9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 9 Jun 2026 14:58:38 +0200 Subject: [PATCH 05/13] fix(docker): make static deployment images build from source --- packaging/docker/Dockerfile | 8 ++++---- packaging/docker/Dockerfile.den | 1 - packaging/docker/Dockerfile.den-web | 1 - packaging/docker/Dockerfile.den-worker-proxy | 1 - 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 160bc59ddc..d6dfa79f41 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:1.2.20 +FROM node:22-bookworm-slim WORKDIR /repo @@ -7,11 +7,11 @@ RUN apt-get update \ ca-certificates \ curl \ git \ - nodejs \ - npm \ tar \ unzip \ - && npm install -g pnpm@10.27.0 \ + && npm install -g pnpm@11.4.0 \ + && curl -fsSL https://bun.sh/install | bash \ + && ln -s /root/.bun/bin/bun /usr/local/bin/bun \ && rm -rf /var/lib/apt/lists/* COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ diff --git a/packaging/docker/Dockerfile.den b/packaging/docker/Dockerfile.den index ed6178658d..4bd4b801c5 100644 --- a/packaging/docker/Dockerfile.den +++ b/packaging/docker/Dockerfile.den @@ -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/email/package.json /app/packages/email/package.json COPY packages/types/package.json /app/packages/types/package.json 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 From 4334240f38a36f8fb79965b50bbe1359382f034b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 9 Jun 2026 15:42:26 +0200 Subject: [PATCH 06/13] docs(runbook): clarify static deployment verification flow --- packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md index 672e356cda..cebd6bbdab 100644 --- a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md +++ b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md @@ -72,12 +72,16 @@ export OPENWORK_HOST_TOKEN='' 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`. @@ -107,9 +111,12 @@ 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 @@ -165,8 +172,8 @@ 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` -- Den verifies the configured host token against `/env/keys` +- 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. From 1bd73bfaa00b4ee6a4831da5046f3adb342e9bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 9 Jun 2026 17:48:39 +0200 Subject: [PATCH 07/13] docs(docker): expose static attach policy env --- packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md | 6 ++++++ packaging/docker/docker-compose.den-static.yml | 3 +++ 2 files changed, 9 insertions(+) diff --git a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md index cebd6bbdab..cbb89b7d00 100644 --- a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md +++ b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md @@ -123,6 +123,10 @@ 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='' @@ -136,6 +140,8 @@ docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-sta `DEN_STATIC_WORKER_TOKEN_MAP_JSON` must contain one entry for every worker URL that Den is allowed to attach as a shared worker. +`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: diff --git a/packaging/docker/docker-compose.den-static.yml b/packaging/docker/docker-compose.den-static.yml index f5542a215a..0041ee20aa 100644 --- a/packaging/docker/docker-compose.den-static.yml +++ b/packaging/docker/docker-compose.den-static.yml @@ -63,6 +63,9 @@ services: 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:-} From 1c5a329d534bbc2776cae479f4cec2c84b9c146a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 9 Jun 2026 20:54:46 +0200 Subject: [PATCH 08/13] fix(docker): pin and verify Bun install --- packaging/docker/Dockerfile | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index d6dfa79f41..7b6ce2c675 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -1,5 +1,9 @@ FROM node:22-bookworm-slim +ARG BUN_VERSION=1.3.8 +ARG BUN_DOWNLOAD_URL= +ARG BUN_LINUX_X64_SHA256=0322b17f0722da76a64298aad498225aedcbf6df1008a1dee45e16ecb226a3f1 + WORKDIR /repo RUN apt-get update \ @@ -10,8 +14,13 @@ RUN apt-get update \ tar \ unzip \ && npm install -g pnpm@11.4.0 \ - && curl -fsSL https://bun.sh/install | bash \ - && ln -s /root/.bun/bin/bun /usr/local/bin/bun \ + && bun_url="${BUN_DOWNLOAD_URL:-https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-linux-x64.zip}" \ + && curl -fsSLo /tmp/bun-linux-x64.zip "$bun_url" \ + && echo "$BUN_LINUX_X64_SHA256 /tmp/bun-linux-x64.zip" | sha256sum -c - \ + && unzip -q /tmp/bun-linux-x64.zip -d /tmp \ + && install -m 0755 /tmp/bun-linux-x64/bun /usr/local/bin/bun \ + && bun --version | grep -qx "$BUN_VERSION" \ + && rm -rf /tmp/bun-linux-x64 /tmp/bun-linux-x64.zip \ && rm -rf /var/lib/apt/lists/* COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ From 54721ed4351909c73ef5ecd80900a2c3d0f0f302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 9 Jun 2026 21:01:02 +0200 Subject: [PATCH 09/13] fix(docker): select Bun artifact by target arch --- packaging/docker/Dockerfile | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 7b6ce2c675..ad4f1156b3 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -1,8 +1,9 @@ FROM node:22-bookworm-slim +ARG TARGETARCH ARG BUN_VERSION=1.3.8 ARG BUN_DOWNLOAD_URL= -ARG BUN_LINUX_X64_SHA256=0322b17f0722da76a64298aad498225aedcbf6df1008a1dee45e16ecb226a3f1 +ARG BUN_SHA256= WORKDIR /repo @@ -14,13 +15,19 @@ RUN apt-get update \ tar \ unzip \ && npm install -g pnpm@11.4.0 \ - && bun_url="${BUN_DOWNLOAD_URL:-https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-linux-x64.zip}" \ - && curl -fsSLo /tmp/bun-linux-x64.zip "$bun_url" \ - && echo "$BUN_LINUX_X64_SHA256 /tmp/bun-linux-x64.zip" | sha256sum -c - \ - && unzip -q /tmp/bun-linux-x64.zip -d /tmp \ - && install -m 0755 /tmp/bun-linux-x64/bun /usr/local/bin/bun \ + && 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-linux-x64 /tmp/bun-linux-x64.zip \ + && rm -rf "/tmp/${bun_artifact%.zip}" "/tmp/${bun_artifact}" \ && rm -rf /var/lib/apt/lists/* COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ From 86383994e4135db17190f914d59f1767c6b16918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 9 Jun 2026 23:56:19 +0200 Subject: [PATCH 10/13] fix: TASK-2026-06-09-002 harden static Docker provisioning Update static Den compose and runbook wiring so smoke validation covers health, workspace, and env-key routes while keeping internal services off host ports and avoiding persistence of env-supplied worker tokens. --- packaging/docker/Dockerfile | 2 +- packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md | 3 ++ packaging/docker/README.md | 8 +++-- packaging/docker/den-dev-up.sh | 5 +++ packaging/docker/docker-compose.den-dev.yml | 35 ++++++++++++++++--- .../docker/docker-compose.den-static.yml | 12 +++---- packaging/docker/docker-compose.yml | 2 +- 7 files changed, 53 insertions(+), 14 deletions(-) diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index ad4f1156b3..7a87994523 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -95,5 +95,5 @@ VOLUME ["/workspace", "/data"] CMD [ \ "sh", \ "-lc", \ - "TOKEN_FILE=/data/openwork-worker.env; if [ -f \"$TOKEN_FILE\" ]; then . \"$TOKEN_FILE\"; fi; if [ -z \"${OPENWORK_TOKEN:-}\" ]; then OPENWORK_TOKEN=owc_$(node -e \"console.log(require('crypto').randomBytes(32).toString('base64url'))\"); export OPENWORK_TOKEN; fi; if [ -z \"${OPENWORK_HOST_TOKEN:-}\" ]; then OPENWORK_HOST_TOKEN=owh_$(node -e \"console.log(require('crypto').randomBytes(32).toString('base64url'))\"); export OPENWORK_HOST_TOKEN; fi; umask 077; printf 'OPENWORK_TOKEN=%s\\nOPENWORK_HOST_TOKEN=%s\\n' \"$OPENWORK_TOKEN\" \"$OPENWORK_HOST_TOKEN\" > \"$TOKEN_FILE\"; echo \"OpenWork worker tokens are stored in $TOKEN_FILE\"; 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" \ + "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:-*}\"; 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/ONPREM_DEN_STATIC_RUNBOOK.md b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md index cbb89b7d00..9d66a32ec8 100644 --- a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md +++ b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md @@ -15,6 +15,7 @@ In `static` mode: - 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. @@ -140,6 +141,8 @@ docker compose -p openwork-den-static -f packaging/docker/docker-compose.den-sta `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 diff --git a/packaging/docker/README.md b/packaging/docker/README.md index c5f8ce2a7a..fdf91d7a9f 100644 --- a/packaging/docker/README.md +++ b/packaging/docker/README.md @@ -67,6 +67,7 @@ Optional env vars (via `.env` or `export`): - `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`) @@ -89,6 +90,7 @@ If you need a non-production compose-only smoke test before wiring a real OpenWo ```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 ``` @@ -97,11 +99,13 @@ 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 health-check simulation for provisioning validation; it is not a production OpenWork runtime and will not satisfy workspace/session APIs. +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 @@ -227,7 +231,7 @@ Validate the worker before adding it to Den: 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. If both variables are unset, the image generates stable per-worker fallback tokens and persists them in `/data/openwork-worker.env`; use that path only for development or an operator-approved bootstrap. Treat `/data/openwork-worker.env` as sensitive bearer-secret material. +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 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 88de8bdac4..be7cf3cf4a 100644 --- a/packaging/docker/docker-compose.den-dev.yml +++ b/packaging/docker/docker-compose.den-dev.yml @@ -23,6 +23,8 @@ # 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 @@ -93,6 +95,7 @@ services: 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} @@ -184,15 +187,39 @@ services: - 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') { - res.writeHead(200, {'content-type':'application/json'}); - res.end(JSON.stringify({ok:true, mode:'static-worker-smoke'})); + json(res, 200, {ok:true, mode:'static-worker-smoke'}); return; } - res.writeHead(404); - res.end('not found'); + 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: diff --git a/packaging/docker/docker-compose.den-static.yml b/packaging/docker/docker-compose.den-static.yml index 0041ee20aa..ee55efe0be 100644 --- a/packaging/docker/docker-compose.den-static.yml +++ b/packaging/docker/docker-compose.den-static.yml @@ -24,8 +24,8 @@ services: timeout: 5s retries: 30 start_period: 10s - ports: - - "${DEN_MYSQL_PORT:-3306}:3306" + expose: + - "3306" volumes: - den-mysql-data:/var/lib/mysql @@ -37,8 +37,8 @@ services: depends_on: mysql: condition: service_healthy - ports: - - "${DEN_API_PORT:-8788}:8788" + 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 @@ -102,8 +102,8 @@ services: depends_on: mysql: condition: service_healthy - ports: - - "${DEN_WORKER_PROXY_PORT:-8789}:8789" + 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 diff --git a/packaging/docker/docker-compose.yml b/packaging/docker/docker-compose.yml index e42a16cb3f..42313ec9df 100644 --- a/packaging/docker/docker-compose.yml +++ b/packaging/docker/docker-compose.yml @@ -15,7 +15,7 @@ services: # Optional: OPENWORK_APPROVAL_TIMEOUT_MS: "30000" OPENWORK_APPROVAL_MODE: ${OPENWORK_APPROVAL_MODE:-manual} OPENWORK_APPROVAL_TIMEOUT_MS: ${OPENWORK_APPROVAL_TIMEOUT_MS:-30000} - OPENWORK_CORS_ORIGINS: ${OPENWORK_CORS_ORIGINS:-*} + OPENWORK_CORS_ORIGINS: "${OPENWORK_CORS_ORIGINS:-*}" OPENWORK_PORT: 8787 OPENWORK_DATA_DIR: /data/openwork-orchestrator OPENWORK_SIDECAR_DIR: /data/sidecars From 6d1086b46c0da4ad7e1e04e0fc0f4a7684840ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Wed, 10 Jun 2026 13:57:11 +0200 Subject: [PATCH 11/13] docs: TASK-2026-06-10-008 document static worker token map Strengthen real LAN static worker quickstart with the required DEN_STATIC_WORKER_TOKEN_MAP_JSON mapping. --- packaging/docker/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packaging/docker/README.md b/packaging/docker/README.md index fdf91d7a9f..79f1b0bbc8 100644 --- a/packaging/docker/README.md +++ b/packaging/docker/README.md @@ -82,9 +82,12 @@ 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 From 35cf5be18045b034e0e312f2299e6962d57e474c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Wed, 10 Jun 2026 16:49:44 +0200 Subject: [PATCH 12/13] fix: TASK-2026-06-10-008 harden docker worker defaults Replace wildcard worker CORS defaults with local-safe origins and require an encoded static Den DATABASE_URL to support special-character MySQL passwords. --- packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md | 3 +++ packaging/docker/README.md | 2 ++ packaging/docker/docker-compose.den-static.yml | 4 +++- packaging/docker/docker-compose.yml | 4 +++- packaging/docker/microsandbox-entrypoint.sh | 2 +- 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md index 9d66a32ec8..50ae6420b5 100644 --- a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md +++ b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md @@ -131,6 +131,9 @@ 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 diff --git a/packaging/docker/README.md b/packaging/docker/README.md index 79f1b0bbc8..77e56e6e38 100644 --- a/packaging/docker/README.md +++ b/packaging/docker/README.md @@ -242,6 +242,7 @@ 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: @@ -251,6 +252,7 @@ Optional: - `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: diff --git a/packaging/docker/docker-compose.den-static.yml b/packaging/docker/docker-compose.den-static.yml index ee55efe0be..1d903939cc 100644 --- a/packaging/docker/docker-compose.den-static.yml +++ b/packaging/docker/docker-compose.den-static.yml @@ -48,7 +48,9 @@ services: environment: CI: "true" OPENWORK_DEV_MODE: ${OPENWORK_DEV_MODE:-0} - DATABASE_URL: mysql://root:${DEN_MYSQL_ROOT_PASSWORD}@mysql:3306/openwork_den + # 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} diff --git a/packaging/docker/docker-compose.yml b/packaging/docker/docker-compose.yml index 42313ec9df..6a4ff623f4 100644 --- a/packaging/docker/docker-compose.yml +++ b/packaging/docker/docker-compose.yml @@ -15,7 +15,9 @@ services: # Optional: OPENWORK_APPROVAL_TIMEOUT_MS: "30000" OPENWORK_APPROVAL_MODE: ${OPENWORK_APPROVAL_MODE:-manual} OPENWORK_APPROVAL_TIMEOUT_MS: ${OPENWORK_APPROVAL_TIMEOUT_MS:-30000} - OPENWORK_CORS_ORIGINS: "${OPENWORK_CORS_ORIGINS:-*}" + # 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 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}" From 025fa5b504006b3ad4b58f66aab4a5d482690bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Wed, 10 Jun 2026 18:06:38 +0200 Subject: [PATCH 13/13] fix: TASK-2026-06-10-009 harden static docker config --- packaging/docker/Dockerfile | 2 +- packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md | 1 + packaging/docker/docker-compose.den-static.yml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 7a87994523..650c03534e 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -95,5 +95,5 @@ VOLUME ["/workspace", "/data"] 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:-*}\"; 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" \ + "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/ONPREM_DEN_STATIC_RUNBOOK.md b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md index 50ae6420b5..d7deeb89aa 100644 --- a/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md +++ b/packaging/docker/ONPREM_DEN_STATIC_RUNBOOK.md @@ -70,6 +70,7 @@ 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 diff --git a/packaging/docker/docker-compose.den-static.yml b/packaging/docker/docker-compose.den-static.yml index 1d903939cc..9b08c3ceae 100644 --- a/packaging/docker/docker-compose.den-static.yml +++ b/packaging/docker/docker-compose.den-static.yml @@ -19,7 +19,7 @@ services: MYSQL_ROOT_PASSWORD: ${DEN_MYSQL_ROOT_PASSWORD:?DEN_MYSQL_ROOT_PASSWORD is required} MYSQL_DATABASE: openwork_den healthcheck: - test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -p$DEN_MYSQL_ROOT_PASSWORD --silent"] + test: ["CMD-SHELL", "MYSQL_PWD=\"$${MYSQL_ROOT_PASSWORD}\" mysqladmin ping -h 127.0.0.1 --silent"] interval: 5s timeout: 5s retries: 30 @@ -114,7 +114,7 @@ services: start_period: 90s environment: CI: "true" - DATABASE_URL: mysql://root:${DEN_MYSQL_ROOT_PASSWORD}@mysql:3306/openwork_den + 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:-}