Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4d2deac
docs: add Portal Dashboard implementation plan
linletian May 22, 2026
d0eef64
docs: add Portal Dashboard task breakdown
linletian May 22, 2026
593ac90
docs: add Task 0 for project README updates (文档优先)
linletian May 22, 2026
f368be7
docs: add step 0 for README updates to implementation order
linletian May 22, 2026
6cfb41b
docs: expand Task 0 to include PRD, ARCHITECTURE, and API doc updates
linletian May 22, 2026
4cddc7f
docs: Task 0 — update project documentation for Portal Dashboard
linletian May 22, 2026
391fc9a
feat: Task 1 — global auth config (Load/Save auth.json)
linletian May 22, 2026
f471137
feat: Task 2 — CLI portal port flag and config auth commands
linletian May 22, 2026
82d7f36
feat: Task 3 — app config extension, server.json read-merge-write wit…
linletian May 22, 2026
45dcbb1
feat(app): Task 4 - withAuth middleware改造
linletian May 22, 2026
974903f
feat(portal): Task 5 — portal core (claimer, registration, CSRF, clea…
linletian May 22, 2026
02d2a1c
feat(portal): Task 6 — HTTP endpoints & dashboard HTML
linletian May 22, 2026
7663d8a
feat(portal): Task 7 — reverse proxy with bidirectional WebSocket
linletian May 22, 2026
b5a1300
feat(portal): Task 8 — Tailscale Serve 自动管理
linletian May 22, 2026
093d050
portal: implement Task 9 - Go generate CSP hash auto-computation
linletian May 22, 2026
56cd577
feat(portal): Task 10 — Portal lifecycle integration
linletian May 22, 2026
ccf7bdf
feat(portal): Task 11 — comprehensive tests (concurrency, end-to-end,…
linletian May 23, 2026
8ad710c
docs, portal: mark tailscale serve as disabled (macOS CLI bug) ; hard…
linletian May 23, 2026
a85beab
feat: auto-generate auth token when not configured
linletian May 23, 2026
a61c1d9
feat: improve remote access functionality
linletian May 24, 2026
a6f50c8
refactor(portal): remove reverse proxy, use direct links; instances b…
linletian May 24, 2026
fc2f0ba
feat: improve remote access and add tests
linletian May 24, 2026
f8af74e
feat: improve remote access UI and functionality
linletian May 24, 2026
d960677
fix: gofmt all Go files
linletian May 24, 2026
3815b88
fix: replace hardcoded port with config, improve browser shortcut fee…
linletian May 24, 2026
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
64 changes: 60 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ myworktree is a thin management layer that:
- Optional built-in HTTPS (`--tls-cert/--tls-key`) and token auth for non-loopback
- Stored backlog redaction for common secrets (e.g. `sk-...`)
- MCP tool endpoints (`/api/mcp/tools`, `/api/mcp/call`)
- Portal Dashboard with shared entry port, auto-discovery of running instances across repos
- Global auth token (HttpOnly Cookie, CSRF protection, tailscale serve integration)

## Requirements
- macOS 12+ (other platforms are not validated yet)
Expand Down Expand Up @@ -137,6 +139,13 @@ mw
When startup succeeds, `mw` opens the web page automatically at the serving URL by default.
`myworktree` prints the URL without opening a browser unless you pass `-open=true`.

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/
```

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.

By default, newly created worktrees are placed next to your repo:
Expand All @@ -163,6 +172,17 @@ myworktree instance start --worktree <worktreeId> --cmd "echo hello && ls"
myworktree instance start --worktree <worktreeId> # starts an interactive shell instance
myworktree instance list
myworktree instance stop <instanceId>

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

# start with remote access & Portal (IPv6 is explicitly disabled)
mw start --listen 0.0.0.0:0 # LAN access, auto-inherits global token
mw start --listen 0.0.0.0:0 --portal-port 12346 # custom Portal port
mw start --listen 0.0.0.0:0 --portal-port 0 # disable Portal
```

Note: command starts are executed inside the instance shell, and you can continue sending input to the same running instance from the UI.
Expand Down Expand Up @@ -211,10 +231,46 @@ The workflow verifies `gofmt`, runs `go test ./...`, and builds both binaries on
Tagged releases (`v*`) run `.github/workflows/release.yml`, which produces darwin `amd64` / `arm64` archives plus SHA256 checksums.

## Remote access
- Default: binds to loopback only.
- If you listen on a non-loopback address, you must set `--auth`.
- For HTTPS, provide `--tls-cert` and `--tls-key`.
- `?token=<token>` works for simple clients, but prefer `Authorization: Bearer <token>` to avoid leaving tokens in browser history or shell history.

### Global Token

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
# 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.

### 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
- 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)

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.

### Network Security

| Access Path | Protocol | Encryption Layer |
|-------------|----------|-----------------|
| Instance direct (local/LAN IP) | `http://192.168.1.18:PORT` → instance | None (LAN only) |
| Instance direct (Tailscale IP) | `http://100.x.x.x:PORT` → instance | WireGuard tunnel |
| Dashboard + proxy (local/LAN) | `http://host:12345` → proxy `http://127.0.0.1:PORT` | None (LAN only) |
| Dashboard + proxy (Tailscale IP) | `http://100.x.x.x:12345` → proxy `http://127.0.0.1:PORT` | WireGuard tunnel |
| `tailscale serve` domain | `https://machine.ts.net` → proxy `http://127.0.0.1:PORT` | Let's Encrypt TLS + WireGuard |

> **Note**: Tailscale's WireGuard tunnel provides network-layer encryption. Application-layer HTTPS is only used when accessing via `tailscale serve` domain (Let's Encrypt certificate).

