You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Independent of #533/#534 — can proceed in parallel.
Following youperiod.app, the server's principal job becomes serving static files with correct security headers. Static assets and the API share one origin, so there is no CORS configuration at all, and they must not be split.
Headers
header
what it prevents
Content-Security-Policy: default-src 'self'
Any third-party resource. No CDN, no web fonts, no analytics — none are used, and the header makes that checkable rather than promised.
connect-src 'self'
The interface becomes provably incapable of sending positions, equity or trade history anywhere but the local process. Browser-enforced, one header, verifiable in seconds. Outbound documentation links are navigation, not connections, and are unaffected.
Referrer-Policy: no-referrer
Leaking the URL to keeltrading.com when a doc link is followed (#539). The session token rides in the URL until the cookie exchange; modern browsers send origin-only cross-origin already, so exposure is small, but one header closes it.
X-Content-Type-Options: nosniff
Content-type sniffing.
Strict-Transport-Security is not sent — the origin is loopback HTTP by design and HSTS would be meaningless. Subresource Integrity authorises any inline <script>/<style>.
A third CSRF layer
keel/web/security.py already documents five layers, and this issue must preserve, not rebuild them: loopback binding, Host validation against DNS rebinding, a session token exchanged for a SameSite=StrictHttpOnly cookie, the closed action set, and an HMAC-derived CSRF token described in-source as "the layer that does not depend on the browser being current."
Added here, because SameSite has had parser bypasses: every POST /api/* requires X-Keel-Client: 1. A custom header forces a CORS preflight a hostile origin cannot satisfy — the attack this closes is the HTML form POST, which is not preflighted. Paired with a Sec-Fetch-Site: same-origin check, which page JavaScript cannot forge.
Asset packaging — the part that silently breaks the product
uv_build packages the module root — keel/ — and everything under it. Correction, measured: the artifacts key is inert on the pinned backend; building with it and with artifacts = [] produces wheels with identical contents. docs/ is absent because it sits at the repository root, outside keel/ — which is why /glossary renders an empty state in every installed deployment (help_console.py:138-146), and why no glob can fix it.
So assert against a built wheel, not a glob. And note the bundle is the half that actually needs work: keel/freeze.py's collect_data names only the template package, so PyInstaller is never told about keel/web/static/ and every /static/* request 404s in a frozen app.
Ship the static assets in the wheel and in the desktop bundle manifest, or the rewrite works from a checkout and produces a blank page for every real user.
Acceptance
Every header above is sent on the correct content types, asserted by tests.
A page load makes zero requests to any origin but the local server.
A POST /api/* without X-Keel-Client is refused; a cross-origin form POST cannot reach an action.
POST /setup/* still works from a plain HTML form with no such header. The rendered pages ship no JavaScript (default-src 'none', no script-src), so a form cannot set a custom header — requiring one there would refuse the only client that exists. /setup/* keeps host validation, the SameSite=Strict cookie, the CSRF token, the closed action set, and Sec-Fetch-Site: same-origin. The check widens to /setup/* in Client shell: router, status view, responsive layout, accessibility baseline #536, when the forms become fetch() calls.
A test posts to /setup/* exactly as a browser does — cookie, CSRF token, Origin, Sec-Fetch-Site, no X-Keel-Client — and asserts the action succeeds.
All five existing security layers still pass their tests, unmodified.
Static assets are present in a built wheel — asserted, not assumed.
A double-click of the packaged app reaches a served page with no terminal command.
Spec:
docs/superpowers/specs/2026-08-23-web-ui-rewrite-design.md§ Server, § Security headers.Independent of #533/#534 — can proceed in parallel.
Following youperiod.app, the server's principal job becomes serving static files with correct security headers. Static assets and the API share one origin, so there is no CORS configuration at all, and they must not be split.
Headers
Content-Security-Policy: default-src 'self'connect-src 'self'Referrer-Policy: no-referrerX-Content-Type-Options: nosniffStrict-Transport-Securityis not sent — the origin is loopback HTTP by design and HSTS would be meaningless. Subresource Integrity authorises any inline<script>/<style>.A third CSRF layer
keel/web/security.pyalready documents five layers, and this issue must preserve, not rebuild them: loopback binding,Hostvalidation against DNS rebinding, a session token exchanged for aSameSite=StrictHttpOnlycookie, the closed action set, and an HMAC-derived CSRF token described in-source as "the layer that does not depend on the browser being current."Added here, because
SameSitehas had parser bypasses: everyPOST /api/*requiresX-Keel-Client: 1. A custom header forces a CORS preflight a hostile origin cannot satisfy — the attack this closes is the HTML form POST, which is not preflighted. Paired with aSec-Fetch-Site: same-origincheck, which page JavaScript cannot forge.Asset packaging — the part that silently breaks the product
uv_buildpackages the module root —keel/— and everything under it. Correction, measured: theartifactskey is inert on the pinned backend; building with it and withartifacts = []produces wheels with identical contents.docs/is absent because it sits at the repository root, outsidekeel/— which is why/glossaryrenders an empty state in every installed deployment (help_console.py:138-146), and why no glob can fix it.So assert against a built wheel, not a glob. And note the bundle is the half that actually needs work:
keel/freeze.py'scollect_datanames only the template package, so PyInstaller is never told aboutkeel/web/static/and every/static/*request 404s in a frozen app.Ship the static assets in the wheel and in the desktop bundle manifest, or the rewrite works from a checkout and produces a blank page for every real user.
Acceptance
POST /api/*withoutX-Keel-Clientis refused; a cross-origin form POST cannot reach an action.POST /setup/*still works from a plain HTML form with no such header. The rendered pages ship no JavaScript (default-src 'none', noscript-src), so a form cannot set a custom header — requiring one there would refuse the only client that exists./setup/*keeps host validation, theSameSite=Strictcookie, the CSRF token, the closed action set, andSec-Fetch-Site: same-origin. The check widens to/setup/*in Client shell: router, status view, responsive layout, accessibility baseline #536, when the forms becomefetch()calls./setup/*exactly as a browser does — cookie, CSRF token,Origin,Sec-Fetch-Site, noX-Keel-Client— and asserts the action succeeds.