diff --git a/CLAUDE.md b/CLAUDE.md index 0329456..b56712e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -128,11 +128,11 @@ source .venv/bin/activate && pip install -r requirements.txt *Notă istorică:* `memory/` era symlink la repo-ul legacy Clawdbot; consolidat în echo-core în migrația OpenClaw (2026-04). -**Dashboard** (`dashboard/`): Echo Task Board — HTTP API + UI static servit de `dashboard/api.py` pe portul 8088, de obicei în spatele unui reverse proxy la `/echo/`. Logica endpoint-urilor în mixin-uri `dashboard/handlers/*.py`; path-uri centralizate în `dashboard/constants.py`. Template systemd user unit la `dashboard/echo-taskboard.service`. `workspace.html` e hub-ul unificat de proiecte (fostul ralph.html + workspace.html); `/echo/ralph.html` → 302 redirect la `/echo/workspace.html`. Autentificare prin cookie httpOnly `dashboard=`; `DASHBOARD_TOKEN` în `dashboard/.env`. +**Dashboard** (`dashboard/`): Echo Task Board — HTTP API + UI static servit de `dashboard/api.py` pe portul 8088, de obicei în spatele unui reverse proxy la `/echo/`. Logica endpoint-urilor în mixin-uri `dashboard/handlers/*.py`; path-uri centralizate în `dashboard/constants.py`. Template systemd user unit la `dashboard/echo-taskboard.service`. `workspace.html` e hub-ul unificat de proiecte (fostul ralph.html + workspace.html); `/echo/ralph.html` → 302 redirect la `/echo/workspace.html`. Autentificarea e **dezactivată implicit** (acces doar prin tailnet); se reactivează cu `DASHBOARD_AUTH=on` în `dashboard/.env`. ## Dashboard — Note arhitecturale -**Cookie auth:** httpOnly cookie `dashboard=...`; SameSite=Strict; Path=/echo/. EventSource SSE trimite cookie-ul automat. `DASHBOARD_TOKEN` din `dashboard/.env` — setează o dată, restart service. Resetare: schimbă valoarea + restart. +**Cookie auth (off by default):** `DASHBOARD_AUTH` nesetat ⇒ `_check_dashboard_cookie` trece mereu, `/echo/login` redirectează direct la dashboard, POST-urile `/api/*` nu mai cer cookie. Motiv: `tailscale serve` expune `/echo` doar în tailnet, deci autentificarea era dublată. Cu `DASHBOARD_AUTH=on` în `dashboard/.env` revine login-ul: httpOnly cookie `dashboard=...`; SameSite=Strict; Path=/echo/; EventSource SSE trimite cookie-ul automat; `DASHBOARD_TOKEN` din `dashboard/.env` — setează o dată, restart service. Resetare: schimbă valoarea + restart. **jsonlock helper (`src/jsonlock.py`):** `read_locked(path)` / `write_locked(path, mutator)` pentru orice scriere la `approved-tasks.json`, `sessions/*.json`. Lock pe sidecar `.lock` (inode stabil chiar și după os.replace). Ordine canonică lock-uri: alfabetic după filename. Re-entrant (threading.local refcount). Expune și `LockTimeoutError`. diff --git a/dashboard/handlers/auth.py b/dashboard/handlers/auth.py index 9c6766e..236e006 100644 --- a/dashboard/handlers/auth.py +++ b/dashboard/handlers/auth.py @@ -7,6 +7,11 @@ This mixin provides: - _check_dashboard_cookie — used by the global POST middleware (and the SSE GET endpoint) to gate access. +Authentication is **disabled by default** — the dashboard is only reachable +over the Tailscale tailnet, which already authenticates the caller, so the +extra token prompt added nothing. Set `DASHBOARD_AUTH=on` in `dashboard/.env` +to re-enable the token login (everything below still works unchanged). + `DASHBOARD_TOKEN` is read once from `dashboard/.env` (loaded into `os.environ` by `dashboard/constants.py` at import time). When the token is not configured we generate a random one at startup, stash it in-process, @@ -34,6 +39,17 @@ _COOKIE_PATH = "/echo/" _DASHBOARD_TOKEN: str | None = None +def _auth_enabled() -> bool: + """True only when `DASHBOARD_AUTH` is explicitly turned on. + + Off by default: access control is delegated to Tailscale. When off, + `_check_dashboard_cookie` always passes and `/echo/login` is never shown. + """ + return os.environ.get("DASHBOARD_AUTH", "").strip().lower() in { + "on", "1", "true", "yes", + } + + def _get_dashboard_token() -> str: """Return the dashboard token (cached). Generates a random one if absent. @@ -79,7 +95,12 @@ class AuthHandlers: # ── helpers ──────────────────────────────────────────────────────── def _check_dashboard_cookie(self) -> bool: - """Return True if the request carries a valid `dashboard` cookie.""" + """Return True if the request carries a valid `dashboard` cookie. + + Always True when auth is disabled (the default) — see `_auth_enabled`. + """ + if not _auth_enabled(): + return True raw = self.headers.get("Cookie", "") or "" cookies = _parse_cookie_header(raw) provided = cookies.get(_COOKIE_NAME, "") @@ -114,6 +135,15 @@ class AuthHandlers: SameSite=Strict; Path=/echo/ so it scopes to the dashboard reverse proxy mount. """ + if not _auth_enabled(): + # Nothing to log into — bounce straight to the dashboard. + self.send_response(302) + self.send_header("Location", "/echo/workspace.html") + self.send_header("Content-Length", "0") + self.send_header("Cache-Control", "no-store") + self.end_headers() + return + # Accept JSON body too (login.html might POST JSON in Lane B2) ctype = (self.headers.get("Content-Type", "") or "").lower() if "application/json" in ctype: