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
1 change: 0 additions & 1 deletion .agent/rules/golang.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ paths:
type GatewayDeps struct {
Hub *gateway.Hub
SM *session.Manager
JWTValidator *security.JWTValidator
Bridge *gateway.Bridge
}

Expand Down
27 changes: 8 additions & 19 deletions .agent/rules/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,19 @@ paths:

> Mutex / 反模式规范 → 见 AGENTS.md 约定与规范

## JWT 认证
## Bot ID 传输

### 必须使用 ES256 签名
```go
if token.Method.Alg() != "ES256" {
return ErrUnauthorized
}
```

### Claims 完整性
JWT 必须包含:`iss`、`sub`、`aud`、`exp`、`iat`、`jti` + `role`、`scope`、`bot_id`、`session_id`
Bot ID 通过 `X-Bot-ID` HTTP header 或 `bot_id` query param 传输。服务端通过 `security.BotIDFromRequest(r)` 提取,无需 JWT。

### Token 生命周期
| 类型 | TTL |
|------|-----|
| Access Token | 5min |
| Gateway Token | 1h |
| Refresh Token | 7d |
### 信任边界

### JTI 黑名单(TTL 缓存)
被撤销的 Token jti 必须进入内存黑名单,超时后自动清理。JTI 生成禁止 `math/rand`,必须用 `crypto/rand`。
Bot ID **未与 API Key 密码学绑定**——任何已认证客户端可指定任意 bot ID。这是可接受的设计:
1. Bot ID 仅决定路由行为(使用哪套 bot 配置),不决定授权
2. API Key 认证已在连接层网关限制访问
3. 跨 Bot 数据隔离由下游 session key 派生强制执行

### 多 Bot 隔离
Token 中的 `bot_id` 必须与请求的 Session 所属 Bot 精确匹配,禁止跨 Bot 操作。
连接中的 `botID` 必须与请求的 Session 所属 Bot 精确匹配,禁止跨 Bot 操作。

---

Expand Down
2 changes: 1 addition & 1 deletion .agent/skills/hotplex-arch-analyzer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ internal/worker — 基础 worker, proc manager
internal/worker/opencodeserver — OCS 单例 + worker
internal/config — Viper 配置, 热重载
internal/agentconfig — Agent 个性/上下文加载器
internal/security — JWT, SSRF, 路径安全, 命令白名单
internal/security — API Key, Bot ID, SSRF, 路径安全, 命令白名单
internal/admin — Admin API 处理器
internal/aep — AEP v1 编解码器
internal/cli — Checker 注册表, onboard 向导
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Viper 配置加载、热重载、三级继承、审计回滚。

## Security (`internal/security/`)

JWT 认证、SSRF 防御、路径安全、白名单。
API Key + Bot ID 认证、SSRF 防御、路径安全、白名单。

→ `explanation/security-model.md` — 安全模型
→ `guides/developer/security-model.md` — 开发者安全指南
Expand Down
2 changes: 1 addition & 1 deletion .agent/skills/hotplex-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ git log "${LAST_TAG}..HEAD" --no-merges --format="%h %s%n%b---"
| `slack`, `feishu`, `messaging`, `stt` | **Messaging** |
| `webchat`, `ui`, `chat` | **WebChat UI** |
| `config`, `agent-config` | **Configuration** |
| `security`, `jwt`, `ssrf` | **Security** |
| `security`, `auth`, `ssrf` | **Security** |
| `cli`, `onboard`, `doctor` | **CLI** |
| `client`, `sdk`, `ts`, `python`, `java` | **SDK** |
| `test`, `ci`, `build`, `makefile` | **Infrastructure** |
Expand Down
7 changes: 3 additions & 4 deletions .agent/skills/hotplex-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ hotplex doctor --json
|---------|---------|------|
| `exists` | config.yaml 不存在 | 运行 `hotplex onboard` 生成 |
| `syntax` | YAML 解析错误 | 检查缩进和语法,参考 `configs/config.yaml` |
| `required` | JWT secret 缺失或无平台启用 | 运行 `hotplex onboard` 或手动设置 |
| `required` | API Key 缺失或无平台启用 | 运行 `hotplex onboard` 或手动设置 |
| `values` | 端口无效或数据目录不存在 | 创建目录或修改端口配置 |
| `env_vars` | JWT_SECRET/ADMIN_TOKEN 未设置 | 在 `.env` 中添加 |
| `env_vars` | ADMIN_TOKEN 未设置 | 在 `.env` 中添加 |

