Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,8 @@ When startup succeeds, `mw` opens the web page automatically at the serving URL

When Portal is enabled, the startup output includes:
```
Portal dashboard at:
http://0.0.0.0:12345/
Tailscale: https://my-machine.tail-scale.ts.net/
[portal] Portal dashboard at: http://0.0.0.0:12345/
[portal] Tailscale URL: https://my-machine.tail-scale.ts.net/
```

myworktree uses the **current working directory** to detect the target repo (git root) and derives an isolated per-project data dir from it, so you can manage other projects by running the same binary in a different repo directory.
Expand Down Expand Up @@ -174,10 +173,11 @@ myworktree instance list
myworktree instance stop <instanceId>

# config (global auth token)
mw config # interactive guided setup (set/view/clear token)
mw config # interactive guided setup (set/view/clear/regen token)
mw config set-auth # set token directly
mw config get-auth # view token (masked)
mw config clear-auth # clear token
mw config regen # regenerate token (with confirmation)

# start with remote access & Portal (IPv6 is explicitly disabled)
mw start --listen 0.0.0.0:0 # LAN access, auto-inherits global token
Expand Down Expand Up @@ -238,18 +238,18 @@ Configure a global auth token once, and all instances automatically inherit it:

```bash
mw config
# → Interactive guided setup: [1] set token [2] view token [3] clear token [q] quit
# → Interactive guided setup: [1] set token [2] view token [3] clear token [4] regen token [q] quit
# Token stored in ~/.config/myworktree/auth.json (0600 permissions)
```

The token is stored as plaintext in `auth.json` (0600 permissions). Instance-level `--auth` override takes precedence over the global token.
When `--auth` is not provided and no token exists in `auth.json`, the CLI **automatically generates a random 32-character hex token** and persists it. This ensures instances always have authentication enabled by default. Instance-level `--auth` override takes precedence over the global token.

### Portal Dashboard

`mw start --listen 0.0.0.0:0` starts a **Portal Dashboard** on port `12345` (configurable via `--portal-port`). The dashboard:

- Lists all running instances across repos with auto-discovery
- Click an instance to jump to its Web UI through the Portal reverse proxy
- Click an instance to jump to its Web UI (directly via instance port — Portal reverse proxy `/s/<repo-hash>/` is planned but not yet implemented)
- Uses **HttpOnly Cookie** (`mw_token`) for authentication — token never appears in URL or JS
- **CSRF protection** via double-submit cookie pattern on login/logout endpoints
- Cookie has 24-hour **sliding expiration** (refreshed on each auth-successful request)
Expand All @@ -258,7 +258,7 @@ Set `--portal-port 0` to disable the Portal.

### Tailscale HTTPS

When Tailscale is installed, the Portal holder automatically configures `tailscale serve` to provide HTTPS access via `https://<machine>.ts.net`. This is fully automated — zero manual configuration.
> **Note**: Automatic `tailscale serve` configuration is **currently disabled** due to a Tailscale 1.98 CLI bug on macOS where `tailscale serve --bg` reports success but does not actually configure the proxy. The relevant code exists in `internal/portal/portal.go` but is not wired into the production code paths. Users can still securely access Portal via Tailscale IP (`http://100.x.x.x:12345`) over the WireGuard tunnel. Automatic `tailscale serve` management will be re-enabled once Tailscale fixes the upstream bug.

### Network Security

Expand Down
16 changes: 8 additions & 8 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,8 @@ mw

当 Portal 启用时,启动输出包含:
```
Portal dashboard at:
http://0.0.0.0:12345/
Tailscale: https://my-machine.tail-scale.ts.net/
[portal] Portal dashboard at: http://0.0.0.0:12345/
[portal] Tailscale URL: https://my-machine.tail-scale.ts.net/
```

myworktree 会用**当前工作目录**定位目标项目(git root),并基于该 git root 计算独立的数据目录,因此要管理其他项目时,只需要在另一个项目仓库目录下运行同一个 myworktree 二进制即可。
Expand Down Expand Up @@ -174,10 +173,11 @@ myworktree instance list
myworktree instance stop <instanceId>

