From 929a722c66be51b7d63c20f98a928402ac3a507a Mon Sep 17 00:00:00 2001 From: zhi Date: Thu, 19 Mar 2026 14:19:46 +0000 Subject: [PATCH] docs: add OpenClaw Plugin development plan - docs/OPENCLAW_PLUGIN_DEV_PLAN.md: Complete development plan * Backend capability assessment * Security analysis (current HTTP heartbeat lacks validation) * Three implementation options (enhanced HTTP / API Key / encrypted payload) * Phased development plan (Phase 1-3) * API specifications * Data models * Sequence diagrams - docs/examples/monitor_heartbeat_secure.py: Reference implementation for secure HTTP heartbeat with challenge validation --- docs/OPENCLAW_PLUGIN_DEV_PLAN.md | 494 ++++++++++++++++++++++ docs/examples/monitor_heartbeat_secure.py | 108 +++++ 2 files changed, 602 insertions(+) create mode 100644 docs/OPENCLAW_PLUGIN_DEV_PLAN.md create mode 100644 docs/examples/monitor_heartbeat_secure.py diff --git a/docs/OPENCLAW_PLUGIN_DEV_PLAN.md b/docs/OPENCLAW_PLUGIN_DEV_PLAN.md new file mode 100644 index 0000000..c22c829 --- /dev/null +++ b/docs/OPENCLAW_PLUGIN_DEV_PLAN.md @@ -0,0 +1,494 @@ +# OpenClaw Plugin 开发计划 + +**文档版本**: 0.1.0 +**日期**: 2026-03-19 +**状态**: 开发中 + +--- + +## 1. 概述 + +本文档定义 HarborForge.OpenclawPlugin 的开发计划,以及 Backend 需要提供的接口支持。 + +### 1.1 目标 + +开发一个 OpenClaw 插件,将服务器遥测数据(系统指标 + OpenClaw 状态)实时传输到 HarborForge Monitor。 + +### 1.2 架构关系 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 远程服务器 (VPS) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ OpenClaw Gateway │ │ +│ │ ┌────────────────────────────────────────────────┐ │ │ +│ │ │ HarborForge.OpenclawPlugin │ │ │ +│ │ │ - 生命周期管理 (随 Gateway 启动/停止) │ │ │ +│ │ │ - 启动 sidecar 进程 │ │ │ +│ │ └────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ 启动/管理 │ │ +│ │ ┌────────────────────────────────────────────────┐ │ │ +│ │ │ Sidecar (独立 Node 进程) │ │ │ +│ │ │ - 收集系统指标 (CPU/内存/磁盘) │ │ │ +│ │ │ - 读取 OpenClaw 状态 (agents) │ │ │ +│ │ │ - HTTP/WebSocket 上报到 Monitor │ │ │ +│ │ └────────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + │ HTTP / WebSocket + ▼ + ┌─────────────────────────┐ + │ HarborForge.Backend │ + │ - /monitor/* 接口 │ + │ - 数据存储 │ + └─────────────────────────┘ +``` + +--- + +## 2. Backend 当前能力评估 + +### 2.1 已实现接口 ✅ + +| 接口 | 功能 | 完整度 | 说明 | +|------|------|--------|------| +| `GET /monitor/public/server-public-key` | 获取 RSA 公钥 | ✅ 100% | 用于插件加密 | +| `POST /admin/servers` | 注册服务器 | ✅ 100% | 返回 server_id | +| `POST /admin/servers/{id}/challenge` | 生成 challenge | ✅ 100% | 10分钟有效期 | +| `WS /monitor/server/ws` | WebSocket 连接 | ✅ 100% | 完整验证逻辑 | +| `POST /monitor/server/heartbeat` | HTTP 心跳 | ⚠️ 50% | 缺少安全验证 | + +### 2.2 当前 HTTP Heartbeat 问题 🔴 + +```python +# 当前实现 (app/api/routers/monitor.py:191-207) +@router.post('/server/heartbeat') +def server_heartbeat(payload: ServerHeartbeat, db: Session = Depends(get_db)): + server = db.query(MonitoredServer).filter( + MonitoredServer.identifier == payload.identifier + ).first() + # 问题:只验证 identifier 存在,不验证 challenge! + # 任何人知道 identifier 就可以伪造数据 +``` + +**对比 WebSocket 实现**: +```python +# WebSocket 有完整验证 +ch = db.query(ServerChallenge).filter( + ServerChallenge.challenge_uuid == challenge_uuid, + ServerChallenge.server_id == server.id +).first() +if not ch or ch.used_at is not None or ch.expires_at < now(): + await websocket.close(code=4401) # 验证失败 +``` + +--- + +## 3. Backend 需要补充的接口 + +### 3.1 方案 A:增强 HTTP Heartbeat(推荐短期方案) + +添加 challenge_uuid 验证: + +```python +@router.post('/server/heartbeat') +def server_heartbeat( + payload: ServerHeartbeatSecure, # 包含 challenge_uuid + db: Session = Depends(get_db) +): + # 1. 验证服务器 + server = db.query(MonitoredServer).filter(...).first() + if not server: + raise HTTPException(404, 'unknown server') + + # 2. 验证 challenge + ch = db.query(ServerChallenge).filter( + ServerChallenge.challenge_uuid == payload.challenge_uuid, + ServerChallenge.server_id == server.id + ).first() + + if not ch or ch.expires_at < now(): + raise HTTPException(401, 'invalid or expired challenge') + + # 3. 存储数据... +``` + +**优点**: 与现有 WebSocket 验证逻辑一致 +**缺点**: Challenge 10分钟过期,需要定期重新注册 + +### 3.2 方案 B:API Key 模式(推荐长期方案) + +添加长期有效的 API Key: + +```python +# 1. 模型添加 api_key 字段 +class MonitoredServer(Base): + ... + api_key = Column(String(64), nullable=True, unique=True, index=True) + +# 2. 新增接口:生成/重置 API Key +@router.post('/admin/servers/{id}/api-key') +def generate_api_key(server_id: int, ...): + api_key = secrets.token_urlsafe(32) + # 存储并返回 (仅显示一次) + +# 3. 心跳接口验证 API Key +@router.post('/server/heartbeat-v2') +def server_heartbeat_v2( + payload: ServerHeartbeat, + x_api_key: str = Header(...), + db: Session = Depends(get_db) +): + server = db.query(MonitoredServer).filter( + MonitoredServer.identifier == payload.identifier, + MonitoredServer.api_key == x_api_key + ).first() + if not server: + raise HTTPException(401, 'invalid credentials') +``` + +**优点**: 长期有效,适合自动化 Agent +**缺点**: 需要新增数据库字段和接口 + +### 3.3 方案 C:加密 Payload(最高安全) + +参考 WebSocket 的 encrypted_payload: + +```python +@router.post('/server/heartbeat') +def server_heartbeat( + encrypted_payload: str = Body(...), # RSA-OAEP 加密 + db: Session = Depends(get_db) +): + # 1. 解密 + data = decrypt_payload_b64(encrypted_payload) + + # 2. 验证时间戳 (防重放) + if not ts_within(data['ts'], max_minutes=10): + raise HTTPException(401, 'expired') + + # 3. 验证 challenge + ch = db.query(ServerChallenge).filter( + challenge_uuid=data['challenge_uuid'] + ).first() + ... +``` + +**优点**: 最高安全性 +**缺点**: 客户端实现复杂,需要 RSA 加密 + +--- + +## 4. OpenclawPlugin 开发计划 + +### Phase 1: 基础功能开发(2-3天) + +**目标**: 可运行的基础版本(开发环境) + +| 任务 | 说明 | 依赖 | +|------|------|------| +| 1.1 Sidecar 基础架构 | Node.js 项目结构,配置加载 | 无 | +| 1.2 系统指标收集 | CPU/内存/磁盘/运行时间 | 无 | +| 1.3 OpenClaw 状态读取 | 读取 agents.json,版本信息 | 无 | +| 1.4 HTTP 心跳上报 | 使用当前 /heartbeat 接口 | ⚠️ 不安全,仅开发 | +| 1.5 Plugin 生命周期 | 随 Gateway 启动/停止 Sidecar | 无 | + +**验收标准**: +- [ ] 可以收集系统指标 +- [ ] 可以上报到 Backend +- [ ] 可以在 Monitor 面板看到数据 + +### Phase 2: 安全增强(2-3天) + +**目标**: 生产环境可用的安全版本 + +| 任务 | 说明 | 依赖 | +|------|------|------| +| 2.1 WebSocket 支持 | 实现 WS 连接和加密握手 | Backend WS 接口 ✅ | +| 2.2 或:等待 HTTP 增强 | Backend 添加 challenge 验证 | Backend 更新 | +| 2.3 重试/退避逻辑 | 连接失败时指数退避 | 无 | +| 2.4 离线缓存 | 暂时存储,恢复后批量上报 | 无 | + +**验收标准**: +- [ ] 连接需要验证(WebSocket 或增强 HTTP) +- [ ] 网络中断后自动恢复 +- [ ] 数据不丢失 + +### Phase 3: 生产就绪(1-2天) + +**目标**: 稳定可靠的监控系统 + +| 任务 | 说明 | 依赖 | +|------|------|------| +| 3.1 日志和诊断 | 结构化日志,调试接口 | 无 | +| 3.2 性能优化 | 减少资源占用 | 无 | +| 3.3 安装脚本完善 | 参考 PaddedCell 格式 | 无 | +| 3.4 文档编写 | 部署指南,故障排查 | 无 | + +**验收标准**: +- [ ] 长时间稳定运行(7天+) +- [ ] 资源占用 < 1% CPU,< 50MB 内存 +- [ ] 安装脚本一键部署 + +--- + +## 5. 接口规格详细定义 + +### 5.1 当前可用接口 + +#### GET /monitor/public/server-public-key +```yaml +Response: + public_key_pem: string # RSA 公钥 (PEM 格式) + key_id: string # 公钥指纹 +``` + +#### POST /admin/servers +```yaml +Headers: + Authorization: Bearer {admin_token} +Body: + identifier: string # 唯一标识 (如 "vps.t1") + display_name: string # 显示名称 +Response: + id: int + identifier: string + challenge_uuid: string # 10分钟有效 + expires_at: ISO8601 +``` + +#### WS /monitor/server/ws +```yaml +连接流程: + 1. Client -> Server: GET /monitor/server/ws (Upgrade) + 2. Client -> Server: { + "encrypted_payload": "base64_rsa_encrypted_json" + } + # 或明文(向后兼容): + # { + # "identifier": "vps.t1", + # "challenge_uuid": "...", + # "nonce": "...", + # "ts": "2026-03-19T14:00:00Z" + # } + 3. Server -> Client: { "ok": true, "server_id": 1 } + 4. Client -> Server: { + "event": "server.metrics", + "payload": { "cpu_pct": 12.5, "mem_pct": 41.2, ... } + } +``` + +#### POST /monitor/server/heartbeat(当前版本,不安全) +```yaml +Body: + identifier: string + openclaw_version: string + agents: [{id, name, status}] + cpu_pct: float + mem_pct: float + disk_pct: float + swap_pct: float +Response: + ok: true + server_id: int + last_seen_at: ISO8601 +``` + +### 5.2 建议新增接口 + +#### POST /server/heartbeat-secure(增强版) +```yaml +Body: + identifier: string + challenge_uuid: string # 新增:必填 + openclaw_version: string + agents: [...] + cpu_pct: float + mem_pct: float + disk_pct: float + swap_pct: float + timestamp: ISO8601 # 可选:防重放 +Response: + ok: true + server_id: int + last_seen_at: ISO8601 + challenge_expires_at: ISO8601 +Error: + 401: { detail: "invalid or expired challenge" } +``` + +--- + +## 6. 数据模型 + +### 6.1 当前 Backend 模型 + +```python +# app/models/monitor.py + +class MonitoredServer: + id: int + identifier: str # 唯一标识 + display_name: str + is_enabled: bool + created_by: int + created_at: datetime + # 建议添加: + # api_key: str # 长期有效的 API Key + +class ServerChallenge: + id: int + server_id: int + challenge_uuid: str # 10分钟有效 + expires_at: datetime + used_at: datetime # 首次使用时间 + created_at: datetime + +class ServerState: + id: int + server_id: int + openclaw_version: str + agents_json: str # JSON 序列化 + cpu_pct: float + mem_pct: float + disk_pct: float + swap_pct: float + last_seen_at: datetime + updated_at: datetime +``` + +### 6.2 Plugin 配置模型 + +```typescript +// ~/.openclaw/openclaw.json +{ + "plugins": { + "harborforge-monitor": { + "enabled": true, + "backendUrl": "https://monitor.hangman-lab.top", + "identifier": "vps.t1", // 服务器标识 + "challengeUuid": "uuid-here", // 从 /admin/servers/{id}/challenge 获取 + "apiKey": "key-here", // 如果使用 API Key 模式(可选) + "reportIntervalSec": 30, + "httpFallbackIntervalSec": 60, + "logLevel": "info" + } + } +} +``` + +--- + +## 7. 开发时序图 + +### 7.1 首次部署流程 + +```mermaid +sequenceDiagram + participant Admin + participant Backend + participant Plugin + participant Server as Server State + + Admin->>Backend: POST /admin/servers
{identifier: "vps.t1"} + Backend->>Admin: {id: 1, identifier: "vps.t1"} + + Admin->>Backend: POST /admin/servers/1/challenge + Backend->>Admin: {challenge_uuid: "abc-123", expires_at: "..."} + + Admin->>Server: 配置 challenge_uuid + + Note over Server: ~/.openclaw/openclaw.json + + Server->>Backend: openclaw gateway restart + + Plugin->>Backend: GET /monitor/public/server-public-key + Backend->>Plugin: {public_key_pem: "..."} + + alt WebSocket 模式 + Plugin->>Backend: WS /monitor/server/ws + Plugin->>Backend: {challenge_uuid, nonce, ts} + Backend->>Plugin: {ok: true} + loop 每 30 秒 + Plugin->>Backend: {event: "server.metrics", payload: {...}} + end + else HTTP 模式 + loop 每 30 秒 + Plugin->>Backend: POST /server/heartbeat
{challenge_uuid, ...} + Backend->>Plugin: {ok: true} + end + end +``` + +### 7.2 数据上报格式 + +```json +{ + "identifier": "vps.t1", + "challenge_uuid": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2026-03-19T14:30:00Z", + + "cpu_pct": 12.5, + "mem_pct": 41.2, + "mem_used_mb": 4096, + "mem_total_mb": 8192, + "disk_pct": 62.0, + "disk_used_gb": 500.5, + "disk_total_gb": 1000.0, + "swap_pct": 0.0, + "uptime_sec": 86400, + "load_avg_1m": 0.5, + "platform": "linux", + "hostname": "vps.t1", + + "openclaw_version": "1.2.3", + "openclaw_agent_count": 2, + "openclaw_agents": [ + {"id": "dev", "name": "Developer", "status": "running"}, + {"id": "ops", "name": "Operator", "status": "idle"} + ] +} +``` + +--- + +## 8. 风险与缓解 + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| HTTP Heartbeat 无验证 | 数据伪造 | 使用 WebSocket 或等待 Backend 修复 | +| Challenge 10分钟过期 | 需要频繁更新 | Backend 添加 API Key 模式 | +| 网络中断 | 数据丢失 | Plugin 实现离线缓存 | +| 资源占用过高 | 影响业务 | 控制采集频率,优化实现 | +| Sidecar 崩溃 | 监控中断 | Plugin 自动重启 Sidecar | + +--- + +## 9. 下一步行动 + +### Backend 团队 +- [ ] 决定采用方案 A/B/C 增强 HTTP Heartbeat 安全 +- [ ] 实现 `/server/heartbeat-secure` 或增强现有接口 +- [ ] (可选)添加 API Key 支持 + +### Plugin 开发团队 +- [ ] Phase 1: 基础功能开发(使用当前不安全 HTTP,仅开发测试) +- [ ] Phase 2: 集成 WebSocket(立即可用,最安全) +- [ ] 等待 Backend 更新后,切换到安全 HTTP + +--- + +## 10. 参考文档 + +- 原始设计文档: `docs/monitor-server-connector-plan.md` +- Backend 代码: `app/api/routers/monitor.py` +- Backend 模型: `app/models/monitor.py` +- 加密服务: `app/services/crypto_box.py` +- PaddedCell 安装脚本参考: `https://git.hangman-lab.top/nav/PaddedCell` + +--- + +**文档维护者**: HarborForge Team +**更新频率**: 随开发进度更新 diff --git a/docs/examples/monitor_heartbeat_secure.py b/docs/examples/monitor_heartbeat_secure.py new file mode 100644 index 0000000..a4b4af2 --- /dev/null +++ b/docs/examples/monitor_heartbeat_secure.py @@ -0,0 +1,108 @@ +""" +Backend 监控接口需要补充的安全验证代码 +添加到 app/api/routers/monitor.py +""" + +from fastapi import Header + +class ServerHeartbeatSecure(BaseModel): + identifier: str + challenge_uuid: str # 新增:必须提供 challenge + openclaw_version: str | None = None + agents: List[dict] = [] + cpu_pct: float | None = None + mem_pct: float | None = None + disk_pct: float | None = None + swap_pct: float | None = None + + +@router.post('/server/heartbeat') +def server_heartbeat( + payload: ServerHeartbeatSecure, + x_challenge_uuid: str = Header(..., description='Challenge UUID from registration'), + db: Session = Depends(get_db) +): + """ + 安全版本的心跳接口,验证 challenge_uuid + """ + # 1. 验证服务器存在且启用 + server = db.query(MonitoredServer).filter( + MonitoredServer.identifier == payload.identifier, + MonitoredServer.is_enabled == True + ).first() + if not server: + raise HTTPException(status_code=404, detail='unknown server identifier') + + # 2. 验证 challenge_uuid 存在且有效 + ch = db.query(ServerChallenge).filter( + ServerChallenge.challenge_uuid == x_challenge_uuid, + ServerChallenge.server_id == server.id + ).first() + + if not ch: + raise HTTPException(status_code=401, detail='invalid challenge') + + if ch.expires_at < datetime.now(timezone.utc): + raise HTTPException(status_code=401, detail='challenge expired') + + # 3. 可选:检查 challenge 是否已被使用过 + # 如果是首次验证,标记为已使用 + if ch.used_at is None: + ch.used_at = datetime.now(timezone.utc) + + # 4. 存储状态 + st = db.query(ServerState).filter(ServerState.server_id == server.id).first() + if not st: + st = ServerState(server_id=server.id) + db.add(st) + + st.openclaw_version = payload.openclaw_version + st.agents_json = json.dumps(payload.agents, ensure_ascii=False) + st.cpu_pct = payload.cpu_pct + st.mem_pct = payload.mem_pct + st.disk_pct = payload.disk_pct + st.swap_pct = payload.swap_pct + st.last_seen_at = datetime.now(timezone.utc) + + db.commit() + + return { + 'ok': True, + 'server_id': server.id, + 'last_seen_at': st.last_seen_at, + 'challenge_valid_until': ch.expires_at.isoformat() + } + + +# 或者,如果需要长期有效的 API Key 方式: + +class ServerHeartbeatApiKey(BaseModel): + identifier: str + openclaw_version: str | None = None + agents: List[dict] = [] + cpu_pct: float | None = None + mem_pct: float | None = None + disk_pct: float | None = None + swap_pct: float | None = None + + +@router.post('/server/heartbeat-v2') +def server_heartbeat_v2( + payload: ServerHeartbeatApiKey, + x_api_key: str = Header(..., description='Server API Key'), + db: Session = Depends(get_db) +): + """ + 使用 API Key 的心跳接口(长期有效,不需要 challenge) + 需要在 MonitoredServer 模型中添加 api_key 字段 + """ + server = db.query(MonitoredServer).filter( + MonitoredServer.identifier == payload.identifier, + MonitoredServer.is_enabled == True, + MonitoredServer.api_key == x_api_key # 需要添加 api_key 字段 + ).first() + + if not server: + raise HTTPException(status_code=401, detail='invalid identifier or api key') + + # ... 存储状态 ...