#### dependencies(依赖)

Expand All @@ -96,7 +96,6 @@ hotplex doctor --json

| checker | 失败原因 | 处理 |
|---------|---------|------|
| `jwt_strength` | JWT secret 太短或低熵 | `openssl rand -base64 48` 重新生成 |
| `admin_token` | Token 为空或弱默认值 | 替换为强随机值 |
| `file_permissions` | 配置文件权限过宽 | `chmod 600 ~/.hotplex/.env ~/.hotplex/config.yaml` |
| `env_in_git` | .env 被 git 追踪 | `git rm --cached .env` |
Expand Down Expand Up @@ -201,7 +200,7 @@ hotplex onboard
hotplex doctor
```

`hotplex onboard` 自动处理:Go/OS/磁盘检查、JWT/Token 生成、Slack/飞书配置、Worker 选择、config.yaml/.env 生成、Agent 配置模板、STT/TTS 检查、系统服务安装。
`hotplex onboard` 自动处理:Go/OS/磁盘检查、Slack/飞书配置、Worker 选择、config.yaml/.env 生成、Agent 配置模板、STT/TTS 检查、系统服务安装。

**非交互模式**(CI/自动化):
```bash
Expand Down
1 change: 0 additions & 1 deletion .agent/skills/hotplex-setup/references/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ hotplex service restart

| checker | 问题 | 修复 |
|---------|------|------|
| `jwt_strength` | JWT secret 太弱 | `openssl rand -base64 48` 重新生成 |
| `admin_token` | 弱默认值 | 替换为强随机值 |
| `env_in_git` | .env 被 git 追踪 | `git rm --cached .env` |

Expand Down
3 changes: 1 addition & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
- ❌ `sync.Mutex` 嵌入或传指针
- ❌ `math/rand` 用于加密
- ❌ Shell 执行(仅允许 `claude` 二进制)
- ❌ 非 ES256 JWT
- ❌ 硬编码路径分隔符
- ❌ 直接使用 POSIX 信号
- ❌ 用 `sed`/`awk` 插入或修改源码行(缩进不可控,必须用 Edit 工具)
Expand Down Expand Up @@ -138,7 +137,7 @@

**支撑模块**:
- `config/` - Viper 配置 + 热重载 + 继承 + 审计/回滚。消息层共享默认值(WorkerType, STT, TTS)通过 `FillFrom()` 传播到平台配置。三级优先级:platform > messaging > Default()。多 bot 支持:`SlackBotConfig`/`FeishuBotConfig` + `normalizeSlackBots`/`normalizeFeishuBots` 向后兼容归一化
- `security/` - JWT、SSRF、路径安全
- `security/` - API Key 认证(`Authenticator`)、Bot ID 提取(`BotIDFromRequest`)、SSRF 防护、路径安全
- `skills/` - Skills 发现
- `metrics/` - Prometheus 指标
- `service/` - 跨平台系统服务管理(systemd/launchd/SCM)
Expand Down
78 changes: 78 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,83 @@
# Changelog

## [Unreleased]

### Summary

移除 JWT 认证依赖,替换为 API Key + Bot ID 认证模型。这是一次 **Breaking Change**,影响所有 WebSocket 客户端和 SDK 用户。

### Breaking Changes

#### 认证模型变更

