Files
web-terminal/docs/ROADMAP.md
Yaojia Wang 469037cb94 feat(auth): optional WEBTERM_TOKEN access-token gate (W5)
An OPTIONAL shared token so the app can be used off-LAN (via the relay/tunnel) more
safely than "anyone who reaches the port gets a shell". Sits IN FRONT OF the existing
Origin/CSRF model — never replacing it — and is fully inert when unset.

- src/http/auth.ts (new, pure): constantTimeEqual hashes both inputs to sha256 (32
  bytes) then crypto.timingSafeEqual — no `===`, no length side-channel, never throws.
  parseCookieHeader / cookieIsAuthed / buildSetCookie / isAuthEnabled.
- WEBTERM_TOKEN in config: 16–512 URL/cookie-safe chars or the server refuses to start.
- GET /?token=<t> or POST /auth (rate-limited 10/min) validates → sets
  HttpOnly; SameSite=Strict; Secure-when-https cookie; public/login.html (no <script>).
- When enabled: the WS handshake (AFTER the Origin check, before handleUpgrade) + a
  central authGate over all remote HTTP require the cookie. Open: /login, /auth.
  Loopback bypass scoped to /hook* ONLY (tighter than the plan — gates the local
  browser too).
- Unset ⇒ isAuthEnabled false ⇒ pure passthrough (LAN zero-config unchanged).

Honest tradeoff (in code + CLAUDE.md + login page): a bar-raiser, NOT a TLS/Tailscale
substitute — on bare ws:// the token is cleartext + replayable; only hardens the
TLS-terminated relay path. Verified: typecheck + build:web clean, 2118 pass at
--test-timeout=30000 (disabled-mode regression proven; only the known tmux flake red);
auth.ts 100% line coverage.
2026-07-13 05:25:07 +02:00

