Serve CORS so the cockpit can reach the daemon (D-3) - #68
Merged
Conversation
fleetd served no CORS headers at all - OPTIONS /missions answered 405 and no
response carried an Access-Control-Allow-Origin. Every browser fetch from the
cockpit failed, which does not merely degrade the FLEET ops grid: store
.reconnect() calls listUnits() over HTTP before opening any per-unit stream, so
a missing layer blocks discovery entirely and the grid renders empty. Filed as
a pre-existing main defect on 2026-08-16, blocking smoke items 1.2, 1.4a and
1.7; re-verified still real on 2026-09-07.
The origin list is an allowlist, deliberately never Any. fleetd binds loopback
but takes commands - POST /missions, POST /units/:id/commands - and loopback is
reachable from every website the operator's browser visits. The bind address is
not a security boundary against a browser, so a permissive origin would let any
page drive the daemon. Defaults cover the Tauri webview on both platforms
(the origin differs) and the Vite dev server; FLEETD_ALLOWED_ORIGINS overrides.
Credentials stay disabled: the daemon has no cookie or Authorization auth, so
allowing them would only widen what a permitted origin can do.
tower-http is the one crate this adds to the graph - tower 0.5.3 was already
resolved via axum, and the lock diff confirms tower-http is the only new entry.
Hand-rolling the headers was the alternative and was rejected: Vary: Origin is
easy to omit and a missing one is a cache-poisoning bug, and preflight has
enough corners to be worth a maintained implementation. The tower dev-dependency
for ServiceExt::oneshot adds nothing, being already in the lock.
Three tests, driven red in both directions rather than one:
layer removed (the D-3 state) -> preflight and allowed-origin FAIL, while
the unlisted-origin test correctly passes,
since no headers at all is right there
allowlist widened to Any -> the unlisted-origin test FAILS
The second is the one worth keeping: it is what stops a future "just make CORS
work" change from handing every website on the internet a command channel.
fleetd 87 -> 90 tests. Workspace check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
fleetd served no CORS headers at all —
OPTIONS /missionsanswered405, and no response carried anAccess-Control-Allow-Origin.This does not merely degrade the FLEET ops grid.
store.reconnect()callslistUnits()over HTTP before opening any per-unit stream, so a missing layer blocks discovery entirely and the grid renders empty. Filed as a pre-existingmaindefect on 2026-08-16 (blocking smoke items 1.2 / 1.4a / 1.7); re-verified still real 2026-09-07.The allowlist is the point
Deliberately never
Any. fleetd binds loopback but takes commands —POST /missions,POST /units/:id/commands— and loopback is reachable from every website the operator's browser visits. The bind address is not a security boundary against a browser; with a permissive origin, any page could drive the daemon.Defaults cover the Tauri webview on both platforms (the origin differs —
tauri://localhostvshttp://tauri.localhost) and the Vite dev server.FLEETD_ALLOWED_ORIGINSoverrides with a comma-separated list. Credentials stay disabled: the daemon has no cookie orAuthorizationauth, so allowing them would only widen what a permitted origin can do.Dependency cost, measured
tower-httpis the one crate this adds —tower0.5.3 was already resolved via axum, and the lock diff confirms it:Hand-rolling the headers was the alternative and was rejected:
Vary: Originis easy to omit and a missing one is a cache-poisoning bug, and preflight has enough corners to be worth a maintained implementation. Thetowerdev-dependency forServiceExt::oneshotadds nothing, being already in the lock.Driven red in both directions
The second is the one worth keeping — it is what stops a future "just make CORS work" change from handing every website on the internet a command channel.
fleetd 87 → 90 tests. Workspace check clean.
🤖 Generated with Claude Code