Repository navigation
feat(windows): serve the dashboard from dashboard-api in the Windows Desktop profile - #7978
Open
IronicRayquaza wants to merge 6 commits into
Open
IronicRayquaza wants to merge 6 commits into
IronicRayquaza wants to merge 6 commits into
Conversation
Add an opt-in ODS_RUNTIME_PROFILE=windows-desktop so dashboard-api can run as a native Windows process with no Docker, no WSL and no host agent. - Refuse to start without an absolute ODS_DATA_DIR and DASHBOARD_API_KEY; the generated-key fallback no longer writes to /data (C:\data on Windows). - Never dial a host agent: AGENT_URL is empty and the shared client raises AgentUnavailable before any socket, so a Docker or WSL ODS on the same PC cannot answer. - Start no child processes: GPU probes, the curl cluster refresh, Token Spy polling and the Docker preflight are skipped. A windowless process would otherwise flash a console every five seconds. - Load only loopback services, since Compose service names resolve to nothing on a native PC and each failed lookup stalls /api/status. - Report Windows CPU and RAM through kernel32 instead of None. - Add a path-filtered windows-latest workflow that installs the hashed lock wheels-only and runs the profile tests. Refs Osmantic#7968 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mypy runs on Linux, where ctypes has no windll or WinError, so assert the Windows platform inside the two kernel32 readers. Count lock hashes in a typed dict instead of a dict mixing names and counters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mypy 2.4 does not treat an assert on sys.platform as unreachable, so define the two readers under a module-level sys.platform == "win32" branch, with stubs that raise OSError elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Desktop profile The Windows Desktop profile (Osmantic#7968) has no nginx, so dashboard-api is the only listener. When the launcher sets ODS_DASHBOARD_DIST to the built dashboard, dashboard-api serves it and applies the boundary nginx applies: who receives the server-held DASHBOARD_API_KEY. native_front.DashboardGate ports the stricter twin of nginx's gate, the Vite dev gate (dev-api-access.js). A request gets the key without a session only from a loopback peer, with a loopback Host naming this listener's port, no forwarding headers, no duplicated Host/Origin/Sec-Fetch-Site, and a same-origin or absent Origin and Sec-Fetch-Site. A website that sends a request to localhost (DNS rebinding, cross-site, CSRF) therefore needs a dashboard session; otherwise it gets nginx's 401 with X-ODS-Sign-In. A client's own Authorization header is always replaced, never trusted. Talk and dashboard sign-in never receive the admin key. Sessions are verified in-process with the existing session_is_valid(). DashboardFiles serves the build with SPA fallback for client routes, nginx's cache rules (hashed assets immutable, everything else no-cache), nginx's security headers and CSP without the Pixel preview origins, and a JSON 404 for unknown API paths instead of the dashboard page. /pixel-preview does not exist in this profile. The mount is registered after every API route so none is shadowed. Refs Osmantic#7968 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ngs Osmantic#7975 requires Osmantic#7978's startup probe predates Osmantic#7975, which refuses a Desktop start without ODS_NATIVE_LLM_URL and ODS_NATIVE_LLM_RUNTIME_DIR. Whichever of the two merges second carries this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
|
Thanks for bringing the native dashboard's access checks and routing behavior into a testable ASGI boundary. Advisory status: not ready yet Quickest path to merge:
What I checked:
|
`python main.py` passed host="0.0.0.0" to uvicorn in every profile, so a Desktop process started that way listened beyond this PC; only the PR6 launcher's --host kept installs on 127.0.0.1. RuntimeProfile.listen_host now makes loopback part of the profile (Compose keeps 0.0.0.0 for Docker publishing), and a fresh-interpreter test runs main.py as __main__ with uvicorn.run intercepted under both profiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Thanks, good catch: the launcher's bind protected the installed path, but not Fixed in 7a305bf:
On Windows (Python 3.13), these pass:
|
8 of 21 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fifth step of #7968: dashboard-api serves the dashboard itself in the Windows Desktop profile. With no nginx, the Desktop app window (an Edge app window on a loopback URL) needs one listener that serves the UI and decides who gets the server-held
DASHBOARD_API_KEY.It applies only when the launcher sets
ODS_DASHBOARD_DISTto an absolute built dashboard folder; the Compose profile never loads it.native_front.DashboardGate(ASGI middleware) ports the stricter twin of nginx's gate, the Vite dev gatedev-api-access.jslocalRequest(). A request receives the key without a session only when all of these hold:Hostislocalhost,127.0.0.1or[::1]with this listener's port;Forwarded/Via/X-Real-IP/X-Forwarded-*headers;Host,OriginandSec-Fetch-Siteare not duplicated;OriginandSec-Fetch-Siteare same-origin or absent.Why the stricter rules: on a home PC the browser visits arbitrary websites, any of which can send requests to
http://localhost:<port>. Under these rules DNS rebinding, cross-site requests and CSRF all count as untrusted. Untrusted requests need a dashboard session, checked in-process with the existingsession_is_valid(); otherwise they get nginx's own 401 body withX-ODS-Sign-In: required.A client's own
Authorizationheader is always removed. Talk and dashboard sign-in/out never receive the admin key, matching nginx.native_front.DashboardFilesserves the build with:assets/immutable for a year, everything elseno-cache;Unknown
/api/*paths return a JSON 404, as nginx forwards them, never the dashboard page with a 200./pixel-preview/*is 404. The/mount is registered after every@approute, so no API route is shadowed. An early version had exactly that bug; the integration test below now guards it.AI Assistance
Claude Code drafted the code, tests and this description, and ran the validation below on my Windows 11 laptop. I chose the scope and design (see #7968), and I'm accountable for the diff and for responding to review.
Release Lane
release/2.6.xmainChanged Surface
Risk And Validation
git diff --checkLive, on Windows 11 Home 26200 (i5-10500H): dashboard-api from this branch on #7972's pinned
pythonw.exe(stdout and stderr redirected), probed withcurl.exe:Related existing tests (
-k "session or auth or main or status or csrf or origin") match main. One async ordering test (test_pixel_agent_teams.py::test_real_session_ids_order_handoff_and_idempotency) failed once while the laptop was under load, then passed 4/4 on this branch, #7970 and main.Operational Change Check
Network exposure and auth are touched for the Windows Desktop profile only. The listener is loopback (PR6's launcher binds
127.0.0.1).Notes For Reviewers
npm ci, which comes with the UI change in PR4. The gate doesn't depend on the UI's contents.Hostis trusted. Requiring a session cookie, or checking the peer process's owner, is a follow-up if you want it.http://127.0.0.1:<port>/rather thanlocalhost(0.2 s per new connection lost to the IPv6 attempt).