## License
MIT. See [LICENSE](./LICENSE).
64 changes: 60 additions & 4 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ myworktree 只做管理,不碰项目具体内容:
- 可选内置 HTTPS(`--tls-cert/--tls-key`),非 loopback 监听必须 `--auth`
- 回放落盘日志脱敏(覆盖常见 secret 与 `sk-...`)
- MCP 接口(`/api/mcp/tools`、`/api/mcp/call`)
- Portal 仪表板:共享入口端口,跨仓库自动发现运行实例
- 全局认证 Token(HttpOnly Cookie、CSRF 防护、Tailscale Serve 自动集成)

## 运行环境
- macOS 12+ 其他平台未验证
Expand Down Expand Up @@ -137,6 +139,13 @@ mw
运行成功后,`mw` 默认会自动打开浏览器访问对应 URL。
`myworktree` 默认只打印 URL;如果也想自动打开浏览器,可传 `-open=true`。

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

myworktree 会用**当前工作目录**定位目标项目(git root),并基于该 git root 计算独立的数据目录,因此要管理其他项目时,只需要在另一个项目仓库目录下运行同一个 myworktree 二进制即可。

默认情况下,新建 worktree 会放在主仓库的同级目录下:
Expand All @@ -163,6 +172,17 @@ myworktree instance start --worktree <worktreeId> --cmd "echo hello && ls"
myworktree instance start --worktree <worktreeId> # 启动一个可交互 shell instance
myworktree instance list
myworktree instance stop <instanceId>

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

# 启动并启用远程访问 + Portal
mw start --listen 0.0.0.0:0 # LAN 访问,自动继承全局 Token
mw start --listen 0.0.0.0:0 --portal-port 12346 # 自定义 Portal 端口
mw start --listen 0.0.0.0:0 --portal-port 0 # 禁用 Portal
```

## Tag 配置
Expand Down Expand Up @@ -209,10 +229,46 @@ GitHub Actions(`.github/workflows/go-ci.yml`)会在以下场景运行:
带 `v*` 标签的发布会触发 `.github/workflows/release.yml`,产出 darwin `amd64` / `arm64` 压缩包和 SHA256 校验文件。

## 远程访问
- 默认只监听本机回环地址。
- 监听到非 loopback(如 `0.0.0.0` 或局域网 IP)时必须提供 `--auth`。
- 需要 HTTPS 时提供 `--tls-cert` 与 `--tls-key`。
- 简单客户端可用 `?token=<token>`,但更推荐 `Authorization: Bearer <token>`,避免 token 落入浏览器历史或 shell 历史。

### 全局 Token

一次性配置全局认证 Token,所有实例自动继承:

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

Token 以明文存储在 `auth.json` 中(0600 权限)。实例级别 `--auth` 参数优先级高于全局 Token。

### Portal 仪表板

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

- 自动发现并列出所有跨仓库运行中的实例
- 点击实例通过 Portal 反向代理跳转到其 Web UI
- 使用 **HttpOnly Cookie**(`mw_token`)进行认证——Token 不出现在 URL 或 JS 中
- 登录/登出端点采用 **CSRF 防护**(double-submit cookie 模式)
- Cookie 具备 24 小时**滑动过期**机制(每次认证成功的请求自动刷新有效期)

设置 `--portal-port 0` 可禁用 Portal。

### Tailscale HTTPS

当 Tailscale 已安装时,Portal 持有者自动配置 `tailscale serve`,提供 HTTPS 访问(`https://<machine>.ts.net`)。全程自动化,无需手动配置。

### 网络安全

| 访问路径 | 协议 | 加密层级 |
|----------|------|----------|
| 实例直连(本地/LAN IP) | `http://192.168.1.18:PORT` → 实例 | 无(仅 LAN 可及) |
| 实例直连(Tailscale IP) | `http://100.x.x.x:PORT` → 实例 | WireGuard 隧道加密 |
| 仪表板 + 代理(本地/LAN) | `http://host:12345` → 代理 `http://127.0.0.1:PORT` | 无(仅 LAN 可及) |
| 仪表板 + 代理(Tailscale IP) | `http://100.x.x.x:12345` → 代理 `http://127.0.0.1:PORT` | WireGuard 隧道加密 |
| `tailscale serve` 域名 | `https://machine.ts.net` → 代理 `http://127.0.0.1:PORT` | Let's Encrypt TLS + WireGuard |