- **移除 JWT (ES256) 认证**:不再支持 `Authorization: Bearer <jwt>` 头。所有客户端必须使用 `X-API-Key` 头传递 API key。(#467)
- **Bot ID 传输变更**:不再通过 JWT `bot_id` claim 传递。改为 `X-Bot-ID` HTTP 头或 `bot_id` query param。服务端通过 `security.BotIDFromRequest(r)` 提取。(#467)
- **浏览器 WebSocket 客户端**:init envelope 的 `auth.token` 字段语义从 JWT 变为 API key(deferred auth)。(#467)
- **环境变量**:`HOTPLEX_JWT_SECRET` 已移除。改用 `HOTPLEX_SECURITY_API_KEY_*` 编号式环境变量。(#467)
- **用户身份**:JWT `sub` claim 的自动 per-user 隔离不再可用。默认身份为 `api_user`。多用户隔离需配置 `APIKeyResolver`(`security.SetKeyResolver()`)将 API key 映射到用户身份。(#467)
- **`--strict` 标志**:`hotplex config validate --strict` 已移除(用于检查 JWT secret 是否设置,不再适用)。

#### SDK Breaking Changes

| SDK | 移除 | 替代 |
|-----|------|------|
| Go Client | `AuthToken()` option, `token.go`, `gen-token/` | `BotID()` option — 发送 `X-Bot-ID` 头 |
| Java Client | `JwtTokenGenerator`, `.tokenGenerator()`, jjwt/bcprov 依赖 | `.apiKey()` + `.botId()` — 发送 `X-Bot-ID` 头 |
| TypeScript Client | `generate-test-token.ts`, `jose` 依赖 | `authToken` 字段语义改为 API key(deferred browser auth) |
| Python Client | — | `auth_token` 参数语义改为 API key |

#### 配置 API 变更

- `config.Load(path, LoadOptions{})` → `config.Load(path)` — 移除 `LoadOptions` 和 `SecretsProvider` 管道(`EnvSecretsProvider`, `ChainedSecretsProvider`)。配置加载仅通过 Viper `AutomaticEnv` 绑定 `HOTPLEX_*` 环境变量。
- `config.NewWatcher(log, path, sp, store, ...)` → `config.NewWatcher(log, path, store, ...)` — 移除 `SecretsProvider` 参数。
- `security.NewAuthenticator(cfg, jwtValidator)` → `security.NewAuthenticator(cfg)` — 移除 JWT validator 参数。
- `security.BotIDFromHeader(r)` → `security.BotIDFromRequest(r)` — 重命名以反映其同时读取 header 和 query param。

### Removed

- `internal/security/jwt.go` — JWT 验证器(ES256 签名、HKDF 密钥派生、JTI 黑名单)。(#467)
- `client/token.go` — Go SDK JWT token 生成。(#467)
- `client/scripts/gen-token/` — Go SDK token 生成命令行工具。(#467)
- `client/examples/08_token_generator/` — Go SDK token 生成示例。(#467)
- `internal/cli/checkers/security_fix_test.go` — JWT 强度检查测试。(#467)
- `examples/typescript-client/scripts/generate-test-token.ts` — TS SDK JWT token 生成脚本。(#467)
- `examples/java-client/src/main/java/dev/hotplex/security/JwtTokenGenerator.java` — Java SDK JWT 生成器。(#467)
- `golang-jwt/jwt/v5` 依赖 — 不再需要。(#467)
- `jjwt-api/impl/jackson` + `bcprov-jdk18on` 依赖(Java SDK) — 不再需要。(#467)

### Changed

- `security.AuthenticateRequest()` 内 `BotIDFromRequest` 调用从 2 次优化为 1 次(提取到局部变量)。(#467)
- `conn.go` 中 `context.TODO()` 替换为 `context.Background()`(与同函数其他路径一致)。(#467)

### Migration Guide

**1. 环境变量替换:**
```bash
# 旧(已移除)
export HOTPLEX_JWT_SECRET="$(openssl rand -base64 32)"

# 新
export HOTPLEX_SECURITY_API_KEY_1="your-api-key"
export HOTPLEX_ADMIN_TOKEN_1="your-admin-token"
```

**2. Go SDK 迁移:**
```go
// 旧
client.New(ctx, URL("ws://localhost:8888/ws"), WorkerType("claude_code"), AuthToken("jwt-token"))

// 新
client.New(ctx, URL("ws://localhost:8888/ws"), WorkerType("claude_code"), BotID("bot-123"))
```

**3. 多用户隔离:** 如果之前依赖 JWT `sub` 实现用户隔离,需在 Gateway 启动时配置 `APIKeyResolver`:
```go
auth.SetKeyResolver(security.NewChainResolver(dbResolver, configResolver))
```

**4. 浏览器 WebSocket:** init envelope 中 `auth.token` 从传 JWT 改为传 API key,`auth.bot_id` 保持不变。

## [1.17.0] - 2026-05-21

### Summary
Expand Down
6 changes: 3 additions & 3 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ hotplex onboard

| Variable | Purpose | Generate |
|----------|---------|----------|
| `HOTPLEX_JWT_SECRET` | ES256 JWT signing key | `openssl rand -base64 32` |
| `HOTPLEX_ADMIN_TOKEN_1` | Admin API bearer token | `openssl rand -base64 32` |
| `HOTPLEX_SECURITY_API_KEY_1` | Client auth key | `openssl rand -base64 32` |

### Config File

Expand Down Expand Up @@ -240,7 +240,7 @@ hotplex gateway status # check running
curl http://localhost:9999/admin/health

# 5. WebSocket endpoint
# ws://localhost:8888 (needs API key or JWT)
# ws://localhost:8888 (needs API key)
```

## Next Steps
Expand Down Expand Up @@ -292,7 +292,7 @@ gh repo star hrygo/hotplex
| `Checksum mismatch` | Corrupted download | Re-run install; if persists, report issue |
| `GitHub API rate limit` | Too many unauth API calls | Use `--release <tag>` instead of `--latest` |
| Permission denied on `/usr/local` | Non-root user writing to system dir | Use `sudo` or `--prefix ~/.local` |
| `hotplex` runs but won't start gateway | Missing secrets | Run `hotplex onboard` or set `HOTPLEX_JWT_SECRET` |
| `hotplex` runs but won't start gateway | Missing secrets | Run `hotplex onboard` or set `HOTPLEX_ADMIN_TOKEN_1` |
| Port 8888/9999 in use | Another process bound | Change `gateway.addr` / `admin.addr` in config |
| Windows: PATH not updated | Terminal not refreshed | Open new PowerShell/CMD window |

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

### 🛡️ Security & Reliability
- 🛡️ **Meta-Cognition Hardening** — Constitutional **META-COGNITION** promoted to the top of B-channel with built-in **XML Sanitizer** to block prompt injection.
- 🔒 **Enterprise-Grade Security** — JWT ES256 authentication, SSRF protection, and **Windows File-Based Injection** to bypass cmd.exe escaping traps.
- 🔒 **Enterprise-Grade Security** — API Key + Bot ID authentication, SSRF protection, and **Windows File-Based Injection** to bypass cmd.exe escaping traps.

### 📱 Multi-Platform Delivery & Integration
- 📱 **Cross-Platform Delivery** — **"Write Once, Deploy Anywhere"**. Bridge agents to Web, Slack (Socket Mode), and Feishu (WebSocket) with zero code changes.
Expand Down
2 changes: 1 addition & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

### 🛡️ 安全加固与可靠性
- 🛡️ **元认知防御基线** — 宪法级 **META-COGNITION** 迁移至 B 通道首位,内置 **XML Sanitizer** 防护,彻底阻断 Prompt 注入与 XML 结构破坏。
- 🔒 **企业级安全加固** — 强制 JWT ES256 认证、SSRF 防护、Windows 临时文件式注入(规避 cmd 转义陷阱)及进程级隔离。
- 🔒 **企业级安全加固** — API Key + Bot ID 认证、SSRF 防护、Windows 临时文件式注入(规避 cmd 转义陷阱)及进程级隔离。

### 📱 多平台分发与集成
- 📱 **跨平台分发能力** — **"一次接入,全端覆盖"**。无需修改 Agent 代码即可秒级分发至 Web、Slack (Socket Mode) 和飞书。
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ We will acknowledge receipt of your report within **48 hours** and provide a tim

- **TLS**: Always enable TLS (`security.tls_enabled: true`) in production.
- **Authentication**: Configure strong `APIKeys` and `Admin.Tokens`.
- **JWT**: Use the `JWTValidator` for user-level session authentication.
- **API Key**: Use `Authenticator` for API key validation. Bot ID: Use `BotIDFromRequest(r)` for multi-bot isolation via X-Bot-ID header.
- **PGID Isolation**: Ensure the gateway process has sufficient permissions to manage process groups (PGID).

---
Expand Down
7 changes: 3 additions & 4 deletions client/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ Standalone Go module (github.com/hrygo/hotplex/client) for connecting to HotPlex
client.go # Client struct: Connect, Resume, SendInput, SendPermissionResponse, SendQuestionResponse, SendElicitationResponse, SendControl, SendReset, SendGC, Close, Events
events.go # Typed event constants (EventMessageDelta, EventDone, etc.) + data helpers (AsDoneData, AsErrorData, AsToolCallData)
options.go # Functional options (AutoReconnect, ClientSessionID, Metadata, Logger, PingInterval)
token.go # TokenGenerator for JWT creation
```

## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| Client struct | `client.go:33` | url, workerType, authToken, apiKey, state machine |
| Client struct | `client.go:33` | url, workerType, botID, apiKey, state machine |
| Connect flow | `client.go:93` | WebSocket dial → send init → recv init_ack → start pumps |
| Resume session | `client.go:99` | Reconnect with existing session_id |
| Event stream | `client.go:193` | Events() returns read-only channel of Event structs |
Expand All @@ -23,7 +22,7 @@ token.go # TokenGenerator for JWT creation
| Heartbeat | `client.go:353` | pingPump: periodic ping at DefaultPingInterval |
| Event constants | `events.go` | 18+ typed constants matching pkg/events Kind values |
| Event data helpers | `events.go` | AsDoneData(), AsErrorData(), AsToolCallData(), etc. |
| Functional options | `options.go` | URL(), WorkerType(), AuthToken(), AutoReconnect(), ClientSessionID(), Metadata() |
| Functional options | `options.go` | URL(), WorkerType(), BotID(), AutoReconnect(), ClientSessionID(), Metadata() |

## KEY PATTERNS

Expand All @@ -43,7 +42,7 @@ token.go # TokenGenerator for JWT creation
- Data helpers: `evt.AsDoneData()`, `evt.AsErrorData()`, `evt.AsToolCallData()` for type-safe access

**Functional options pattern**
- `client.New(ctx, URL(...), WorkerType(...), AuthToken(...), AutoReconnect(true))`
- `client.New(ctx, URL(...), WorkerType(...), BotID(...), AutoReconnect(true))`
- `ClientSessionID("my-session-001")` for UUIDv5 deterministic mapping
- `Metadata(map[string]any{"key": "val"})` for init handshake metadata

Expand Down
21 changes: 9 additions & 12 deletions client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Functional options pattern, passed to `New`:
```go
client.URL("ws://localhost:8888") // required
client.WorkerType("claude_code") // required
client.AuthToken("jwt-token") // JWT bearer token
client.BotID("bot-123") // Bot ID for multi-bot setups
client.APIKey("sk-xxx") // API key header
client.PingInterval(30 * time.Second) // heartbeat (default 54s)
client.ClientSessionID("my-session-001") // client-managed session ID (UUIDv5 mapped)
Expand Down Expand Up @@ -200,17 +200,15 @@ StateTerminated // worker exited
StateDeleted // GC'd
```

## Token Generation
## Bot ID (Multi-Bot Setup)

```go
gen, err := client.NewTokenGenerator(signingKey)
if err != nil { /* ... */ }

// Key formats: PEM file path, 64-char hex, or 44-char base64
token, err := gen.Generate("user-id", []string{"read", "write"}, 1*time.Hour)

// Custom audience (default "gateway")
gen.WithAudience("custom-aud")
c, err := client.New(ctx,
client.URL("ws://localhost:8888"),
client.WorkerType("claude_code"),
client.APIKey("ak-xxx"),
client.BotID("bot-123"), // specify target Bot ID
)
```

## Examples
Expand All @@ -219,13 +217,12 @@ gen.WithAudience("custom-aud")
|------|-------------|
| [`examples/quickstart.go`](examples/quickstart.go) | Minimal connect & chat |
| [`examples/complete.go`](examples/complete.go) | Full features: permissions, stats, resume |
| [`scripts/gen-token/main.go`](scripts/gen-token/main.go) | JWT token generator CLI |

Run an example:

```bash
cd client
HOTPLEX_SIGNING_KEY=<key> go run examples/quickstart.go
HOTPLEX_API_KEY=<key> go run examples/quickstart.go
```

## Related
Expand Down
10 changes: 5 additions & 5 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type Client struct {
// config from options
url string
workerType string
authToken string
botID string
apiKey string
clientSessionID string

Expand Down Expand Up @@ -193,8 +193,8 @@ func (c *Client) Resume(ctx context.Context, sessionID string) (*InitAckData, er

func (c *Client) doConnect(ctx context.Context, sessionID string, isResume bool) (*InitAckData, error) {
hdr := http.Header{}
if c.authToken != "" {
hdr.Set("Authorization", "Bearer "+c.authToken)
if c.botID != "" {
hdr.Set("X-Bot-ID", c.botID)
}
if c.apiKey != "" {
hdr.Set("X-API-Key", c.apiKey)
Expand Down Expand Up @@ -226,8 +226,8 @@ func (c *Client) doConnect(ctx context.Context, sessionID string, isResume bool)
if c.metadata != nil {
initData["config"] = map[string]any{"metadata": c.metadata}
}
if c.authToken != "" {
initData["auth"] = map[string]any{"token": c.authToken}
if c.botID != "" {
initData["auth"] = map[string]any{"bot_id": c.botID}
}
if c.clientSessionID != "" || isResume {
initData["session_id"] = sessionID
Expand Down
Loading