Remote Access GUI Toggle

Status:approved
Date:2026-07-12
Branch:feature/remote-access-toggle
Type:spec

Goal

A Remote access card on the dashboard's Settings > Advanced tab: a master toggle (Allow remote access) plus a scope picker (Tailscale only, recommended/default, vs All interfaces). Flipping it persists to the settings DB and immediately restarts the service with the new bind. The settings DB becomes the single source of truth for the bind host; CLI --host becomes a one-shot explicit override that is never persisted. Autostart artifacts stop baking --host, so reboots and upgrades honor whatever the GUI last said.

Decisions Already Made (do not re-litigate)

DecisionChoice
Exposure scopeBoth, as a GUI choice: "Tailscale only" (loopback + 100.x, default, recommended badge) and "All interfaces" (0.0.0.0, loud warning styling).
When the bind change appliesImmediately: persist, then self-restart. "Restarting…" overlay, health-poll back in.
Source of truthSettings DB. Precedence: explicit CLI --host (one-shot, never persisted) > DB setting > loopback default.
PlacementSettings > Advanced tab. NOT the Features tab: features (jacked/api/routes/features.py) toggle Claude Code assets on disk; this is jacked's own server runtime.
Confirm dialogsEnable shows the real security story (no auth layer, Tailscale ACL is the access control). "All interfaces" gets a louder variant. Remote-origin + turning OFF gets a lockout warning.
No new dependenciesTailscale detection is stdlib + optional tailscale CLI shell-out. No psutil/netifaces.

Pre-existing Bugs This Fixes (verified first-hand)

Architecture

flowchart TD
    GUI[Settings > Advanced card] -- PUT /api/settings/remote-access --> DB[(settings table\nremote_access_enabled\nremote_access_scope)]
    GUI -- POST /api/settings/remote-access/restart --> RS[restart: execv POSIX /\ndetached respawn Windows]
    RS --> BOOT[service boot]
    CLI[explicit --host flag\none-shot override] --> RES
    BOOT --> RES[resolve_bind in jacked/service/bind.py\nflag > DB > loopback]
    DB --> RES
    RES --> DET[Tailscale IP detection\nUDP route trick + CLI fallback]
    RES --> SOCK[pre-bound socket set\nSO_REUSEADDR]
    SOCK --> UV[uvicorn Server.run sockets=...]
    RES --> ENV[JACKED_HOST env\nCORS/CSRF/lifespan]
  
flowchart TD
    GUI[Settings > Advanced card] -- PUT /api/settings/remote-access --> DB[(settings table)]
    GUI -- POST /api/settings/remote-access/restart --> RS[restart]
    RS --> BOOT[service boot]
    CLI[explicit --host flag] --> RES
    BOOT --> RES[resolve_bind: flag > DB > loopback]
    DB --> RES
    RES --> DET[Tailscale IP detection]
    RES --> SOCK[pre-bound socket set]
    SOCK --> UV[uvicorn Server.run sockets=...]
    RES --> ENV[JACKED_HOST env]
  

Bind Resolution — new module jacked/service/bind.py

resolve_bind(cli_host: str | None, port: int) -> BindPlan
# precedence: explicit CLI --host (one-shot, never persisted)
#             > DB setting (remote_access_enabled/scope) > loopback default
# BindPlan: mode ('loopback'|'tailscale'|'all'), sockets spec,
#           primary_host (feeds JACKED_HOST), tailscale_ip | None,
#           fallback_reason: str | None (tailscale requested but not detected)

Tailscale IP Detection (stdlib-only)

Storage and API

Restart Mechanics

Frontend (Settings > Advanced)

Autostart Artifacts and Migration

Security Posture

File Structure

FileChange
jacked/service/bind.pyNEW: resolve_bind, BindPlan, socket-set construction, tailscale detection.
jacked/api/routes/settings_remote.pyNEW: GET/PUT remote-access, POST restart.
jacked/api/main.pyRegister new router before system router; store BindPlan on app.state.
jacked/api/routes/system.pyProtected keys += remote_access_*.
jacked/service/tray.py_start_uvicorn → sockets; readiness checks follow socket set; _on_toggle_autostart drops host.
jacked/service/platform.pyGenerators drop --host; install_autostart bootout/bootstrap; migration parser.
jacked/cli.pywebux/start/service host defaults → None + resolve_bind; _spawn_service_detached conditional --host; service install/restart write DB.
jacked/data/web/js/components/settings.jsRemote access card on Advanced tab.
jacked/data/web/js/websocket.js, header.jsrestart_started event → restart modal/polling; lockout terminal state.
tests/…See Testing Plan.
README.mdRemote Access section rewritten GUI-first; CLI as alternative.

Testing Plan

Milestones (build order)

  1. bind.py: resolve_bind + BindPlan + socket sets + tailscale detection, fully unit-tested.
  2. Serving paths: tray + webux on Server.run(sockets=...); JACKED_HOST fed from BindPlan; readiness checks fixed; multi-socket integration test green.
  3. API: settings_remote router (GET/PUT/restart) + protected keys + restart mechanics (POSIX/Windows) + restart_started WS event.
  4. Frontend: Advanced-tab card, dialogs, status line, lockout guard, restart overlay; JS tests.
  5. Autostart: generators drop --host, bootout/bootstrap fix, data-preserving migration, CLI parity (service install/restart write DB).
  6. Docs: README Remote Access rewritten GUI-first; CLI help text updated.
  7. Gates: full local CI, browser QA of the flow, security review, code review; PR.

Generated with the jacked HTML artifact template. Edit freely.