openclacky 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.clacky/skills/gem-release/SKILL.md +27 -31
- data/CHANGELOG.md +14 -0
- data/Dockerfile +28 -0
- data/docs/engineering-article.md +343 -0
- data/lib/clacky/agent/llm_caller.rb +1 -5
- data/lib/clacky/cli.rb +1 -1
- data/lib/clacky/message_format/anthropic.rb +17 -1
- data/lib/clacky/providers.rb +34 -0
- data/lib/clacky/server/channel/adapters/dingtalk/adapter.rb +142 -5
- data/lib/clacky/server/channel/adapters/dingtalk/api_client.rb +309 -0
- data/lib/clacky/ui2/ui_controller.rb +14 -0
- data/lib/clacky/ui_interface.rb +14 -0
- data/lib/clacky/utils/model_pricing.rb +96 -25
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +8 -0
- data/lib/clacky/web/index.html +1 -1
- data/lib/clacky/web/onboard.js +6 -0
- data/lib/clacky/web/settings.js +17 -5
- data/scripts/build/lib/apt.sh +30 -10
- data/scripts/build/lib/network.sh +3 -2
- data/scripts/install.sh +30 -9
- metadata +3 -16
- data/docs/HOW-TO-USE-CN.md +0 -96
- data/docs/HOW-TO-USE.md +0 -94
- data/docs/browser-cdp-native-design.md +0 -195
- data/docs/c-end-user-positioning.md +0 -64
- data/docs/config.example.yml +0 -27
- data/docs/deploy-architecture.md +0 -619
- data/docs/deploy_subagent_design.md +0 -540
- data/docs/install-script-simplification.md +0 -89
- data/docs/memory-architecture.md +0 -343
- data/docs/openclacky_cloud_api_reference.md +0 -584
- data/docs/security-design.md +0 -109
- data/docs/session-management-redesign.md +0 -202
- data/docs/system-skill-authoring-guide.md +0 -47
- data/docs/why-developer.md +0 -371
- data/docs/why-openclacky.md +0 -266
|
@@ -1,584 +0,0 @@
|
|
|
1
|
-
# OpenClacky License API — 接口文档
|
|
2
|
-
|
|
3
|
-
**Base URL**: `https://your-platform.com`
|
|
4
|
-
**Content-Type**: `application/json`
|
|
5
|
-
**协议说明**: License Key **全程不通过网络传输**,所有认证均基于 HMAC-SHA256 知识证明。
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 接口目录
|
|
10
|
-
|
|
11
|
-
| # | 接口名称 | 方法 | 路径 | 说明 |
|
|
12
|
-
|---|---------|------|------|------|
|
|
13
|
-
| 1 | [激活 License](#1-激活-license) | POST | `/api/v1/licenses/activate` | 首次激活,绑定设备 |
|
|
14
|
-
| 2 | [获取 Skills 列表](#2-获取-skills-列表) | POST | `/api/v1/licenses/skills` | 查询许可范围内的 Skill |
|
|
15
|
-
| 3 | [心跳检测](#4-心跳检测) | POST | `/api/v1/licenses/heartbeat` | 定期验证许可有效性 |
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## 通用错误码
|
|
20
|
-
|
|
21
|
-
所有接口均使用统一的错误响应格式:
|
|
22
|
-
|
|
23
|
-
```json
|
|
24
|
-
{
|
|
25
|
-
"status": "error",
|
|
26
|
-
"code": "<错误码>"
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
| HTTP 状态码 | code | 说明 |
|
|
31
|
-
|------------|------|------|
|
|
32
|
-
| 400 | `missing_params` | 缺少必填参数 |
|
|
33
|
-
| 401 | `invalid_proof` | 激活证明验证失败 |
|
|
34
|
-
| 401 | `invalid_signature` | 请求签名验证失败 |
|
|
35
|
-
| 401 | `nonce_replayed` | Nonce 重放攻击,请求已被拒绝 |
|
|
36
|
-
| 401 | `timestamp_expired` | 时间戳超出允许范围(±5 分钟) |
|
|
37
|
-
| 401 | `user_id_mismatch` | user_id 与 License 不匹配 |
|
|
38
|
-
| 401 | `device_revoked` | 设备已被撤销(heartbeat 专用) |
|
|
39
|
-
| 403 | `license_revoked` | License 已被撤销 |
|
|
40
|
-
| 403 | `license_expired` | License 已过期 |
|
|
41
|
-
| 403 | `device_revoked` | 设备已被撤销 |
|
|
42
|
-
| 403 | `device_limit_reached` | 已达到设备数量上限 |
|
|
43
|
-
| 403 | `invalid_status` | License 状态不允许操作 |
|
|
44
|
-
| 404 | `invalid_license` | License 不存在 |
|
|
45
|
-
| 404 | `device_not_found` | 设备未激活,请先调用激活接口 |
|
|
46
|
-
| 429 | `rate_limited` | 请求过于频繁,请稍后重试 |
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## 1. 激活 License
|
|
51
|
-
|
|
52
|
-
**POST** `/api/v1/licenses/activate`
|
|
53
|
-
|
|
54
|
-
首次在设备上激活 License。使用 HMAC 知识证明,License Key 本身不发送到服务器。
|
|
55
|
-
激活成功后,服务器返回许可范围内的 Skills 列表及有效期。
|
|
56
|
-
|
|
57
|
-
> 同一设备重复激活幂等安全(不会消耗新的设备配额)。
|
|
58
|
-
|
|
59
|
-
### 请求参数
|
|
60
|
-
|
|
61
|
-
| 参数 | 类型 | 必填 | 说明 |
|
|
62
|
-
|------|------|------|------|
|
|
63
|
-
| `key_hash` | string | 是 | `SHA256(license_key)` 的 hex 字符串(64 字符),用于服务器查找 License |
|
|
64
|
-
| `user_id` | string | 是 | 从 License Key 结构中提取的用户 ID(十进制字符串) |
|
|
65
|
-
| `device_id` | string | 是 | 设备唯一标识符(建议 32 字符 hex,参见设备 ID 算法) |
|
|
66
|
-
| `timestamp` | string | 是 | 当前 Unix 时间戳(秒,字符串格式),需与服务器时间误差 ≤ 5 分钟 |
|
|
67
|
-
| `nonce` | string | 是 | 32 字符随机 hex,每次请求必须唯一,10 分钟内不可重用 |
|
|
68
|
-
| `proof` | string | 是 | HMAC-SHA256 激活证明(64 字符 hex),计算方式见下方 |
|
|
69
|
-
| `device_info` | object | 否 | 设备元数据(如 OS、版本号等),仅用于管理展示 |
|
|
70
|
-
|
|
71
|
-
**proof 计算方式:**
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
message = "activate:{key_hash}:{user_id}:{device_id}:{timestamp}:{nonce}"
|
|
75
|
-
proof = HMAC-SHA256(license_key, message) // hex 编码
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### 响应参数(成功 200)
|
|
79
|
-
|
|
80
|
-
```json
|
|
81
|
-
{
|
|
82
|
-
"status": "success",
|
|
83
|
-
"data": {
|
|
84
|
-
"status": "active",
|
|
85
|
-
"expires_at": "2027-01-01T00:00:00Z",
|
|
86
|
-
"device_id": "a1b2c3d4...",
|
|
87
|
-
"device_limit": 3,
|
|
88
|
-
"activated_devices": 1,
|
|
89
|
-
"skills": [
|
|
90
|
-
{
|
|
91
|
-
"id": 42,
|
|
92
|
-
"name": "Code Review Bot",
|
|
93
|
-
"version": "1.2.0",
|
|
94
|
-
"encrypted": false,
|
|
95
|
-
"checksum": "sha256hex..."
|
|
96
|
-
}
|
|
97
|
-
]
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
| 字段 | 类型 | 说明 |
|
|
103
|
-
|------|------|------|
|
|
104
|
-
| `data.status` | string | License 状态:`active` / `assigned` |
|
|
105
|
-
| `data.expires_at` | string | ISO 8601 过期时间,null 表示永久有效 |
|
|
106
|
-
| `data.device_id` | string | 当前设备 ID(回显) |
|
|
107
|
-
| `data.device_limit` | integer | 最大可激活设备数 |
|
|
108
|
-
| `data.activated_devices` | integer | 当前已激活设备数 |
|
|
109
|
-
| `data.skills` | array | License 授权的 Skill 列表(简要信息) |
|
|
110
|
-
| `data.skills[].id` | integer | Skill ID |
|
|
111
|
-
| `data.skills[].name` | string | Skill 名称 |
|
|
112
|
-
| `data.skills[].version` | string | 当前版本号 |
|
|
113
|
-
| `data.skills[].encrypted` | boolean | 是否加密 |
|
|
114
|
-
| `data.skills[].checksum` | string | 最新版本校验码 |
|
|
115
|
-
|
|
116
|
-
### curl 示例
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
# 1. 本地计算(伪代码,实际由 SDK 完成)
|
|
120
|
-
KEY="0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4"
|
|
121
|
-
KEY_HASH=$(echo -n "$KEY" | sha256sum | awk '{print $1}')
|
|
122
|
-
DEVICE_ID="a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
|
|
123
|
-
TS=$(date +%s)
|
|
124
|
-
NONCE=$(openssl rand -hex 16)
|
|
125
|
-
MSG="activate:${KEY_HASH}:42:${DEVICE_ID}:${TS}:${NONCE}"
|
|
126
|
-
PROOF=$(echo -n "$MSG" | openssl dgst -sha256 -hmac "$KEY" | awk '{print $2}')
|
|
127
|
-
|
|
128
|
-
# 2. 发起请求
|
|
129
|
-
curl -s -X POST https://your-platform.com/api/v1/licenses/activate \
|
|
130
|
-
-H "Content-Type: application/json" \
|
|
131
|
-
-d "{
|
|
132
|
-
\"key_hash\": \"${KEY_HASH}\",
|
|
133
|
-
\"user_id\": \"42\",
|
|
134
|
-
\"device_id\": \"${DEVICE_ID}\",
|
|
135
|
-
\"timestamp\": \"${TS}\",
|
|
136
|
-
\"nonce\": \"${NONCE}\",
|
|
137
|
-
\"proof\": \"${PROOF}\",
|
|
138
|
-
\"device_info\": {\"os\": \"macOS\", \"version\": \"14.0\", \"app_version\": \"1.0.0\"}
|
|
139
|
-
}"
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
**成功响应示例:**
|
|
143
|
-
```json
|
|
144
|
-
{
|
|
145
|
-
"status": "success",
|
|
146
|
-
"data": {
|
|
147
|
-
"status": "active",
|
|
148
|
-
"expires_at": "2027-03-06T10:00:00Z",
|
|
149
|
-
"device_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
|
150
|
-
"device_limit": 3,
|
|
151
|
-
"activated_devices": 1,
|
|
152
|
-
"skills": [
|
|
153
|
-
{ "id": 42, "name": "Code Review Bot", "version": "1.2.0",
|
|
154
|
-
"encrypted": false, "checksum": "abcdef1234..." }
|
|
155
|
-
]
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
---
|
|
161
|
-
|
|
162
|
-
## 2. 获取 Skills 列表
|
|
163
|
-
|
|
164
|
-
**POST** `/api/v1/licenses/skills`
|
|
165
|
-
|
|
166
|
-
获取当前 License 授权范围内的全部 Skill,包含版本信息和下载地址。
|
|
167
|
-
支持按可见性(公开/私有)和关键词过滤。
|
|
168
|
-
|
|
169
|
-
### 请求参数
|
|
170
|
-
|
|
171
|
-
**认证参数(必填,同 heartbeat / check_version):**
|
|
172
|
-
|
|
173
|
-
| 参数 | 类型 | 必填 | 说明 |
|
|
174
|
-
|------|------|------|------|
|
|
175
|
-
| `user_id` | string | 是 | 从 License Key 提取的用户 ID(十进制字符串) |
|
|
176
|
-
| `device_id` | string | 是 | 已激活的设备 ID |
|
|
177
|
-
| `timestamp` | string | 是 | 当前 Unix 时间戳(秒),与服务器误差 ≤ 5 分钟 |
|
|
178
|
-
| `nonce` | string | 是 | 32 字符随机 hex,每次请求唯一 |
|
|
179
|
-
| `signature` | string | 是 | HMAC-SHA256 请求签名(64 字符 hex),计算方式见下方 |
|
|
180
|
-
|
|
181
|
-
**signature 计算方式:**
|
|
182
|
-
|
|
183
|
-
```
|
|
184
|
-
message = "{user_id}:{device_id}:{timestamp}:{nonce}"
|
|
185
|
-
signature = HMAC-SHA256(license_key, message) // hex 编码
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
**过滤参数(可选):**
|
|
189
|
-
|
|
190
|
-
| 参数 | 类型 | 默认值 | 说明 |
|
|
191
|
-
|------|------|--------|------|
|
|
192
|
-
| `visibility` | string | `all` | `public`(公开)/ `private`(私有)/ `all`(全部) |
|
|
193
|
-
| `keyword` | string | — | 关键词,匹配 Skill 的 name 或 description(不区分大小写) |
|
|
194
|
-
|
|
195
|
-
### 响应参数(成功 200)
|
|
196
|
-
|
|
197
|
-
```json
|
|
198
|
-
{
|
|
199
|
-
"status": "success",
|
|
200
|
-
"total": 2,
|
|
201
|
-
"expires_at": "2027-01-01T00:00:00Z",
|
|
202
|
-
"skills": [
|
|
203
|
-
{
|
|
204
|
-
"id": 42,
|
|
205
|
-
"name": "Code Review Bot",
|
|
206
|
-
"slug": "code-review-bot",
|
|
207
|
-
"description": "Automated code review using AI",
|
|
208
|
-
"visibility": "public",
|
|
209
|
-
"version": "1.2.0",
|
|
210
|
-
"encrypted": false,
|
|
211
|
-
"emoji": null,
|
|
212
|
-
"download_count": 1024,
|
|
213
|
-
"latest_version": {
|
|
214
|
-
"version": "1.2.0",
|
|
215
|
-
"checksum": "a3f8b2c1d4e5...",
|
|
216
|
-
"release_notes": "Fix edge case in Python parsing",
|
|
217
|
-
"published_at": "2026-03-01T00:00:00Z",
|
|
218
|
-
"download_url": "https://bucket.s3.region.amazonaws.com/skills/abc.zip"
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
]
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
| 字段 | 类型 | 说明 |
|
|
226
|
-
|------|------|------|
|
|
227
|
-
| `total` | integer | 符合条件的 Skill 总数 |
|
|
228
|
-
| `expires_at` | string | License 过期时间(ISO 8601) |
|
|
229
|
-
| `skills[].id` | integer | Skill ID |
|
|
230
|
-
| `skills[].name` | string | Skill 名称 |
|
|
231
|
-
| `skills[].slug` | string | URL 友好标识符 |
|
|
232
|
-
| `skills[].description` | string | Skill 描述 |
|
|
233
|
-
| `skills[].visibility` | string | `public` / `private` |
|
|
234
|
-
| `skills[].version` | string | 当前版本号(SemVer) |
|
|
235
|
-
| `skills[].encrypted` | boolean | 是否加密 |
|
|
236
|
-
| `skills[].emoji` | string/null | 图标 Emoji |
|
|
237
|
-
| `skills[].download_count` | integer | 累计下载次数 |
|
|
238
|
-
| `skills[].latest_version` | object/null | 最新版本详情,无版本时为 null |
|
|
239
|
-
| `skills[].latest_version.version` | string | 版本号 |
|
|
240
|
-
| `skills[].latest_version.checksum` | string | 文件 SHA256 校验码 |
|
|
241
|
-
| `skills[].latest_version.release_notes` | string | 更新说明 |
|
|
242
|
-
| `skills[].latest_version.published_at` | string | 发布时间(ISO 8601) |
|
|
243
|
-
| `skills[].latest_version.download_url` | string/null | 文件下载直链(S3 公开 URL,无过期时间) |
|
|
244
|
-
|
|
245
|
-
### curl 示例
|
|
246
|
-
|
|
247
|
-
```bash
|
|
248
|
-
TS=$(date +%s)
|
|
249
|
-
NONCE=$(openssl rand -hex 16)
|
|
250
|
-
USER_ID="42"
|
|
251
|
-
DEVICE_ID="a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
|
|
252
|
-
KEY="0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4"
|
|
253
|
-
MSG="${USER_ID}:${DEVICE_ID}:${TS}:${NONCE}"
|
|
254
|
-
SIG=$(echo -n "$MSG" | openssl dgst -sha256 -hmac "$KEY" | awk '{print $2}')
|
|
255
|
-
|
|
256
|
-
# 获取全部 Skills
|
|
257
|
-
curl -s -X POST https://your-platform.com/api/v1/licenses/skills \
|
|
258
|
-
-H "Content-Type: application/json" \
|
|
259
|
-
-d "{
|
|
260
|
-
\"user_id\": \"${USER_ID}\",
|
|
261
|
-
\"device_id\": \"${DEVICE_ID}\",
|
|
262
|
-
\"timestamp\": \"${TS}\",
|
|
263
|
-
\"nonce\": \"${NONCE}\",
|
|
264
|
-
\"signature\": \"${SIG}\"
|
|
265
|
-
}"
|
|
266
|
-
|
|
267
|
-
# 只看公开 Skills,且名称包含 "code"
|
|
268
|
-
curl -s -X POST https://your-platform.com/api/v1/licenses/skills \
|
|
269
|
-
-H "Content-Type: application/json" \
|
|
270
|
-
-d "{
|
|
271
|
-
\"user_id\": \"${USER_ID}\",
|
|
272
|
-
\"device_id\": \"${DEVICE_ID}\",
|
|
273
|
-
\"timestamp\": \"${TS}\",
|
|
274
|
-
\"nonce\": \"${NONCE}\",
|
|
275
|
-
\"signature\": \"${SIG}\",
|
|
276
|
-
\"visibility\": \"public\",
|
|
277
|
-
\"keyword\": \"code\"
|
|
278
|
-
}"
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
---
|
|
282
|
-
|
|
283
|
-
## 3. 心跳检测
|
|
284
|
-
|
|
285
|
-
**POST** `/api/v1/licenses/heartbeat`
|
|
286
|
-
|
|
287
|
-
轻量级 License 有效性确认,建议每 1 天调用一次(或使用 SDK `start_heartbeat!` 自动化)。
|
|
288
|
-
服务器同步更新设备的 `last_seen_at` 时间。
|
|
289
|
-
|
|
290
|
-
### 请求参数
|
|
291
|
-
|
|
292
|
-
| 参数 | 类型 | 必填 | 说明 |
|
|
293
|
-
|------|------|------|------|
|
|
294
|
-
| `user_id` | string | 是 | 用户 ID |
|
|
295
|
-
| `device_id` | string | 是 | 设备 ID |
|
|
296
|
-
| `timestamp` | string | 是 | Unix 时间戳 |
|
|
297
|
-
| `nonce` | string | 是 | 随机 hex,每次唯一 |
|
|
298
|
-
| `signature` | string | 是 | HMAC-SHA256 签名 |
|
|
299
|
-
|
|
300
|
-
### 响应参数(成功 200)
|
|
301
|
-
|
|
302
|
-
```json
|
|
303
|
-
{
|
|
304
|
-
"status": "ok",
|
|
305
|
-
"expires_at": "2027-01-01T00:00:00Z"
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
| 字段 | 类型 | 说明 |
|
|
310
|
-
|------|------|------|
|
|
311
|
-
| `status` | string | 固定为 `ok` |
|
|
312
|
-
| `expires_at` | string | License 过期时间(ISO 8601) |
|
|
313
|
-
|
|
314
|
-
### curl 示例
|
|
315
|
-
|
|
316
|
-
```bash
|
|
317
|
-
TS=$(date +%s)
|
|
318
|
-
NONCE=$(openssl rand -hex 16)
|
|
319
|
-
USER_ID="42"
|
|
320
|
-
DEVICE_ID="a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
|
|
321
|
-
KEY="0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4"
|
|
322
|
-
MSG="${USER_ID}:${DEVICE_ID}:${TS}:${NONCE}"
|
|
323
|
-
SIG=$(echo -n "$MSG" | openssl dgst -sha256 -hmac "$KEY" | awk '{print $2}')
|
|
324
|
-
|
|
325
|
-
curl -s -X POST https://your-platform.com/api/v1/licenses/heartbeat \
|
|
326
|
-
-H "Content-Type: application/json" \
|
|
327
|
-
-d "{
|
|
328
|
-
\"user_id\": \"${USER_ID}\",
|
|
329
|
-
\"device_id\": \"${DEVICE_ID}\",
|
|
330
|
-
\"timestamp\": \"${TS}\",
|
|
331
|
-
\"nonce\": \"${NONCE}\",
|
|
332
|
-
\"signature\": \"${SIG}\"
|
|
333
|
-
}"
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
**响应示例:**
|
|
337
|
-
```json
|
|
338
|
-
{ "status": "ok", "expires_at": "2027-03-06T10:00:00Z" }
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
---
|
|
342
|
-
|
|
343
|
-
## License 算法与验证说明
|
|
344
|
-
|
|
345
|
-
### 一、License Key 格式
|
|
346
|
-
|
|
347
|
-
License Key 是一个 **40 个 hex 字符、5 组 8 字符**的字符串:
|
|
348
|
-
|
|
349
|
-
```
|
|
350
|
-
UUUUUUUU - PPPPPPPP - RRRRRRRR - RRRRRRRR - CCCCCCCC
|
|
351
|
-
│ │ │ │
|
|
352
|
-
user_id plan_id random(8字节熵) HMAC校验位
|
|
353
|
-
(uint32) (uint32) 64-bit 随机数 (前4字节)
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
**示例:**
|
|
357
|
-
```
|
|
358
|
-
0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4
|
|
359
|
-
│ │ │ │
|
|
360
|
-
user=42 plan=7 随机 8 字节 HMAC checksum
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
**字段说明:**
|
|
364
|
-
|
|
365
|
-
| 段 | 长度 | 说明 |
|
|
366
|
-
|----|------|------|
|
|
367
|
-
| `UUUUUUUU` | 8 hex | `user_id` 大端序 uint32,客户端本地可直接解析 |
|
|
368
|
-
| `PPPPPPPP` | 8 hex | `plan_id` 大端序 uint32,标识许可计划 |
|
|
369
|
-
| `RRRRRRRR-RRRRRRRR` | 16 hex | 8 字节 SecureRandom,保证每个 Key 全局唯一 |
|
|
370
|
-
| `CCCCCCCC` | 8 hex | `HMAC-SHA256(LICENSE_SECRET, U+P+R)[0..3]` 的 hex,4 字节完整性校验 |
|
|
371
|
-
|
|
372
|
-
---
|
|
373
|
-
|
|
374
|
-
### 二、Key 生成算法(服务端)
|
|
375
|
-
|
|
376
|
-
```ruby
|
|
377
|
-
# 服务端生成(app/models/license.rb)
|
|
378
|
-
def generate_key
|
|
379
|
-
u = [user_id].pack('N').unpack1('H*').upcase # uint32 → 8 hex
|
|
380
|
-
p = [plan_id].pack('N').unpack1('H*').upcase # uint32 → 8 hex
|
|
381
|
-
r = SecureRandom.bytes(8).unpack1('H*').upcase # 8 字节随机 → 16 hex
|
|
382
|
-
|
|
383
|
-
payload = u + p + r # 32 hex = 16 字节
|
|
384
|
-
checksum = HMAC-SHA256(LICENSE_SECRET, payload)[0, 4] # 取前 4 字节
|
|
385
|
-
checksum = checksum.unpack1('H*').upcase # → 8 hex
|
|
386
|
-
|
|
387
|
-
key = "#{u}-#{p}-#{r[0..7]}-#{r[8..15]}-#{checksum}"
|
|
388
|
-
key_hash = SHA256(key) # 用于激活时的查找索引
|
|
389
|
-
end
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
**安全属性:**
|
|
393
|
-
- `user_id`/`plan_id` 明文嵌入,客户端无需联网即可解析
|
|
394
|
-
- `random` 8 字节(64-bit 熵)保证唯一性,碰撞概率 ≈ 2^(-32) 在 40 亿次生成前
|
|
395
|
-
- `HMAC checksum` 使用独立密钥 `LICENSE_SECRET`,篡改任意字符均使校验失败
|
|
396
|
-
- `key_hash = SHA256(key)` 存储在数据库,作为激活时的查找索引(无法反推 key)
|
|
397
|
-
|
|
398
|
-
---
|
|
399
|
-
|
|
400
|
-
### 三、客户端本地解析(无需联网)
|
|
401
|
-
|
|
402
|
-
```ruby
|
|
403
|
-
# 客户端本地解析(sdk/openclacky_license_client.rb)
|
|
404
|
-
hex = key.delete('-').upcase # 去掉分隔符
|
|
405
|
-
user_id = hex[0..7].to_i(16) # 前 8 hex → integer
|
|
406
|
-
plan_id = hex[8..15].to_i(16) # 次 8 hex → integer
|
|
407
|
-
key_hash = SHA256(key) # 64 字符 hex
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
---
|
|
411
|
-
|
|
412
|
-
### 四、激活流程:HMAC 知识证明
|
|
413
|
-
|
|
414
|
-
激活阶段的核心问题:**如何向服务器证明持有 license_key,但又不发送 license_key?**
|
|
415
|
-
|
|
416
|
-
**解决方案:HMAC 知识证明(Zero-Transmission Proof)**
|
|
417
|
-
|
|
418
|
-
```
|
|
419
|
-
┌─ 客户端 ──────────────────────────────────────────────────┐
|
|
420
|
-
│ │
|
|
421
|
-
│ 1. 本地解析: │
|
|
422
|
-
│ user_id = key[0..7].to_i(16) │
|
|
423
|
-
│ key_hash = SHA256(license_key) │
|
|
424
|
-
│ │
|
|
425
|
-
│ 2. 构造证明消息: │
|
|
426
|
-
│ message = "activate:{key_hash}:{user_id}: │
|
|
427
|
-
│ {device_id}:{timestamp}:{nonce}" │
|
|
428
|
-
│ │
|
|
429
|
-
│ 3. 计算 HMAC 证明: │
|
|
430
|
-
│ proof = HMAC-SHA256(license_key, message) │
|
|
431
|
-
│ │
|
|
432
|
-
│ 4. 发送(不含 license_key): │
|
|
433
|
-
│ { key_hash, user_id, device_id, timestamp, │
|
|
434
|
-
│ nonce, proof, device_info } │
|
|
435
|
-
└───────────────────────────────────────────────────────────┘
|
|
436
|
-
│ HTTP POST │
|
|
437
|
-
▼ ▼
|
|
438
|
-
┌─ 服务端 ──────────────────────────────────────────────────┐
|
|
439
|
-
│ │
|
|
440
|
-
│ 1. 通过 key_hash 查找 License(O(1) 索引) │
|
|
441
|
-
│ 2. 验证时间戳(±5 分钟) │
|
|
442
|
-
│ 3. 验证 nonce(10 分钟 TTL,防重放) │
|
|
443
|
-
│ 4. 重建 message,使用数据库中存储的 license.key 计算: │
|
|
444
|
-
│ expected = HMAC-SHA256(license.key, message) │
|
|
445
|
-
│ 5. 常量时间比较:expected == proof │
|
|
446
|
-
│ 6. 验证通过 → 绑定设备,记录激活时间 │
|
|
447
|
-
│ │
|
|
448
|
-
└───────────────────────────────────────────────────────────┘
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
**为什么安全?**
|
|
452
|
-
- 只有持有 `license_key` 的客户端才能计算出正确的 `proof`
|
|
453
|
-
- `key_hash = SHA256(key)` 是单向函数,服务器无法从 `key_hash` 反推 `key`
|
|
454
|
-
- `nonce` 一次性使用,攻击者截获请求后无法重放(10 分钟缓存过期)
|
|
455
|
-
- `timestamp` ±5 分钟窗口,进一步限制重放时间窗口
|
|
456
|
-
|
|
457
|
-
---
|
|
458
|
-
|
|
459
|
-
### 五、后续请求:签名认证
|
|
460
|
-
|
|
461
|
-
激活后的所有 API 调用(`skills`、`heartbeat`)使用简化的请求签名:
|
|
462
|
-
|
|
463
|
-
```
|
|
464
|
-
┌─ 客户端 ───────────────────────────────────────┐
|
|
465
|
-
│ │
|
|
466
|
-
│ message = "{user_id}:{device_id}: │
|
|
467
|
-
│ {timestamp}:{nonce}" │
|
|
468
|
-
│ signature = HMAC-SHA256(license_key, message) │
|
|
469
|
-
│ │
|
|
470
|
-
│ 发送:{ user_id, device_id, timestamp, │
|
|
471
|
-
│ nonce, signature, ...业务参数 } │
|
|
472
|
-
└─────────────────────────────────────────────────┘
|
|
473
|
-
│
|
|
474
|
-
▼
|
|
475
|
-
┌─ 服务端 ───────────────────────────────────────┐
|
|
476
|
-
│ │
|
|
477
|
-
│ 1. 通过 (device_id + user_id) 查找设备/License│
|
|
478
|
-
│ 2. 验证时间戳(±5 分钟) │
|
|
479
|
-
│ 3. 验证 nonce(防重放) │
|
|
480
|
-
│ 4. Cross-check: │
|
|
481
|
-
│ params.user_id == license_plan.user_id │
|
|
482
|
-
│ 5. 重算签名并比较 │
|
|
483
|
-
│ 6. 更新 last_seen_at │
|
|
484
|
-
│ │
|
|
485
|
-
└─────────────────────────────────────────────────┘
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
**Cross-check 的意义:**
|
|
489
|
-
攻击者即使猜到了别人的 `device_id`,也无法伪造请求——因为必须同时满足:
|
|
490
|
-
1. `user_id` 与 License 绑定的 `user_id` 一致
|
|
491
|
-
2. HMAC 签名正确(需要持有 `license_key`)
|
|
492
|
-
|
|
493
|
-
---
|
|
494
|
-
|
|
495
|
-
### 六、稳定设备 ID 算法
|
|
496
|
-
|
|
497
|
-
SDK 提供 `LicenseClient.stable_device_id` 方法,基于机器指纹生成稳定的 32 字符 hex 设备 ID:
|
|
498
|
-
|
|
499
|
-
```ruby
|
|
500
|
-
# 优先级:/etc/machine-id → /var/lib/dbus/machine-id → hostname
|
|
501
|
-
sources = ['/etc/machine-id', '/var/lib/dbus/machine-id']
|
|
502
|
-
content = sources.find { |f| File.exist?(f) }
|
|
503
|
-
&.then { |f| File.read(f).strip }
|
|
504
|
-
|
|
505
|
-
device_id = if content
|
|
506
|
-
SHA256(content)[0, 32]
|
|
507
|
-
else
|
|
508
|
-
SHA256("#{Socket.gethostname}-openclacky")[0, 32]
|
|
509
|
-
end
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
**特性:**
|
|
513
|
-
- 同一机器每次调用返回相同值(幂等)
|
|
514
|
-
- 重新安装 App 不影响设备 ID(基于硬件标识)
|
|
515
|
-
- 不含任何可识别个人身份的信息
|
|
516
|
-
|
|
517
|
-
---
|
|
518
|
-
|
|
519
|
-
### 七、安全防御矩阵
|
|
520
|
-
|
|
521
|
-
| 威胁 | 防御机制 |
|
|
522
|
-
|------|----------|
|
|
523
|
-
| License Key 网络截获 | Key 全程不传输;激活用 `key_hash + HMAC proof` |
|
|
524
|
-
| 激活 proof 重放 | `timestamp ±5min` + `nonce` 唯一性(Cache TTL 10min) |
|
|
525
|
-
| 伪造请求(不持有 key) | HMAC-SHA256 签名,key 作为签名密钥 |
|
|
526
|
-
| Device ID 碰撞伪冒 | 每次请求 cross-check `user_id == license_plan.user_id` |
|
|
527
|
-
| 伪造 License Key 格式 | Key 含 `HMAC(LICENSE_SECRET, payload)` 校验位,服务端快速拒绝 |
|
|
528
|
-
| SHA256(key) 碰撞攻击 | 碰撞空间 2^256,实际不可行 |
|
|
529
|
-
| 撤销不传播 | 每次请求查询 `device.revoked_at` + `license.status` |
|
|
530
|
-
| 暴力枚举 key_hash | `rack-attack`:激活 10 次/小时/IP,API 120 次/小时/设备 |
|
|
531
|
-
| 时序攻击(签名比较) | `ActiveSupport::SecurityUtils.secure_compare` 常量时间 |
|
|
532
|
-
| 多设备超限 | `device_limit = license_plan.seats`,激活时强制检查 |
|
|
533
|
-
|
|
534
|
-
---
|
|
535
|
-
|
|
536
|
-
### 八、速率限制
|
|
537
|
-
|
|
538
|
-
| 规则 | 限制 | 周期 | 维度 |
|
|
539
|
-
|------|------|------|------|
|
|
540
|
-
| 激活接口 | 10 次 | 1 小时 | 客户端 IP |
|
|
541
|
-
| 所有 License API | 120 次 | 1 小时 | device_id |
|
|
542
|
-
|
|
543
|
-
触发限制时响应:
|
|
544
|
-
```json
|
|
545
|
-
HTTP 429
|
|
546
|
-
{ "status": "error", "code": "rate_limited", "message": "Rate limit exceeded. Please try again later." }
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
响应头包含 `Retry-After: <seconds>`。
|
|
550
|
-
|
|
551
|
-
---
|
|
552
|
-
|
|
553
|
-
### 九、SDK 快速接入(Ruby)
|
|
554
|
-
|
|
555
|
-
```ruby
|
|
556
|
-
require_relative 'sdk/openclacky_license_client'
|
|
557
|
-
|
|
558
|
-
# 初始化(仅需一次)
|
|
559
|
-
client = OpenClacky::LicenseClient.new(
|
|
560
|
-
base_url: "https://your-platform.com",
|
|
561
|
-
device_info: { os: RUBY_PLATFORM, app_version: "1.0.0" },
|
|
562
|
-
store: OpenClacky::LicenseStore.new("~/.myapp/license.json")
|
|
563
|
-
)
|
|
564
|
-
client.load_license("0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4")
|
|
565
|
-
|
|
566
|
-
# 激活(首次运行)
|
|
567
|
-
license_info = client.activate!
|
|
568
|
-
|
|
569
|
-
# 获取 Skills 列表
|
|
570
|
-
result = client.list_skills # 全部
|
|
571
|
-
result = client.list_skills(visibility: 'public') # 仅公开
|
|
572
|
-
result = client.list_skills(keyword: 'code review') # 关键词搜索
|
|
573
|
-
|
|
574
|
-
# 下载 Skill(获取直链后自行下载)
|
|
575
|
-
skill = result['skills'].first
|
|
576
|
-
download_url = skill['latest_version']['download_url']
|
|
577
|
-
|
|
578
|
-
# 启动后台心跳(每 10 分钟自动检测)
|
|
579
|
-
client.start_heartbeat!(
|
|
580
|
-
on_revoked: ->(e) { puts "License 已撤销:#{e.message}"; exit 1 },
|
|
581
|
-
on_expired: ->(e) { puts "License 已过期,请续费" },
|
|
582
|
-
on_error: ->(e) { puts "心跳失败(将自动重试):#{e.message}" }
|
|
583
|
-
)
|
|
584
|
-
```
|
data/docs/security-design.md
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# openclacky 安全设计方案
|
|
2
|
-
|
|
3
|
-
> 创建时间:2026-03-14
|
|
4
|
-
> 背景:用户反馈 openclacky 可以操作任意文件和电脑,感觉不安全。本文档梳理现有安全机制和待实施的安全策略。
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## 一、现有安全机制
|
|
9
|
-
|
|
10
|
-
| 机制 | 说明 |
|
|
11
|
-
|---|---|
|
|
12
|
-
| 权限模式 | CLI 默认 `confirm_safes`,危险操作需用户确认 |
|
|
13
|
-
| SafeShell | `rm` → 软删除到 trash、`sudo` 拦截、`curl \| bash` 拦截 |
|
|
14
|
-
| 文件 diff 预览 | `write`/`edit` 操作前展示变更内容 |
|
|
15
|
-
| 安全日志 | 每个项目有 `~/.clacky/safety_logs/<hash>/safety.log` 记录拦截记录 |
|
|
16
|
-
| 受保护文件 | `.env`、`.ssh/`、`.aws/`、`Gemfile` 等不能被删除 |
|
|
17
|
-
|
|
18
|
-
### 权限模式说明
|
|
19
|
-
|
|
20
|
-
| 模式 | 行为 | 适用场景 |
|
|
21
|
-
|---|---|---|
|
|
22
|
-
| `confirm_safes` | 只读操作自动执行,写/危险 shell 需用户确认 | **CLI 默认** |
|
|
23
|
-
| `confirm_all` | 遇到 `request_user_feedback` 才等待人工 | WebUI session 默认 |
|
|
24
|
-
| `auto_approve` | 全自动执行所有工具,无需确认 | 定时任务默认 |
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## 二、现有安全薄弱点
|
|
29
|
-
|
|
30
|
-
1. **`write` 工具无路径限制** — 可写到项目目录外的任意路径(如 `~/.bashrc`)
|
|
31
|
-
2. **`shell` 和 `safe_shell` 同时注册** — AI 可能绕过 safe_shell 直接用 `shell`
|
|
32
|
-
3. **WebUI 无认证** — 任何能访问本地端口的进程或局域网用户都能控制 Agent
|
|
33
|
-
4. **write 覆盖无备份** — 文件被 AI 改写后无法恢复(rm 有软删除,write 没有)
|
|
34
|
-
5. **安全机制不可见** — 用户感知不到现有保护,导致不信任
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## 三、安全策略(待实施)
|
|
39
|
-
|
|
40
|
-
### 🟢 第一梯队:低成本、高收益(推荐优先)
|
|
41
|
-
|
|
42
|
-
#### 1. 操作审计日志
|
|
43
|
-
- Agent 执行的每个 `write`/`edit`/`shell` 操作,写入 `~/.clacky/audit.log`
|
|
44
|
-
- 格式:时间戳 + 项目 + 操作类型 + 详情
|
|
45
|
-
- 提供 `clacky audit` 命令查看历史操作
|
|
46
|
-
- **价值**:让用户看得见 AI 做了什么,信任感最强
|
|
47
|
-
|
|
48
|
-
#### 2. 权限模式状态可见化
|
|
49
|
-
- WebUI 顶部常驻显示当前权限级别,颜色区分
|
|
50
|
-
- 🔴 `auto_approve` — 全自动,无需确认
|
|
51
|
-
- 🟡 `confirm_safes` — 危险操作需确认(推荐)
|
|
52
|
-
- 🟢 `confirm_all` — 所有操作均需确认
|
|
53
|
-
- CLI 启动时打印当前权限模式
|
|
54
|
-
|
|
55
|
-
#### 3. 统一走 safe_shell(堵漏洞)
|
|
56
|
-
- 移除裸 `shell` 工具,只保留 `safe_shell`
|
|
57
|
-
- 确保所有 shell 命令都经过安全替换器处理
|
|
58
|
-
- 成本极低,安全提升明显
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
### 🟡 第二梯队:中等成本、用户感知强
|
|
63
|
-
|
|
64
|
-
#### 4. write/edit 路径白名单
|
|
65
|
-
- 只允许写项目工作目录内的文件
|
|
66
|
-
- 写项目外路径(如 `~/.bashrc`、`~/.ssh/`)需额外确认,或默认拒绝
|
|
67
|
-
- 防止 AI 越界修改系统/用户配置文件
|
|
68
|
-
|
|
69
|
-
#### 5. WebUI 本地认证 Token
|
|
70
|
-
- 启动 Web 服务时生成随机 token,终端显示带 token 的访问链接
|
|
71
|
-
- 防止本地其他进程或局域网内的人劫持 Agent
|
|
72
|
-
- 参考:Jupyter Notebook 的成熟方案
|
|
73
|
-
|
|
74
|
-
#### 6. write 操作自动备份(撤销支持)
|
|
75
|
-
- `write` 覆盖文件前,自动备份原文件到 `~/.clacky/trash/`
|
|
76
|
-
- 支持 `clacky undo` 恢复被 AI 改写的文件
|
|
77
|
-
- 用户最担心的就是"AI 把我文件改了找不回来"
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
### 🔵 第三梯队:大投入、长期规划
|
|
82
|
-
|
|
83
|
-
#### 7. 沙箱模式(Docker)
|
|
84
|
-
- 可选的隔离执行环境,把 Agent 限制在容器内
|
|
85
|
-
- 主机文件系统与 Agent 完全隔离
|
|
86
|
-
- 适合高敏感场景,启动成本高、对体验有影响
|
|
87
|
-
|
|
88
|
-
#### 8. 网络访问白名单
|
|
89
|
-
- 控制 `web_fetch`/`curl` 能访问的域名范围
|
|
90
|
-
- 适合企业级部署场景
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## 四、实施优先级建议
|
|
95
|
-
|
|
96
|
-
1. **审计日志** — 让用户"看得见"AI 做了什么,信任感提升最显著
|
|
97
|
-
2. **write/edit 路径白名单** — 堵最大的安全漏洞
|
|
98
|
-
3. **统一走 safe_shell** — 低成本堵漏,可顺手实施
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## 五、让用户放心的核心原则
|
|
103
|
-
|
|
104
|
-
> **安全机制不够,用户感知更重要。**
|
|
105
|
-
|
|
106
|
-
- 用户能看到 AI 正在做什么(实时显示)
|
|
107
|
-
- 用户能看到 AI 做过什么(审计日志)
|
|
108
|
-
- 用户能撤销 AI 的操作(备份+恢复)
|
|
109
|
-
- 用户能控制 AI 的权限(权限模式可见+可切换)
|