Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
44 changes: 22 additions & 22 deletions docs/guides/VSCODE-COPILOT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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. |

---

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
82 changes: 82 additions & 0 deletions tests/unit/dockerfile-dashboard-embed-arg-10273.test.ts
Original file line number Diff line number Diff line change
@@ -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"
);
});
Loading