# config(全局认证 Token)
mw config # 交互式引导(设置/查看/清除 Token)
mw config # 交互式引导(设置/查看/清除/重新生成 Token)
mw config set-auth # 直接设置 Token
mw config get-auth # 查看 Token(掩码显示)
mw config clear-auth # 清除 Token
mw config regen # 重新生成 Token(需确认)

# 启动并启用远程访问 + Portal
mw start --listen 0.0.0.0:0 # LAN 访问,自动继承全局 Token
Expand Down Expand Up @@ -236,18 +236,18 @@ GitHub Actions(`.github/workflows/go-ci.yml`)会在以下场景运行:

```bash
mw config
# → 交互式引导:[1] 设置 Token [2] 查看 Token [3] 清除 Token [q] 退出
# → 交互式引导:[1] 设置 Token [2] 查看 Token [3] 清除 Token [4] 重新生成 Token [q] 退出
# Token 存储在 ~/.config/myworktree/auth.json(0600 权限)
```

Token 以明文存储在 `auth.json` 中(0600 权限)。实例级别 `--auth` 参数优先级高于全局 Token。
当未提供 `--auth` 且 `auth.json` 中无已有 Token 时,CLI 会**自动生成一个随机的 32 字符 hex Token** 并持久化。这确保实例默认启用认证。实例级别 `--auth` 参数优先级高于全局 Token。

### Portal 仪表板

`mw start --listen 0.0.0.0:0` 会在端口 `12345` 启动 **Portal 仪表板**(可通过 `--portal-port` 自定义)。仪表板功能:

- 自动发现并列出所有跨仓库运行中的实例
- 点击实例通过 Portal 反向代理跳转到其 Web UI
- 点击实例直接跳转到其 Web UI(通过实例端口直连——Portal 反向代理 `/s/<repo-hash>/` 计划中但尚未实现)
- 使用 **HttpOnly Cookie**(`mw_token`)进行认证——Token 不出现在 URL 或 JS 中
- 登录/登出端点采用 **CSRF 防护**(double-submit cookie 模式)
- Cookie 具备 24 小时**滑动过期**机制(每次认证成功的请求自动刷新有效期)
Expand All @@ -256,7 +256,7 @@ Token 以明文存储在 `auth.json` 中(0600 权限)。实例级别 `--auth

### Tailscale HTTPS

当 Tailscale 已安装时,Portal 持有者自动配置 `tailscale serve`,提供 HTTPS 访问(`https://<machine>.ts.net`)。全程自动化,无需手动配置
> **说明**:自动 `tailscale serve` 配置功能**当前已禁用**,原因是 macOS 上 Tailscale 1.98 CLI 存在 bug:`tailscale serve --bg` 返回成功但实际未配置代理。相关代码存在于 `internal/portal/portal.go` 中但未接入正式代码路径。用户仍可通过 Tailscale IP(`http://100.x.x.x:12345`)经 WireGuard 隧道安全访问 Portal。待 Tailscale 修复上游 bug 后将重新启用自动 `tailscale serve` 管理功能

### 网络安全

Expand Down
18 changes: 17 additions & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Auth:
- For Portal dashboard access, the `mw_token` HttpOnly Cookie is used as the third token source (automatically sent by browser after `/api/auth` login).
- Token priority: `Authorization` header → `?token=` query → `mw_token` Cookie.
- Prefer the `Authorization` header when possible so tokens do not end up in browser history or shell history.
- **Auto-generate token**: When `--auth` is not provided and no token exists in `auth.json`, the CLI automatically generates a random 32-char hex token, persists it to `~/.config/myworktree/auth.json`, and uses it as the instance auth token. This ensures instances always have auth enabled by default.

Common response header:
- `X-Myworktree-Server-Rev: <rev>` is returned by API and UI responses.
Expand Down Expand Up @@ -592,9 +593,11 @@ Always returns 200 (idempotent — successful even if not logged in).
Errors:
- `403`: CSRF token invalid/expired/used or missing

### Reverse proxy (access instance)
### Reverse proxy (access instance) — planned, not yet implemented
`ANY /s/<repo-hash>/*`

> **Status: Planned but not yet implemented.** Currently the Portal dashboard links directly to instance ports (`http://<host>:<port>/`) instead of using the reverse proxy path. The `/s/<repo-hash>/*` route handler is not registered in the Portal HTTP server.

