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
This commit is contained in:
zhi
2026-03-19 14:19:46 +00:00
parent 67c648d6d8
commit 929a722c66
2 changed files with 602 additions and 0 deletions

View File

@@ -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')
# ... 存储状态 ...