From dd70abe1cab9cc63a721da8f28d488bec65809e7 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 19 Aug 2026 12:10:44 -0300 Subject: [PATCH] feat(docker): expose DASHBOARD_ALLOW_EMBED as a build argument (#10701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(docker): expose DASHBOARD_ALLOW_EMBED as a build argument The dashboard's frame-ancestors policy is compiled into the route manifest at build time, so the only way to get an embed-enabled image was to edit the Dockerfile: Docker silently drops a --build-arg with no matching ARG, so `docker build --build-arg DASHBOARD_ALLOW_EMBED=vscode` produced the default image and no error. Declared as ARG+ENV in the builder stage, mirroring OMNIROUTE_BASE_PATH, and empty by default — the unframable default posture is unchanged. The runtime stages deliberately do not carry it: the headers are already baked, so a runtime value would advertise an effect it cannot have. Guarded by tests/unit/dockerfile-dashboard-embed-arg-10273.test.ts, verified by mutation (a bare ENV in place of the ARG fails 2 of the 3 assertions). Docs updated across the guide, ENVIRONMENT.md and .env.example. The guide also carries prettier normalization (emphasis markers, table padding) applied by lint-staged on commit. Refs #10273 * chore(changelog): correct the fragment to the real PR number (#10701) --------- Co-authored-by: Xiangzhe --- .env.example | 2 + Dockerfile | 12 +++ .../10701-dockerfile-dashboard-embed-arg.md | 1 + docs/guides/VSCODE-COPILOT.md | 44 +++++----- docs/reference/ENVIRONMENT.md | 2 +- ...ckerfile-dashboard-embed-arg-10273.test.ts | 82 +++++++++++++++++++ 6 files changed, 120 insertions(+), 23 deletions(-) create mode 100644 changelog.d/features/10701-dockerfile-dashboard-embed-arg.md create mode 100644 tests/unit/dockerfile-dashboard-embed-arg-10273.test.ts diff --git a/.env.example b/.env.example index 7f524749a4..c809f7653c 100644 --- a/.env.example +++ b/.env.example @@ -143,6 +143,8 @@ PORT=20128 # (/api, /v1, /v1beta, /a2a, /healthz and the root-level aliases) keeps the strict # headers regardless. Only `vscode` is recognised; `1`/`true` do NOT enable it. # Used by: next.config.mjs via scripts/build/dashboardEmbed.mjs — build-time, rebuild after changing. +# Docker: pass it as a build arg (`docker build --build-arg DASHBOARD_ALLOW_EMBED=vscode`); +# setting it on an already-built server or image does nothing. # DASHBOARD_ALLOW_EMBED=vscode # Split-port mode: serve Dashboard and API on separate ports for network isolation. diff --git a/Dockerfile b/Dockerfile index eefd6ed57d..de9b5a1499 100644 --- a/Dockerfile +++ b/Dockerfile @@ -140,6 +140,18 @@ ENV OMNIROUTE_USE_TURBOPACK="${OMNIROUTE_USE_TURBOPACK}" ARG OMNIROUTE_BASE_PATH="" ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH +# #10273: the dashboard's `frame-ancestors` policy is compiled into the route +# manifest by next.config.mjs (via scripts/build/dashboardEmbed.mjs), so it is +# fixed when the image is built and cannot be flipped with `-e` on a running +# container. Build with `--build-arg DASHBOARD_ALLOW_EMBED=vscode` to produce an +# image whose HTML pages may be framed by the VS Code Simple Browser +# (OmniCopilot's `dashboardOpen: "editor"`). Unset — the default — keeps every +# route on `frame-ancestors 'none'` + X-Frame-Options: DENY. Builder-stage only: +# the runner stage deliberately does not carry it, because a runtime value would +# suggest an effect it cannot have. +ARG DASHBOARD_ALLOW_EMBED="" +ENV DASHBOARD_ALLOW_EMBED=$DASHBOARD_ALLOW_EMBED + # Docker containers cannot run the MITM/Agent-Bridge stack (no host DNS/cert # access), so keep @/mitm/manager on the graceful stub (#3390). This flag is # Docker-only: npm/Electron/VPS builds must bundle the REAL manager (#6344). diff --git a/changelog.d/features/10701-dockerfile-dashboard-embed-arg.md b/changelog.d/features/10701-dockerfile-dashboard-embed-arg.md new file mode 100644 index 0000000000..552a873268 --- /dev/null +++ b/changelog.d/features/10701-dockerfile-dashboard-embed-arg.md @@ -0,0 +1 @@ +- **feat(docker):** `DASHBOARD_ALLOW_EMBED` is now a Docker build argument — `docker build --build-arg DASHBOARD_ALLOW_EMBED=vscode` produces an image whose dashboard renders inside the VS Code Simple Browser (OmniCopilot's `dashboardOpen: "editor"`). Previously the flag was only reachable from a source build: Docker silently drops a `--build-arg` with no matching `ARG`, so the operator got the default image and no error. Builder-stage only and empty by default — the runtime stages deliberately do not carry it, and the unframable default posture is unchanged ([#10701](https://github.com/diegosouzapw/OmniRoute/pull/10701)) diff --git a/docs/guides/VSCODE-COPILOT.md b/docs/guides/VSCODE-COPILOT.md index a8ed6258c2..e0a4386fc0 100644 --- a/docs/guides/VSCODE-COPILOT.md +++ b/docs/guides/VSCODE-COPILOT.md @@ -6,16 +6,16 @@ lastUpdated: 2026-08-18 # VS Code Copilot Chat — OmniCopilot extension -**OmniCopilot** puts every model your OmniRoute serves into the *native* GitHub Copilot Chat +**OmniCopilot** puts every model your OmniRoute serves into the _native_ GitHub Copilot Chat model picker. No second sidebar, no separate chat UI — Copilot's agent mode, tool calling, MCP servers and custom instructions all keep working, just running on the model you pick. -| | | -| --- | --- | -| **Install (VS Code)** | [Marketplace → `diegosouzapw.omnicopilot`](https://marketplace.visualstudio.com/items?itemName=diegosouzapw.omnicopilot) | -| **Install (forks)** | [Open VSX](https://open-vsx.org/extension/diegosouzapw/omnicopilot) — Cursor, Windsurf, VSCodium, Theia, code-server, Gitpod, Antigravity, Kiro | -| **Source / issues** | [github.com/diegosouzapw/OmniCopilot](https://github.com/diegosouzapw/OmniCopilot) (MIT) | -| **Requires** | VS Code 1.104+ | +| | | +| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| **Install (VS Code)** | [Marketplace → `diegosouzapw.omnicopilot`](https://marketplace.visualstudio.com/items?itemName=diegosouzapw.omnicopilot) | +| **Install (forks)** | [Open VSX](https://open-vsx.org/extension/diegosouzapw/omnicopilot) — Cursor, Windsurf, VSCodium, Theia, code-server, Gitpod, Antigravity, Kiro | +| **Source / issues** | [github.com/diegosouzapw/OmniCopilot](https://github.com/diegosouzapw/OmniCopilot) (MIT) | +| **Requires** | VS Code 1.104+ | > **No Copilot subscription needed.** Since VS Code 1.122 a language-model provider works > without a GitHub sign-in and without any Copilot plan. Inline completions and @@ -60,7 +60,7 @@ The extension requests **`GET /v1/models?prefix=alias`** so one id arrives per m changing the server-wide setting for your other clients. On a reference instance this collapsed **2345 entries to 1396 — 949 duplicates, zero models lost.** -If you would rather fix it server-wide for *every* client, set the +If you would rather fix it server-wide for _every_ client, set the `MODELS_CATALOG_PREFIX_MODE` feature flag to `alias` in the dashboard. See [API_REFERENCE → prefix](../reference/API_REFERENCE.md#model-id-prefixes-prefix) for the query parameter and the warning about `canonical`. @@ -81,7 +81,7 @@ and OmniRoute translates those for `/v1/chat/completions`, so they are perfectly ### Providers you never configured -The catalog lists models from providers with an **active connection** *plus* every **noAuth** +The catalog lists models from providers with an **active connection** _plus_ every **noAuth** provider — the keyless ones that make up much of the free tier. That is intentional. To hide them, add them to `blockedProviders` in the dashboard settings; nothing changes in the extension. @@ -108,11 +108,11 @@ DASHBOARD_ALLOW_EMBED=vscode npm run build # or npm run build:release npm start ``` -| How you installed | Can you enable embedding? | -| --- | --- | -| From source | ✅ set the variable on the build command, as above | -| `npm install -g omniroute` | ❌ the published package ships a prebuilt bundle — build from source instead | -| Docker image | ❌ the official image has no build arg for it — build your own from the `Dockerfile` with the variable set | +| How you installed | Can you enable embedding? | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| From source | ✅ set the variable on the build command, as above | +| `npm install -g omniroute` | ❌ the published package ships a prebuilt bundle — build from source instead | +| Docker image | ✅ `docker build --build-arg DASHBOARD_ALLOW_EMBED=vscode -t omniroute:embed .` — the prebuilt image on Docker Hub is not embed-enabled | Without an embed-enabled build the page refuses to frame, the extension detects that from the response headers and falls back to the external browser — nothing breaks, and it says so once. @@ -133,14 +133,14 @@ Kilo and Roo — the same configs described in ## Troubleshooting -| Symptom | Cause / fix | -| --- | --- | -| No OmniRoute models in the picker | Server unreachable. The status-bar dot goes grey; run `OmniRoute: Check Connection`. Discovery is silent by design and contributes no models rather than prompting. | -| Every model appears twice | You are on an OmniCopilot older than 1.0.1 — update. The extension now requests `?prefix=alias`. | -| An image/audio model used to be listed and is gone | Intentional since 1.0.1 — it could never answer a chat request. | -| Panel missing from the Activity Bar | VS Code moves extra view containers into the **"…"** overflow at the bottom of the Activity Bar, and a container hidden via right-click stays hidden. Right-click the Activity Bar → tick **OmniRoute**, or open it with `OmniRoute: Manage Connection`. | -| Dashboard opens in the browser despite `editor` mode | The server was not **built** with `DASHBOARD_ALLOW_EMBED=vscode` (see above) — setting it at startup on a prebuilt install does nothing. The fallback is deliberate. | -| Models list is stale after changing providers | `OmniRoute: Refresh Models`, or the ↻ link in the panel. | +| Symptom | Cause / fix | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| No OmniRoute models in the picker | Server unreachable. The status-bar dot goes grey; run `OmniRoute: Check Connection`. Discovery is silent by design and contributes no models rather than prompting. | +| Every model appears twice | You are on an OmniCopilot older than 1.0.1 — update. The extension now requests `?prefix=alias`. | +| An image/audio model used to be listed and is gone | Intentional since 1.0.1 — it could never answer a chat request. | +| Panel missing from the Activity Bar | VS Code moves extra view containers into the **"…"** overflow at the bottom of the Activity Bar, and a container hidden via right-click stays hidden. Right-click the Activity Bar → tick **OmniRoute**, or open it with `OmniRoute: Manage Connection`. | +| Dashboard opens in the browser despite `editor` mode | The server was not **built** with `DASHBOARD_ALLOW_EMBED=vscode` (see above) — setting it at startup on a prebuilt install does nothing. The fallback is deliberate. | +| Models list is stale after changing providers | `OmniRoute: Refresh Models`, or the ↻ link in the panel. | --- diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 55cebb557e..ccb6a6981f 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -131,7 +131,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari | `PORT` | `20128` | `src/lib/runtime/ports.ts` | Primary port for both Dashboard UI and API endpoints (single-port mode). | | `OMNIROUTE_BASE_PATH` | _(empty = root)_ | `next.config.mjs`, `scripts/docker/ensure-docker-base-path.mjs` | URL subpath for serving OmniRoute behind a reverse proxy (sets Next.js `basePath`; auth redirects are basePath-aware). E.g. `/omniroute`. In Docker the value is baked during `docker build` (`ARG OMNIROUTE_BASE_PATH`); pre-built root images can apply a different runtime value once at container start before Next.js boots. Set `NEXT_PUBLIC_BASE_URL` to the public origin including the same subpath. | | `NEXT_PUBLIC_OMNIROUTE_BASE_PATH` | _(empty = root)_ | `src/shared/hooks/useDisplayBaseUrl.ts` | Browser-visible mirror of `OMNIROUTE_BASE_PATH`, inlined at build time so the dashboard endpoint display shows `https://host/omniroute/v1` instead of `https://host/v1`. Falls back to `OMNIROUTE_BASE_PATH` when unset. Rebuild after changing (Next `basePath` is build-time). | -| `DASHBOARD_ALLOW_EMBED` | _(unset = never framable)_ | `next.config.mjs`, `scripts/build/dashboardEmbed.mjs` | Opt-in iframe embedding of the HTML pages. Unset, every route ships `frame-ancestors 'none'` + `X-Frame-Options: DENY`. Set to `vscode` to serve the pages (dashboard, login, docs, landing) with `frame-ancestors 'self' vscode-webview:` and no `X-Frame-Options`, so the VS Code Simple Browser can render them (OmniCopilot's `dashboardOpen: "editor"` mode). The API surface (`/api`, `/v1`, `/v1beta`, `/a2a`, `/healthz`, root-level aliases) keeps the strict headers either way. Only `vscode` is recognised — `1`/`true` do not enable it. Build-time: rebuild after changing. | +| `DASHBOARD_ALLOW_EMBED` | _(unset = never framable)_ | `next.config.mjs`, `scripts/build/dashboardEmbed.mjs` | Opt-in iframe embedding of the HTML pages. Unset, every route ships `frame-ancestors 'none'` + `X-Frame-Options: DENY`. Set to `vscode` to serve the pages (dashboard, login, docs, landing) with `frame-ancestors 'self' vscode-webview:` and no `X-Frame-Options`, so the VS Code Simple Browser can render them (OmniCopilot's `dashboardOpen: "editor"` mode). The API surface (`/api`, `/v1`, `/v1beta`, `/a2a`, `/healthz`, root-level aliases) keeps the strict headers either way. Only `vscode` is recognised — `1`/`true` do not enable it. Build-time: rebuild after changing (`docker build --build-arg DASHBOARD_ALLOW_EMBED=vscode` for images; setting it on a prebuilt install has no effect). | | `API_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the `/v1/*` proxy API on this separate port. | | `API_HOST` | `0.0.0.0` | `src/lib/runtime/ports.ts` | Bind address for the API port. | | `DASHBOARD_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the Dashboard UI on this separate port. | diff --git a/tests/unit/dockerfile-dashboard-embed-arg-10273.test.ts b/tests/unit/dockerfile-dashboard-embed-arg-10273.test.ts new file mode 100644 index 0000000000..6b3416721a --- /dev/null +++ b/tests/unit/dockerfile-dashboard-embed-arg-10273.test.ts @@ -0,0 +1,82 @@ +/** + * #10273 — `DASHBOARD_ALLOW_EMBED=vscode` relaxes the dashboard's CSP + * `frame-ancestors` so the VS Code Simple Browser (the OmniCopilot extension's + * `dashboardOpen: "editor"` mode) can render it. next.config.mjs reads the + * variable while the bundle is built and Next.js compiles the result into the + * route manifest, so the policy is frozen at build time. + * + * The Dockerfile therefore has to expose it as a build argument. Without an + * `ARG`, `docker build --build-arg DASHBOARD_ALLOW_EMBED=vscode` is silently + * dropped by Docker and the operator gets the default (unframable) image with + * no error — the same class of failure #6700's `OMNIROUTE_USE_TURBOPACK` note + * documents for a bare `ENV`. + * + * Guarded here rather than in a real `docker build`, which this sandbox cannot + * run: the assertions pin the mechanism (declared as ARG+ENV, inside the + * builder stage, before the build step) and that the runner stage does NOT + * carry the variable — a runtime value would advertise an effect it cannot + * have. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const lines = fs.readFileSync(path.join(repoRoot, "Dockerfile"), "utf-8").split("\n"); + +/** Line indices bounding a named stage: its FROM up to the next FROM. */ +function stageRange(name: string): { start: number; end: number } { + const start = lines.findIndex((l) => + new RegExp(`^FROM\\s+\\S+\\s+AS\\s+${name}\\b`, "i").test(l.trim()) + ); + assert.ok(start >= 0, `Dockerfile must declare a \`${name}\` stage`); + const after = lines.slice(start + 1).findIndex((l) => /^FROM\s+/i.test(l.trim())); + return { start, end: after === -1 ? lines.length : start + 1 + after }; +} + +test("#10273 the builder stage exposes DASHBOARD_ALLOW_EMBED as a build arg", () => { + const { start, end } = stageRange("builder"); + const stage = lines.slice(start, end); + + const argIdx = stage.findIndex((l) => /^ARG\s+DASHBOARD_ALLOW_EMBED\b/.test(l.trim())); + assert.ok( + argIdx >= 0, + "builder stage must declare `ARG DASHBOARD_ALLOW_EMBED` — without it, " + + "`docker build --build-arg DASHBOARD_ALLOW_EMBED=vscode` is silently ignored" + ); + + // ARG alone is not visible to the build process; it has to be promoted to ENV, + // and the ENV must come from the ARG (a bare `ENV X=vscode` would shadow it). + const envIdx = stage.findIndex((l) => + /^ENV\s+DASHBOARD_ALLOW_EMBED=\$\{?DASHBOARD_ALLOW_EMBED\}?\s*$/.test(l.trim()) + ); + assert.ok(envIdx > argIdx, "ARG must be promoted to ENV from the ARG value, after the ARG"); + + // It only has an effect if it is set before `next build` runs. + // The build command sits inside a multi-line RUN block, so match the line itself. + const buildIdx = stage.findIndex((l) => /\bnpm run build\b/.test(l)); + assert.ok(buildIdx > envIdx, "DASHBOARD_ALLOW_EMBED must be set before the build step"); +}); + +test("#10273 the default is empty, so images stay unframable unless asked", () => { + const { start, end } = stageRange("builder"); + const arg = lines.slice(start, end).find((l) => /^ARG\s+DASHBOARD_ALLOW_EMBED\b/.test(l.trim())); + assert.match( + String(arg).trim(), + /^ARG\s+DASHBOARD_ALLOW_EMBED=(""|'')$/, + "the build arg must default to empty — embedding is opt-in (Hard Rule: default posture unchanged)" + ); +}); + +test("#10273 no runtime stage carries DASHBOARD_ALLOW_EMBED", () => { + const { end } = stageRange("builder"); + const afterBuilder = lines.slice(end).join("\n"); + assert.doesNotMatch( + afterBuilder, + /^\s*(ENV|ARG)\s+DASHBOARD_ALLOW_EMBED\b/m, + "the runtime stages (runner-base / runner-web / runner-cli) must not set it: the " + + "headers are already baked, so a runtime value would advertise an effect it cannot have" + ); +});