**Authentication required** (Cookie `mw_token` or Bearer token). Each successful request refreshes the `mw_token` Cookie's expiration (sliding).

Proxies the request to the corresponding instance at `http://127.0.0.1:<port>`. Since the proxy connects via loopback, the instance's auth middleware automatically bypasses token validation.
Expand All @@ -608,6 +611,19 @@ Errors:
- `401`: Not authenticated
- `502`: Target instance offline

### Instance login page (instance-level, HTML)
`GET /login`, `POST /login`

**No authentication required** — this is the login page itself.

The instance server serves an HTML login form at `/login` for browser-based authentication (separate from the Portal JSON API). Non-loopback browser requests that lack valid auth are redirected to this page.

- `GET /login` — Returns an HTML login page with password input and form. If the user already has a valid `mw_token` Cookie that matches the global auth config, they are redirected to `/` immediately.
- `POST /login` — Accepts `token` and optional `next` form fields. On successful auth, sets `mw_token` HttpOnly Cookie (Max-Age=86400, SameSite=Lax, Secure on HTTPS) and redirects to the `next` path (or `/` if not provided). **Note**: `/login` is explicitly exempt from `withAuth` middleware rate limiting; no per-IP rate limits apply to this endpoint.

Errors:
- `401`: Invalid token

## 8) LLM 配置
### 获取当前配置
`GET /api/llm/config`
Expand Down
4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ It does **not** analyze project code or prevent concurrent write conflicts insid
- `internal/monitor/` — resource stats collector (CPU delta via gopsutil/process.Times, memory via RSS)
- `internal/llm/` — LLM API client(OpenAI / Anthropic / OpenAI Compatible),可选,LLM Settings 通过 Web UI 对话框配置
- `internal/config/` — global auth configuration (read/write `auth.json`)
- `internal/portal/` — Portal dashboard (port claiming, instance registry, CSRF state management, HTTP endpoints, reverse proxy; tailscale serve automation code is defined but **currently unused** due to tailscale CLI bug)
- `internal/portal/` — Portal dashboard (port claiming, instance registry, CSRF state management, HTTP endpoints; **reverse proxy `/s/<repo-hash>/` planned but not yet implemented** — current dashboard links point to instance ports directly; tailscale serve automation code is defined but **currently unused** due to tailscale CLI bug)
- `internal/ui/` — embedded static UI.

## 3. Data & persistence
Expand Down Expand Up @@ -359,7 +359,7 @@ myworktree implements a **dual-layer authentication architecture**:
- Optional built-in HTTPS via `--tls-cert/--tls-key`
- Redaction on stored backlog (e.g. `sk-...`)

**Proxy authentication bypass**: Portal reverse proxy forwards requests to instances via `127.0.0.1` (loopback), so instances automatically skip auth — users never need to manually pass tokens to individual instances.
**Proxy authentication bypass (planned)**: The planned Portal reverse proxy (`/s/<repo-hash>/`) will forward requests to instances via `127.0.0.1` (loopback), so instances automatically skip auth. **Currently not yet implemented** — dashboard links connect to instance ports directly.

**Tailscale**: WireGuard tunnel provides network-layer encryption. ~~Portal holder automatically manages `tailscale serve` for HTTPS domain access (`https://<machine>.ts.net`) with Let's Encrypt certificates.~~ **Currently disabled** — tailscale CLI `serve` command on macOS returns success but does not actually configure the proxy. The `tailscaleServeLoop` goroutine and related `cleanupStaleTailscaleServe()` call are removed from the production code path. Users can still securely access Portal via Tailscale IP (`http://100.x.x.x:12345`) over the WireGuard tunnel.

Expand Down
18 changes: 10 additions & 8 deletions docs/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,16 @@
- 命名冲突:如目标分支已存在,自动给 `<name>` 加 `-2/-3` 后缀避免冲突;并支持将既有 worktree **纳入管理(import)**。