161 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Roadmap
> **Status (2026-07-13):** Wave 1-4 (all 8 items) implemented + committed on `develop` — see docs/PROGRESS_LOG.md for the per-feature commits. Remaining: Wave 5 (fan-out board, access token, Android parity) + the deferred backlog.
Prioritized backlog of features to build next, derived from a grounded analysis of
the codebase + the existing planning docs (5 exploration lenses → 24 candidates →
this synthesis). Each item keeps **what it touches** (real files/subsystems) and a
rough **effort** so it's actionable, not aspirational.
**The throughline:** the product already *captures* everything — Claude Code hooks,
statusLine telemetry, the activity timeline, git diff, live-sessions — but under-*acts*
on it. The highest-leverage work turns passive capture into a **trustworthy remote
review-and-drive surface**, without touching the byte-shuttle. Two small primitives
unlock a whole family of features: a **server-side PTY-inject** call and a **preview on
the approval bar**.
Effort key: **S** ≈ 12 days · **M** ≈ 34 days · **L** ≈ 12 weeks. Order = suggested
build sequence (dependencies noted).
---
## Recently shipped (context)
- **Split-grid watch board (v0.8, desktop)** — 1×2/1×3/2×2/2×3 layouts, click-to-focus,
per-quadrant inline approve / maximize / read-only monitor, drag-to-quadrant,
resizable splitters, saved presets. (`public/grid-layout.ts`, `grid-presets.ts`,
`cell-monitor.ts`, `tabs.ts`.)
- **Fixed:** browser worktree-create was 400ing (frontend sent `repoPath`, server reads
`path`); now aligned + regression-tested (`public/projects.ts`, `test/worktree-form.test.ts`).
---
## Wave 1 — ship first (high-trust / high-delight, independent)
- [x] **Approval preview — see-what-you-approve**_(strongest)_
Show the pending `Bash` command or `Edit`/`Write` diff *above* Approve/Reject, so remote
one-tap approval stops being blind (today you could tap Approve on `rm -rf build`).
*The core walk-away trust gap.*
**Touches:** `src/http/hook.ts` (`tool_input` is already parsed at `:104` — derive a
bounded preview for Bash/Edit/Write/MultiEdit) → `/hook/permission` + `pendingApprovals`
in `src/server.ts:423` (attach + re-send to late joiners, like `gate` already does) →
optional bounded `preview` on the `status` ServerMessage in `src/types.ts``manager`
→ approval bar in `public/tabs.ts` (reuse `public/diff.ts` renderer + `sanitizeField`).
Pure side-channel. **Effort: M.** Risk: truncate + strip control chars + cap bytes,
`textContent`/diff-render only, unknown tools fall back to today's name-only bar.
- [x] **Clickable URLs & file paths in the terminal**
Tap the dev-server URL or file path Claude prints instead of soft-keyboard copy gymnastics
(paths reuse `POST /open-in-editor`). TECH_DOC named `@xterm/addon-web-links` in v0.2; never built.
**Touches:** frontend only — `public/terminal-session.ts` (add the addon + a path-matcher
regex → `/open-in-editor`). Zero server change. **Effort: S.**
---
## Wave 2 — the unlocking primitive
- [x] **Server-side PTY-inject + queued follow-up prompt** _(unlocks a family)_
A thin Origin/loopback-guarded route that writes text into a session's PTY, plus a
per-session queue that fires one entry when Claude goes idle ("now run the tests", then
"now open a PR"). Walk-away = give a task and leave; queuing lets the session advance itself.
**Touches:** new `POST /live-sessions/:id/queue` calling the existing `writeInput`
(`src/session/session.ts:201`); a small queue in `manager.ts`; dequeue in the existing
Stop/SessionEnd hook branch (`src/server.ts:414`); FE near `public/quick-reply.ts`.
Injection is identical to a keystroke → byte-shuttle preserved, broadcasts to mirrors.
**Effort: M.** Risk: gate firing on `claudeStatus==='idle'` + settle delay; surface the
queue in UI. **Foundation for** templated launches, auto-continue, issue-intake (backlog).
---
## Wave 3 — read-only side-channel batch (review from the phone)
- [x] **Diff against a base branch** (`?base=<rev>`)
Review a whole agent branch vs `main` before landing, not just uncommitted changes.
`src/http/diff.ts` already deferred this (FR-B1.9) and named its mitigation:
`git rev-parse --verify` allow-list before any revision reaches the CLI.
**Touches:** optional `base` on `getDiff()` (guarded `git rev-parse --verify <base>` +
trailing `--`, then `git diff <base>...`); branch-picker in `public/diff.ts`. **Effort: S/M.**
- [x] **PR + CI/checks status via `gh`** (read-only)
Per-project/session chip: PR state · checks passing · mergeable — glance from the phone,
re-engage only when red. No `gh` usage exists in `src/` yet; `gh` emits JSON (no parsing pain).
**Touches:** new `src/http/gh.ts` (mirror `diff.ts`'s `runGit`); `GET /projects/pr?path=`
guarded by `isValidGitDir`; `PrStatus` in `src/types.ts`; FE chip in project detail.
Capability-probe + empty-degrade when `gh` absent/unauthed. **Effort: M.**
- [x] **Quick wins** (small, cheap, high-delight)
- [ ] **Sync chip on project cards** — ahead/behind + last-commit, folded into the existing
per-repo metadata pass in `src/http/projects.ts` (no new route). **S.**
- [ ] **Cost budget guard + push alert**`costUsd` already flows via statusLine
(`handleStatusLine`); add `COST_BUDGET_USD` + a one-shot latch (like `stuckNotified`) +
warn styling in `public/preview-grid.ts`. The rail that makes unattended auto-continue safe. **SM.**
- [ ] **"While you were away" reconnect digest** — read-side aggregate over `manager.list()`
+ telemetry/timeline/status → "3 done, 1 waiting, $6, 2 PRs" on reconnect. **SM.**
- [ ] **Recent-commits log per project**`git log --oneline -n N` (NUL-delimited) via a
guarded `GET /projects/log`; inert-text render. **SM.**
---
## Wave 4 — close the git loop
- [x] **Worktree lifecycle: remove / prune** (create is now fixed)
Delete losing worktrees + land the winner from any device — closes the create-only loop.
**Touches:** `removeWorktree`/`pruneWorktrees` in `src/http/worktrees.ts` (same execFile
no-shell + timeout, validate target in `git worktree list` & not main, reuse realpath
containment); `DELETE /projects/worktree` + `POST /projects/worktree/prune` (Origin-guarded);
make the existing `locked`/`prunable` tags actionable. **Effort: SM.** Risk: destructive —
require `--force` + confirm for dirty trees, reject the main worktree, safe error messages.
- [x] **Stage / commit / push from the diff viewer**
Claude's done, you reviewed on the phone — now commit + push without typing git into a
mobile terminal. Highest-risk git write; bound the MVP to per-file stage-toggle + commit +
push-current-branch only; **defer discard/checkout**; realpath-contain paths, cap msg length,
push only to existing upstream or `-u`, CSRF-guarded. **Effort: ML.**
---
## Wave 5 — bigger bets
- [ ] **Worktree fan-out board** _(the north star: 真并行不互踩)_
Fan one task across N branch/agent lanes of one repo, watch them race, approve/kill per lane,
keep the winner. **Mostly composition of shipped parts**`createWorktree` + live-sessions +
the split-grid watch board + statusLine gauges + per-quadrant inline approve; server side is a
thin "sessions grouped by repo/worktree" endpoint. Cost is UI. **Effort: L.** Depends on Wave 4.
- [x] **App-level access token** (leave-the-LAN bar-raiser) — shipped on `develop`
A constant-time-compared (`crypto.timingSafeEqual` over SHA-256, fixed-length guard)
`WEBTERM_TOKEN` checked on the WS handshake (alongside, not replacing, Origin) + a central
gate over every remote HTTP route, set as an `HttpOnly; SameSite=Strict; Secure-when-https`
cookie after one-time `GET /?token=` or `POST /auth` (rate-limited 10/min/IP), disabled when
unset (keeps LAN zero-config byte-identical). Charset/length-validated at load; loopback
`/hook*` ingest exempt. `src/http/auth.ts` + wiring in `src/server.ts`.
**Honest tradeoff:** a bar-raiser, **not** a TLS/Tailscale substitute — on bare `ws://` the
token is cleartext and replayable; only hardens the TLS-terminated relay/tunnel path. **Effort: M.**
- [ ] **Android Projects / Diff / Worktree screens** (client parity)
Android's whole v0.6/v0.7 projects-git UI is SDK-gated/off in `settings.gradle.kts`.
Zero server change (the `:api-client` module already speaks the endpoints), but the largest
scope. Sequence *after* the server-side git features so it's one parity pass. **Effort: L.**
---
## Deferred backlog (your own recorded intent, surfaced from the docs)
Consciously punted in the planning docs; several become cheap once the two unlocking primitives
(PTY-inject, approval-preview) land.
| Feature | Deferred in | Note |
|---|---|---|
| Line-level review comments → agent | `FEATURE_WALKAWAY_WORKBENCH.md §B1.6 / US-B1.4` | Tap a diff line, type feedback, composed with `file:line` and sent via `TerminalSession.send`. No new route. Pairs with diff-vs-base. **M.** |
| GitHub issue → new session/worktree | `FEATURE_PROJECT_MANAGER.md §9` | Pick an issue → spawn a session (optionally fresh worktree) with title+body as the prompt. Rides PTY-inject + host `gh`. Treat issue text as untrusted bytes. **M.** |
| Templated one-tap launches (repo + prompt + mode) | implied by `attach.cwd` + Projects launchers | "Triage repo X in plan mode" as one tap. Rides PTY-inject + a template store. **M.** |
| Auto-continue on idle (bounded) | no idle automation today | Opt-in auto-inject "continue" up to N times under a cost ceiling. Rides PTY-inject + budget guard. Build last, low default cap, never auto-approve. **SM.** |
| Cross-session cost rollup / trends | `FEATURE_WALKAWAY_WORKBENCH.md §B2.6` | Bounded ring beside the latest-only field + `GET /telemetry/summary` + dashboard panel. Overlaps the budget-guard quick win. **M.** |
| Mission Control: cross-session feed + durable run log | (no cockpit persistence today) | Merged reverse-chron feed + append-only on-disk tail (reuse `subscription-store.ts` JSONL/atomic write, byte-capped) so overnight runs survive restart. The "while you were away" digest is its first slice. **ML.** |
| Per-project task backlog | task tracking unbuilt | Per-repo TODO store mirroring `prefs-store.ts`; one tap spawns Claude pre-filled. Local state, **not** a GitHub Issues sync (YAGNI). **M.** |
| Host tmux session discovery + attach | user request (this is not raw-iTerm; needs tmux) | We already run as a tmux client on the user's default tmux server (sessions `web_<id>`), so `tmux ls` already sees manual/iTerm tmux sessions. Add a discovery list + "attach external tmux session" entry in the launcher → spawn a client PTY `tmux attach -t <name>`, wrapped in the session model. **SM.** |
---
_Kept in sync by the maintainer. Ideas are grounded against the code as of `develop` — verify
file/line references before starting (they drift)._