Uh oh!
There was an error while loading. Please reload this page.
fix(web): harden web dashboard auth with Origin/Host validation and tokens - #424
Merged
Jason Robert (jrob5756) merged 2 commits intoAug 13, 2026
Merged
Conversation
…okens Add conductor/web/auth.py: a pure-ASGI OriginHostGuard middleware that enforces Origin/Host validation on every HTTP and WebSocket request, plus per-run token auth on mutating routes and the WebSocket handshake. The token is minted automatically per run, overridable via CONDUCTOR_GATE_TOKEN, and discoverable via a 0600 token file under ~/.conductor/runs/ for CLI commands (guide, gate respond) that need to authenticate without an explicit --token. Read-only routes remain Origin/Host-protected only. Wires the guard into web/server.py and web/replay.py, threads token resolution through cli/app.py, cli/gate.py, and cli/guide.py, and adds frontend support (lib/auth.ts) for presenting the injected token on mutating requests and the WebSocket handshake.
Fixes all 9 blocking findings from the code review: 1. constant_time_match now compares UTF-8 bytes (surrogatepass) instead of raw str, so a non-ASCII presented token (latin-1-decoded Authorization header, or a %FF query token already mangled by errors="replace") 403s instead of crashing the guard with an unhandled TypeError -> 500. Applied the identical fix to server.py's duplicate _gate_token_ok (now delegates to auth.constant_time_match, dropping the dead `import hmac`). 2. The token file is now written with resolve_expected_token(self._token) (the value the guard actually validates against) instead of the raw minted token, so CONDUCTOR_GATE_TOKEN no longer breaks CLI auto-discovery of the dashboard token. 3. remove_token_file now takes an `expected` token and only unlinks when the file's contents match, closing the port-reuse race where a draining stop() could delete a newer run's token file (the same hazard cli/pid.py::remove_pid_file_at already guards). Moved the call above the WebSocket drain and wrapped it in try/except so a failure doesn't skip emitter.unsubscribe(). 4. Frontend: wsDisconnectedSince now starts on the very first failed connection attempt (not just a drop from 'connected'), so the issue #330 stuck-reconnecting banner fires even for a WebSocket that's never once connected. A handshake that never reaches onopen after a successful /api/state fetch sets a new wsAuthFailed flag, which stops the hot exponential-backoff retry and switches the banner's wording to cover an auth rejection. sendGateResponse/sendDialogMessage/ sendDialogDecline/sendIterationLimitResponse now log + set a new wsSendFailed flag instead of silently discarding the send when disconnected; a new SendFailedBanner surfaces it to the user. 5. Added mutation-proof tests: two tests assert every mutating HTTP route and every WebSocket route in the live FastAPI app is actually registered with OriginHostGuard's protected_paths/websocket_paths (catches the "drop a route from the hardcoded test list" mutation), plus wrong-token WS handshake tests (query param and Authorization header) asserting close code 1008 (catches weakening constant_time_match). Verified both mutations described in the review are now caught. 6. Promoted the four copy-pasted _isolated_runs_dir fixtures into one autouse fixture in tests/conftest.py, closing the gap where test_markup_injection.py read the developer's real ~/.conductor/runs. Added a meta-test asserting rundir.runs_dir() is never the real home during the suite. 7. Added anti-clickjacking/anti-sniffing headers (X-Frame-Options: DENY, Content-Security-Policy: frame-ancestors 'none', X-Content-Type-Options, Referrer-Policy) via OriginHostGuard's HTTP pass-through, applied to every response (including rejections), not just GET /. 8. Restored the "### Debugging `--web-bg` failures" heading in AGENTS.md, fixing the dead anchor from docs/cli-reference.md:318. 9. Added a CHANGELOG.md ### Security entry under Unreleased documenting the token-by-default requirement, CONDUCTOR_WEB_ALLOW_ORIGINS, the token file, and the breaking Content-Type requirement. Recommendations adopted: none beyond what the blocking fixes already required (see PR description / task summary for the recommendations evaluated and deliberately skipped). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Jason Robert (jrob5756)
marked this pull request as ready for review
August 13, 2026 00:21
Uh oh!
There was an error while loading. Please reload this page.
Jason Robert (jrob5756)
deleted the
feature/397-harden-web-dashboard-auth
branch
August 13, 2026 00:28
This was referenced Aug 13, 2026
Jason Robert (jrob5756) pushed a commit
that referenced
this pull request
Aug 14, 2026
Resolves the overlap between the Fleet Manager's run-record launch gate (D2) and the two-stage readiness contract main added in #410/#417. Both survive; they are complementary rather than alternatives: - Stage one (`_wait_for_server`) is unchanged. - The parent-side `write_pid_file` main wrote between the stages is replaced by this branch's poll for the *child's* run record. That is a strictly stronger signal -- the child only writes the record once it is executing -- and preserves D2's single-writer invariant, which a reinstated parent-side write would break. - Stage two (`_wait_for_workflow_start`, `/api/info`) is retained, so `workflow_started` / `still_running` still distinguish "listening" from "actually started" and from "already exited". - The stage-two failure cleanup moves from `remove_pid_file_at` to a new `_remove_dead_child_record`, identity-checked on `pid` for the same reason (issue #344): a resumed launch can carry a checkpoint's original `run_id`, so the record under that key may belong to a live process. Test resolution: both sides' new test classes are kept. Main's stage-two tests are re-pointed from the PID file to the run record. Nineteen with-blocks that predate stage two now skip it explicitly -- without that they ran the real 30s `/api/info` wait, taking `test_bg_runner.py` from 2s to 4m33s. Docs: AGENTS.md's `bg_runner.py` bullet describes the merged three-stage sequence; the env-var table keeps main's rewritten `CONDUCTOR_GATE_TOKEN` row (#424) and `CONDUCTOR_WEB_ALLOW_ORIGINS` alongside this branch's `CONDUCTOR_HOME` and `CONDUCTOR_FLEET_NO_ANIM`; CHANGELOG keeps both sides' entries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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 freeto 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
Hardens the web dashboard against cross-origin and unauthenticated access:
conductor/web/auth.py: a pure-ASGIOriginHostGuardmiddleware enforcing Origin/Host validation on every HTTP and WebSocket request, plus per-run token auth on mutating routes and the WebSocket handshake.CONDUCTOR_GATE_TOKEN.~/.conductor/runs/dashboard-<port>.token(mode0600) lets CLI commands (guide,gate respond) discover the token without an explicit--token./api/state,/api/info,/api/logs,/api/gate-status,/api/files/*, replay app) remain Origin/Host-protected only, no token required.lib/auth.ts) presents the server-injected token on mutating requests and the WebSocket handshake.Closes#397
Testing
tests/test_web/test_request_guard.py(new)test_gate_respond_api.py,test_guidance_api.py,test_replay.py,test_server.py,test_gate.py,test_guide.py,test_stop_ladder.py