> **说明**:Tailscale 的 WireGuard 隧道已对网络层加密。仅通过 `tailscale serve` 域名访问时使用应用层 HTTPS(Let's Encrypt 证书)。

## License
MIT 协议,详见 [LICENSE](./LICENSE)。
137 changes: 136 additions & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Base URL: printed when starting `myworktree` or `mw`, e.g. `http://127.0.0.1:500
Auth:
- If `--auth <token>` is set, send `Authorization: Bearer <token>`.
- Alternatively, pass `?token=<token>` for simple clients.
- 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.

Common response header:
Expand Down Expand Up @@ -473,7 +475,140 @@ Supported tool names:
- `branch_list`, `tag_list`
- `instance_list`, `instance_start`, `instance_stop`, `instance_input`, `instance_delete`, `instance_log_tail`

## 7) LLM 配置
## 7) Portal Dashboard

The Portal dashboard provides a shared entry point for discovering and accessing all running instances across repos.

Base URL: `http://<host>:<portal-port>/` (default portal port: 12345).

**Auth model**: Portal uses `mw_token` HttpOnly Cookie for authentication. The token is obtained via the CSRF-protected `/api/auth` endpoint. Once authenticated, the Cookie is automatically sent by the browser on all subsequent requests. Cookie has 24-hour sliding expiration (refreshed on each successful auth request).

**CSRF protection**: `/api/auth` and `/api/logout` endpoints use double-submit cookie pattern. Client must fetch a CSRF token from `/api/csrf-token`, then include it in the request body. CSRF tokens are single-use with a 5-minute TTL.

### Dashboard page
`GET /`

Returns the embedded Portal dashboard HTML page (no authentication required).

Response headers:
- `Content-Security-Policy: default-src 'self'; script-src 'sha256-<hash>' ...; style-src 'self' 'sha256-<hash>' ...`
- `X-Content-Type-Options: nosniff`

### Get CSRF token
`GET /api/csrf-token`

Returns a new single-use CSRF token and sets `mw_csrf` Cookie.

Rate limit: 1 request per second per IP.

Response:
```json
{ "csrf_token": "<64-char-hex>" }
```

Sets Cookie: `mw_csrf=<token>; Path=/; SameSite=Strict` (non-HttpOnly — JS must read it for CSRF double-submit).

### Authenticate (login)
`POST /api/auth`

Authenticates with the global auth token. Requires valid CSRF token.

Body:
```json
{ "token": "<auth-token>", "csrf_token": "<csrf-token>" }
```

Rate limit: 20 attempts per minute per IP.

Success (200): Sets `mw_token` HttpOnly Cookie (`Max-Age=86400, SameSite=Lax`) and returns:
```json
{ "status": "ok" }
```

Errors:
- `400`: Auth token not configured on server (`{"error":"auth token not configured on server"}`)
- `401`: Invalid token
- `403`: CSRF token invalid/expired/used
- `429`: Rate limit exceeded

### List instances
`GET /api/list`

**Authentication required** (Cookie `mw_token` or Bearer token).

Returns JSON with all running instances and Portal status. Each successful request refreshes the `mw_token` Cookie's expiration (sliding).

Response:
```json
{
"is_portal": true,
"portal_port": 12345,
"processes": [
{
"instance_id": "12345-1710000000-a1b2c3",
"pid": 12345,
"port": 50053,
"host": "0.0.0.0",
"repo_name": "myproject",
"repo_hash": "a1b2c3d4e5f6",
"started_at": "2024-03-10T12:00:00Z",
"alive": true
}
]
}
```

- `is_portal`: whether the current process holds the Portal port
- `portal_port`: Portal port number
- `alive`: determined by PID liveness and TCP port reachability

### Portal status
`GET /api/portal-status`

No authentication required. Returns whether the current instance holds the Portal port.

Response:
```json
{ "is_portal": true }
```

### Logout
`POST /api/logout`

**CSRF required**. Clears the `mw_token` Cookie.

Body:
```json
{ "csrf_token": "<csrf-token>" }
```

Response (200):
```json
{ "status": "ok" }
```

Always returns 200 (idempotent — successful even if not logged in).

Errors:
- `403`: CSRF token invalid/expired/used or missing

### Reverse proxy (access instance)
`ANY /s/<repo-hash>/*`

**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.

Security:
- `repo-hash` format validation: only `[a-f0-9]+` (lowercase hex) accepted; path traversal characters (`..`, `/`, `\`) rejected with 400
- WebSocket upgrade is automatically handled by the reverse proxy (Go's `httputil.ReverseProxy` natively supports WebSocket hijacking)

Errors:
- `400`: Invalid `repo-hash` format
- `401`: Not authenticated
- `502`: Target instance offline

## 8) LLM 配置
### 获取当前配置
`GET /api/llm/config`

Expand Down
Loading