## 6. 安全
- 默认只监听 `127.0.0.1`
- 监听非 loopback(例如 `0.0.0.0` 或局域网 IP)时:必须提供 `--auth`
- 默认监听 `0.0.0.0:0`,自动选择端口并持久化
- 启动时若未通过 `--auth` 显式提供 Token,则自动生成 32 位随机 Token 并持久化到 `auth.json`(0o600 权限),确保认证始终启用
- 可选内置 HTTPS:`--tls-cert/--tls-key`。
- **Portal 端口绑定**:Portal 仪表板可绑定到 `0.0.0.0`,用户通过 LAN IP 或 Tailscale 域名访问。非 loopback 访问 Portal 时须通过 `mw_token` Cookie 认证。
- **全局 Token(HttpOnly Cookie + CSRF)**:全局 Token 存储在 `~/.config/myworktree/auth.json`(0600 权限),通过 `mw config` 交互式配置。Portal 仪表板使用 HttpOnly Cookie(`mw_token`)传输 Token——JS 不可读取,防止 XSS 窃取。登录/登出端点采用 double-submit cookie 模式做 CSRF 防护。
- **速率限制**:实例端认证失败限流 20 次/分钟/IP;Portal 端 CSRF token 生成限流 1 req/s/IP,认证尝试限流 20 次/分钟/IP。
- **Tailscale HTTPS**:~~当 Tailscale 可用时,Portal 持有者自动配置 `tailscale serve` 提供 `https://<machine>.ts.net` 域名访问(Let's Encrypt 证书)。~~ **(暂未启用)** 在 macOS 下测试发现 tailscale 1.98 CLI 的 `serve` 命令存在 bug:`tailscale serve --bg <port>` 返回成功但实际未配置代理、`tailscale serve status` 始终报告 No serve config。相关代码(`tailscaleServeLoop`、`ensureTailscaleServe` 等)已封存不调用,待 tailscale 修复后恢复。Tailscale WireGuard 隧道本身提供网络层加密,仍可通过 `http://100.x.x.x:PORT` 安全访问。
- 涉及宿主机图形界面的快捷动作(例如从侧栏直接打开 Terminal / Finder)只在浏览器通过 `127.0.0.1` / `localhost` 访问时展示;远程访问时隐藏,避免误导用户在远端会话里触发本机 GUI 行为。
- 对应后端接口也强制仅接受 loopback 客户端请求,不能只依赖前端隐藏来形成安全边界。
- Loopback 判断仅支持 IPv4(`127.x.x.x` / `localhost`);IPv6 地址(含 `::1`)不被识别为 loopback,需走完整的 Token 认证流程。
- 日志/回放脱敏:
- env 键名包含 `TOKEN/SECRET/KEY/PASSWORD` 的值写入状态时替换为 `***`。
- 输出回放中按模式脱敏主流 AI key(如 `sk-***`)。
Expand All @@ -59,13 +61,13 @@
- WebSocket 握手协议:服务端发送 `{"type":"ready"}`,客户端等待后发送 resize 开始数据流。
- 窗口尺寸传递:前端监听窗口 resize 并通知后端 PTY,确保 TUI 程序正确重绘。
- 智能重绘:前端在收到第一条数据后延迟 50ms 再次发送 resize,确保 TUI 完整刷新。
- 超时降级:5 秒握手超时后自动降级到 SSE 方案。
- 超时降级:客户端若在 5 秒内未收到 WebSocket `ready` 握手消息,则自动关闭 WebSocket 并回退到 SSE 方案。
- 运行中的实例在前端按实例维护各自的终端会话;切换标签时隐藏非活动终端,而不是强制断开其 PTY 连接。
- 终端配置:Web TTY 的缓冲区(scrollback)、主题、字体等参数由前端灵活配置,以适应不同的调试和使用场景。
- 规划增强:**Portal Dashboard MVP**(已实现):
- 全局 Token 配置(`mw config` 交互式引导,`~/.config/myworktree/auth.json`,`0o600` 权限)
- Portal 仪表板(共享入口端口,自动发现所有仓库的运行实例,HttpOnly Cookie 认证,CSRF 防护)
- 反向代理(通过 Portal 统一入口访问各实例,解决跨域 Cookie 问题,支持 WebSocket)
- ~~反向代理(通过 Portal 统一入口访问各实例,解决跨域 Cookie 问题,支持 WebSocket)~~ **(暂未实现)**:计划通过 `/s/<repo-hash>/` 路径代理到对应实例端口,当前仪表板链接直接指向实例端口
- ~~Tailscale Serve 自动管理(自动配置 `tailscale serve` 提供 `https://<machine>.ts.net` 域名访问)~~ **(暂未启用,见 §6 安全说明)**
- 双层认证架构(Portal 层 Cookie + CSRF,实例层 loopback 绕过)
- 浏览器关闭保护:前端在 `beforeunload` 事件时,无论是否存在运行中实例,均触发浏览器原生确认对话框,防止误操作关闭页面。
Expand All @@ -77,12 +79,12 @@
- UI 重连可看到所有已管理对象,并能回放 instance 近期输出。
- 本机访问 Web UI 时,可从侧栏一键打开所选主工作区/worktree 的 Terminal 与 Finder;远程访问时不展示这两个快捷入口。
- **Portal Dashboard MVP**:
- `mw config` 交互式引导可完成全局 Token 的配置、查看(掩码)、清除
- `mw config` 交互式引导可完成全局 Token 的配置、查看(掩码)、清除、重新生成
- `mw start --listen 0.0.0.0:0` 自动启动 Portal 仪表板,多实例中仅一个持有 Portal 端口
- Portal 仪表板可通过 LAN IP 和 Tailscale 域名访问,显示所有运行实例并可点击跳转
- Cookie 认证流程:获取 CSRF token → 提交 auth → 获得 HttpOnly Cookie → 访问实例列表/代理
- 实例通过 Portal 反向代理访问时,loopback 请求自动绕过实例端 auth 中间件
- 反向代理支持 WebSocket 升级转发
- Cookie 认证流程:获取 CSRF token → 提交 auth → 获得 HttpOnly Cookie → 访问实例列表
- ~~实例通过 Portal 反向代理访问时,loopback 请求自动绕过实例端 auth 中间件~~ **(暂未实现)**
- ~~反向代理支持 WebSocket 升级转发~~ **(暂未实现)**
- ~~Tailscale 可用时自动配置 `tailscale serve`,提供 HTTPS 域名访问~~ **(暂未启用)**
- Portal 持有者崩溃后,其他实例在 10~15 秒内完成故障转移接管
## 9. LLM 智能分支命名
Expand Down
27 changes: 19 additions & 8 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"syscall"
"time"

"myworktree/internal/config"
"myworktree/internal/gitx"
"myworktree/internal/instance"
"myworktree/internal/llm"
Expand Down Expand Up @@ -584,9 +585,12 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
token := extractAuthToken(r)
if token == s.cfg.AuthToken {
http.Redirect(w, r, "/", http.StatusFound)
return
if token != "" {
cfg, _ := config.Load()
if cfg != nil && token == cfg.AuthToken {
http.Redirect(w, r, "/", http.StatusFound)
return
}
}
loginHTML := `<!DOCTYPE html>
<html lang="en">
Expand Down Expand Up @@ -668,7 +672,8 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
return
}
token := strings.TrimSpace(r.FormValue("token"))
if token != s.cfg.AuthToken {
cfg, err := config.Load()
if err != nil || token != cfg.AuthToken {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
Expand Down Expand Up @@ -1618,9 +1623,6 @@ func (s *Server) handleMCPCall(w http.ResponseWriter, r *http.Request) {
}

func (s *Server) withAuth(next http.Handler) http.Handler {
if strings.TrimSpace(s.cfg.AuthToken) == "" {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isLoopbackRequest(r) {
next.ServeHTTP(w, r)
Expand All @@ -1634,8 +1636,17 @@ func (s *Server) withAuth(next http.Handler) http.Handler {
next.ServeHTTP(w, r)
return
}
cfg, err := config.Load()
if err != nil {
http.Error(w, "auth config unavailable", http.StatusServiceUnavailable)
return
}
if strings.TrimSpace(cfg.AuthToken) == "" {
next.ServeHTTP(w, r)
return
}
token := extractAuthToken(r)
if token != s.cfg.AuthToken {
if token != cfg.AuthToken {
if !s.allowAuthAttempt(clientIP(r.RemoteAddr)) {
http.Error(w, "too many unauthorized attempts", http.StatusTooManyRequests)
return
Expand Down
Loading